mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: Minimal Tailwind+Alpine landing + shortener UI
This commit is contained in:
246
index.html
246
index.html
@@ -1,157 +1,159 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="h-full">
|
||||
<!doctype html><html lang=en class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>4ev.link — shorten, 4ever</title>
|
||||
<meta name="description" content="Get a forever short link. Free random slugs. Paid: custom URLs, analytics, proxy/CDN, your subdomain (soon).">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Ctext y='.9em' font-size='52'%3E🔗%3C/text%3E%3C/svg%3E">
|
||||
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>4ev.link — Short links that last</title>
|
||||
<meta name=description content="Get your 4ev.link. Free random short links. Paid: custom URLs, analytics, CDN proxy, your own subdomain (coming soon).">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath fill='%23ff2d55' d='M16 29S2 22 2 11a7 7 0 0 1 13-3 7 7 0 0 1 13 3c0 11-14 18-14 18'/%3E%3C/svg%3E">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>[x-cloak]{display:none}</style>
|
||||
<script>tailwind.config={theme:{extend:{colors:{bg:'#0b0b0d'}}}}</script>
|
||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
</head>
|
||||
<body class="min-h-full bg-black text-neutral-200 selection:bg-neutral-800">
|
||||
<header class="sticky top-0 z-10 border-b border-neutral-900/80 bg-black/60 backdrop-blur">
|
||||
<nav class="mx-auto max-w-3xl px-4 py-3 flex items-center justify-between">
|
||||
<a href="/" class="font-semibold text-white tracking-tight">4ev.link</a>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="#" class="px-3 py-1.5 rounded-lg border border-neutral-800 hover:bg-neutral-900">Log in</a>
|
||||
<a href="#" class="px-3 py-1.5 rounded-lg bg-white text-black hover:bg-neutral-200">Sign up</a>
|
||||
</div>
|
||||
</nav>
|
||||
<body class="min-h-screen bg-bg text-gray-100 antialiased selection:bg-white/10 selection:text-white"
|
||||
x-data="{
|
||||
u:'', r:'', e:'', busy:!1,
|
||||
async shorten(){
|
||||
this.e=''; this.r='';
|
||||
let v=this.u?.trim();
|
||||
if(!v) return this.e='Please paste a URL';
|
||||
try{ new URL(v) }catch(_){ return this.e='Invalid URL' }
|
||||
this.busy=!0;
|
||||
try{
|
||||
let res=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:v})});
|
||||
if(!res.ok){ this.e=(await res.text())||'Failed to shorten'; }
|
||||
else { let d=await res.json(); this.r=d.shortUrl }
|
||||
}catch(_){ this.e='Network error' }
|
||||
this.busy=!1; this.$nextTick(()=>window.lucide&&lucide.createIcons())
|
||||
},
|
||||
copy(){ navigator.clipboard.writeText(this.r) }
|
||||
}" x-init="$nextTick(()=>window.lucide&&lucide.createIcons())">
|
||||
|
||||
<header class="sticky top-0 z-10 border-b border-white/5 bg-bg/80 backdrop-blur">
|
||||
<div class="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2 font-semibold text-white">
|
||||
<i data-lucide="infinity" class="w-5 h-5 text-white/80"></i>
|
||||
4ev.link
|
||||
<span class="ml-2 rounded-full bg-white/5 px-2 py-0.5 text-xs text-white/60">beta</span>
|
||||
</a>
|
||||
<nav class="flex items-center gap-2">
|
||||
<a href="/login" class="px-3 py-1.5 rounded-md text-sm text-white/80 hover:text-white hover:bg-white/5 transition">
|
||||
<i data-lucide="log-in" class="w-4 h-4 inline -mt-0.5 mr-1"></i> Log in
|
||||
</a>
|
||||
<a href="/signup" class="px-3 py-1.5 rounded-md text-sm bg-white text-black hover:bg-gray-200 transition">
|
||||
<i data-lucide="user-plus" class="w-4 h-4 inline -mt-0.5 mr-1"></i> Sign up
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto max-w-3xl px-4">
|
||||
<section class="pt-16 pb-10 text-center">
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-semibold text-white">
|
||||
Get ur 4ev.link <span class="align-middle text-red-500">❤️</span>
|
||||
</h1>
|
||||
<p class="mt-3 text-neutral-400 max-w-xl mx-auto">
|
||||
Minimal, fast, forever. Random slugs for free. Paid gets custom URLs, analytics, proxy/CDN, your subdomain (soon).
|
||||
</p>
|
||||
</section>
|
||||
<main class="mx-auto max-w-6xl px-4">
|
||||
<section class="pt-16 sm:pt-24">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight text-white">
|
||||
Get ur 4ev.link <span class="align-middle">❤️</span>
|
||||
</h1>
|
||||
<p class="mt-3 text-sm sm:text-base text-white/60">
|
||||
Minimal, fast, and forever-ish. Free gives you a random short link. Paid adds power.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section x-data="app" x-cloak x-effect="lucide.createIcons()" class="mx-auto max-w-2xl">
|
||||
<form @submit.prevent="go" class="rounded-2xl border border-neutral-900 bg-neutral-950/60 shadow-xl">
|
||||
<div class="p-3 sm:p-4 flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-500">
|
||||
<i data-lucide="link-2" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input x-model.trim="url" type="url" inputmode="url" name="url" required
|
||||
placeholder="https://example.com"
|
||||
class="w-full rounded-xl border border-neutral-800 bg-transparent pl-9 pr-3 py-3 outline-none ring-0 focus:border-neutral-500"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" :disabled="busy"
|
||||
class="inline-flex items-center gap-2 rounded-xl bg-white text-black px-4 py-3 font-medium hover:bg-neutral-200 disabled:opacity-50">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||
<span x-text="busy?'Working…':'Shorten'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<template x-if="err">
|
||||
<div class="px-4 pb-4 text-left">
|
||||
<div class="text-sm text-red-400 flex items-center gap-2">
|
||||
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
||||
<span x-text="err"></span>
|
||||
<form @submit.prevent="shorten" class="mx-auto mt-8 w-full max-w-2xl">
|
||||
<div class="rounded-xl bg-white/5 ring-1 ring-white/10 p-2 sm:p-3">
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
||||
<div class="relative flex-1">
|
||||
<i data-lucide="link" class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-white/40"></i>
|
||||
<input x-model="u" type="url" inputmode="url" required
|
||||
placeholder="https://example.com/very/long/url"
|
||||
class="w-full bg-transparent pl-9 pr-3 py-3 rounded-lg text-white placeholder-white/30 outline-none border border-transparent focus:border-white/20">
|
||||
</div>
|
||||
<button type="submit" :disabled="busy"
|
||||
class="inline-flex items-center justify-center gap-2 rounded-lg bg-white text-black px-4 py-3 font-medium hover:bg-gray-200 disabled:opacity-60 disabled:hover:bg-white transition">
|
||||
<template x-if="!busy"><i data-lucide="wand-2" class="w-4 h-4"></i></template>
|
||||
<template x-if="busy"><i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i></template>
|
||||
Shorten
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
|
||||
<template x-if="out">
|
||||
<div class="mt-4 rounded-2xl border border-neutral-900 bg-neutral-950/60 p-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<i data-lucide="link" class="w-4 h-4 text-neutral-400 shrink-0"></i>
|
||||
<a :href="out.shortUrl" target="_blank" rel="noopener noreferrer"
|
||||
class="truncate text-white hover:underline" x-text="out.shortUrl"></a>
|
||||
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-2 text-xs text-white/50">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="hash" class="w-3.5 h-3.5"></i>
|
||||
Free: random slug
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="copy" class="px-3 py-2 rounded-lg border border-neutral-800 hover:bg-neutral-900 inline-flex items-center gap-2">
|
||||
<i data-lucide="copy" class="w-4 h-4" :class="copied?'hidden':''"></i>
|
||||
<i data-lucide="check" class="w-4 h-4 text-emerald-400" :class="copied?'':'hidden'"></i>
|
||||
<span class="text-sm" x-text="copied?'Copied!':'Copy'"></span>
|
||||
<i data-lucide="pencil" class="w-3.5 h-3.5"></i>
|
||||
Custom URLs <span class="ml-1 rounded bg-white/10 px-1.5 py-0.5 text-[10px] text-white/70">paid</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
|
||||
Analytics • Proxy (CDN) • Subdomain <span class="ml-1 rounded bg-white/10 px-1.5 py-0.5 text-[10px] text-white/70">coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p x-show="e" x-text="e" class="mt-3 text-sm text-rose-400" x-cloak></p>
|
||||
|
||||
<div x-show="r" x-cloak class="mt-4 rounded-xl border border-white/10 bg-white/5 p-3">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div class="truncate">
|
||||
<span class="text-xs text-white/50">Your short link</span>
|
||||
<div class="mt-0.5 font-mono text-sm sm:text-base text-white truncate">
|
||||
<a :href="r" x-text="r" class="hover:underline" target="_blank" rel="noopener"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" @click="copy" class="inline-flex items-center gap-2 rounded-lg bg-white/10 hover:bg-white/15 px-3 py-2 text-sm">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i> Copy
|
||||
</button>
|
||||
<a :href="out.shortUrl" target="_blank" rel="noopener noreferrer"
|
||||
class="px-3 py-2 rounded-lg bg-white text-black hover:bg-neutral-200 inline-flex items-center gap-2">
|
||||
<i data-lucide="arrow-up-right" class="w-4 h-4"></i>
|
||||
<span class="text-sm">Open</span>
|
||||
<a :href="r" target="_blank" rel="noopener"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-white text-black px-3 py-2 text-sm hover:bg-gray-200">
|
||||
Open <i data-lucide="external-link" class="w-4 h-4"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-neutral-500">
|
||||
<span class="font-mono text-neutral-400" x-text="out.slug"></span>
|
||||
<span class="mx-1">→</span>
|
||||
<span class="break-all" x-text="out.target"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="mt-10 grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||
<section class="mx-auto mt-16 sm:mt-24 max-w-3xl">
|
||||
<div class="grid sm:grid-cols-3 gap-4">
|
||||
<div class="rounded-lg border border-white/10 p-4 bg-white/5">
|
||||
<div class="flex items-center gap-2 text-white">
|
||||
<i data-lucide="dice-5" class="w-4 h-4"></i><b>Free</b>
|
||||
<i data-lucide="zap" class="w-4 h-4 text-white/70"></i><span class="text-sm font-semibold">Fast</span>
|
||||
</div>
|
||||
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||
<li class="flex gap-2"><i data-lucide="check" class="w-4 h-4 text-neutral-500"></i>Random slug</li>
|
||||
<li class="flex gap-2"><i data-lucide="zap" class="w-4 h-4 text-neutral-500"></i>Instant redirect</li>
|
||||
</ul>
|
||||
<p class="mt-1 text-sm text-white/60">Edge-hosted redirects with no fuss.</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||
<div class="rounded-lg border border-white/10 p-4 bg-white/5">
|
||||
<div class="flex items-center gap-2 text-white">
|
||||
<i data-lucide="star" class="w-4 h-4"></i><b>Paid</b> <span class="text-xs text-neutral-400">(coming soon)</span>
|
||||
<i data-lucide="shield" class="w-4 h-4 text-white/70"></i><span class="text-sm font-semibold">Safe</span>
|
||||
</div>
|
||||
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||
<li class="flex gap-2"><i data-lucide="type" class="w-4 h-4 text-neutral-500"></i>Custom URLs</li>
|
||||
<li class="flex gap-2"><i data-lucide="bar-chart-2" class="w-4 h-4 text-neutral-500"></i>Analytics</li>
|
||||
<li class="flex gap-2"><i data-lucide="shield" class="w-4 h-4 text-neutral-500"></i>Proxy/CDN</li>
|
||||
<li class="flex gap-2"><i data-lucide="globe" class="w-4 h-4 text-neutral-500"></i>Your subdomain</li>
|
||||
</ul>
|
||||
<p class="mt-1 text-sm text-white/60">Validate URLs client + server side.</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||
<div class="rounded-lg border border-white/10 p-4 bg-white/5">
|
||||
<div class="flex items-center gap-2 text-white">
|
||||
<i data-lucide="info" class="w-4 h-4"></i><b>How it works</b>
|
||||
<i data-lucide="settings" class="w-4 h-4 text-white/70"></i><span class="text-sm font-semibold">Scalable</span>
|
||||
</div>
|
||||
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||
<li>Paste a URL → Shorten</li>
|
||||
<li>We store target & redirect</li>
|
||||
<li>No account needed (free)</li>
|
||||
</ul>
|
||||
<p class="mt-1 text-sm text-white/60">Built on Cloudflare KV and Workers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center text-sm text-neutral-500">
|
||||
Worker: <code class="text-neutral-400">ev.awww.workers.dev</code> · Built on Cloudflare
|
||||
<div class="mt-6 text-center text-xs text-white/40">
|
||||
Paid: custom slugs, analytics, proxy (CDN), your own subdomain — coming soon.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="mt-16 pb-10 text-center text-xs text-neutral-500">
|
||||
© <span x-data x-text="new Date().getFullYear()"></span> 4ev.link — keep it short, keep it 4ev
|
||||
<footer class="mt-20 border-t border-white/5">
|
||||
<div class="mx-auto max-w-6xl px-4 py-8 text-xs text-white/50 flex items-center justify-between">
|
||||
<span>© <span x-text="new Date().getFullYear()"></span> 4ev.link</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="/terms" class="hover:text-white/80">Terms</a>
|
||||
<a href="/privacy" class="hover:text-white/80">Privacy</a>
|
||||
<a href="https://github.com/4ev-link/4ev.link" class="hover:text-white/80 flex items-center gap-1">
|
||||
<i data-lucide="github" class="w-4 h-4"></i> GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script>
|
||||
const API='https://ev.awww.workers.dev';
|
||||
document.addEventListener('alpine:init',()=>Alpine.data('app',()=>({
|
||||
api:API,url:'',out:null,err:'',busy:!1,copied:!1,
|
||||
n(s){s=s.trim();if(!s)return s;if(!/^https?:\/\//i.test(s))s='https://'+s;return s},
|
||||
async go(){
|
||||
this.err=''; let s=this.n(this.url);
|
||||
try{new URL(s)}catch{this.err='Enter a valid URL';return}
|
||||
this.busy=!0;
|
||||
try{
|
||||
const r=await fetch(this.api+'/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:s})});
|
||||
if(!r.ok)throw new Error((await r.text())||r.statusText);
|
||||
this.out=await r.json(); await this.$nextTick(()=>lucide.createIcons());
|
||||
}catch(e){this.err=e.message||'Something went wrong'}
|
||||
finally{this.busy=!1}
|
||||
},
|
||||
async copy(){if(!this.out)return;try{await navigator.clipboard.writeText(this.out.shortUrl);this.copied=!0;setTimeout(()=>this.copied=!1,1200)}catch{}}
|
||||
})));
|
||||
document.addEventListener('DOMContentLoaded',()=>lucide.createIcons());
|
||||
</script>
|
||||
<script>addEventListener('DOMContentLoaded',()=>window.lucide&&lucide.createIcons())</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user