Feat: Landing page for 4ev.link

This commit is contained in:
2025-09-26 19:21:10 -07:00
parent 0da496a1e5
commit 9bc29dcc8a

View File

@@ -1,92 +1,171 @@
<!doctype html><html lang="en"><head> <!doctype html><html lang=en>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <head>
<title>4ev.link — tiny links that last</title><meta name="description" content="Privacy-first URL shortener. Free: random slug • Paid: custom URLs (coming soon). Links are forever (or until we go bankrupt)."> <meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<link rel="icon" href="data:,"><style>[x-cloak]{display:none!important}</style> <title>4ev.link — Short, simple, 4ever</title>
<meta name=description content="Free, fast URL shortener. No account needed. Random 4-char slugs now. Custom slugs coming soon.">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script> <style>
<script>tailwind.config={theme:{extend:{fontFamily:{sans:['Inter','ui-sans-serif','system-ui','-apple-system','Segoe UI','Roboto','Arial'],display:['Space Grotesk','Inter','system-ui']}}}}</script> :root{--bg:#fff;--fg:#111;--muted:#6b7280;--b:#e5e7eb;--b2:#d1d5db;--card:#fff;--shadow:0 0 0 1px rgba(0,0,0,.04),0 6px 20px rgba(0,0,0,.06);--acc:#111;--ok:#16a34a;--bad:#ef4444}
<script src="//unpkg.com/alpinejs" defer></script> *{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.5 Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
<script src="https://unpkg.com/lucide@latest" defer></script> a{color:var(--acc);text-decoration:none}a:hover{text-decoration:underline}
</head><body class="bg-white text-gray-900 antialiased"> .container{max-width:1100px;margin:auto;padding:16px}
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200"> .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}
<nav class="max-w-3xl mx-auto px-4 h-14 flex items-center justify-between"> .brand{display:flex;align-items:center;gap:10px;font:700 18px/1 Space Grotesk,Inter;color:var(--fg)}
<a href="/" class="font-semibold tracking-tight text-xl">4ev.link</a> .brand i{width:20px;height:20px}
<div class="flex items-center gap-4"> .actions{display:flex;gap:8px}
<a href="/login" class="text-sm text-gray-700 hover:text-black">Log in</a> .btn{appearance:none;border:1px solid var(--b);background:#fff;color:#111;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
<a href="/signup" class="text-sm bg-black text-white px-3 py-1.5 rounded-md hover:bg-gray-900">Sign up</a> .btn:hover{border-color:var(--b2)}
</div> .btn.primary{background:#111;color:#fff;border-color:#111}
</nav> .btn.ghost{background:transparent}
</header> .hero{padding:32px 0 18px;text-align:center}
h1{margin:0 0 8px;font:700 clamp(28px,6vw,40px)/1.15 Space Grotesk,Inter}
<main class="max-w-3xl mx-auto px-4 pt-16 pb-24" x-data="{t:'',o:'',e:'',l:!1,async s(){this.e='',this.o='';let u=this.t.trim();try{new URL(u)}catch(_){this.e='Enter a valid URL';return}this.l=!0;try{let r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:u})});if(!r.ok)throw 0;let j=await r.json();this.o=j.shortUrl;this.$nextTick(()=>lucide?.createIcons())}catch(_){this.e='Something went wrong. Try again.'}finally{this.l=!1}},async c(){try{await navigator.clipboard.writeText(this.o);let b=this.$refs.copy;b?.classList.add('text-green-600');setTimeout(()=>b?.classList.remove('text-green-600'),900)}catch(_){}}"> h1 .heart{font-size:1em;color:#e11d48}
<section class="text-center"> .sub{color:var(--muted);font-weight:600}
<h1 class="font-display text-3xl sm:text-4xl font-semibold tracking-tight inline-flex items-center gap-2 justify-center"> .card{background:var(--card);border:1px solid var(--b);border-radius:14px;box-shadow:var(--shadow)}
<span>Get ur 4ev.link</span> .form{margin:18px auto 0;max-width:780px;padding:14px}
<span aria-hidden="true" class="inline-block align-[-2px]"> .row{display:flex;gap:8px;align-items:center}
<svg viewBox="0 0 24 24" class="w-6 h-6 text-red-500" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> .row>.grow{flex:1}
<path d="M12.1 21.35c-.1.06-.22.06-.32 0C7.14 18.23 2 14.52 2 9.86 2 7.2 4.15 5 6.86 5c1.6 0 3.11.76 4.14 1.98C12.99 5.76 14.5 5 16.1 5 18.81 5 21 7.2 21 9.86c0 4.66-5.14 8.37-8.9 11.49z"/> .input{width:100%;border:1px solid var(--b);border-radius:10px;padding:12px 14px;font-size:15px}
</svg> .input:focus{outline:none;border-color:#9ca3af;box-shadow:0 0 0 3px rgba(0,0,0,.06)}
</span> .help{margin-top:8px;color:var(--muted);font-size:13px}
</h1> .result{margin-top:12px;padding:12px;border:1px dashed var(--b);border-radius:10px;display:flex;align-items:center;justify-content:space-between;gap:8px}
<p class="mt-3 text-gray-600 text-sm">Free: random short link • Paid: custom URLs .result .link{font:600 15px/1.4 "JetBrains Mono",monospace;word-break:break-all}
<span class="ml-1 inline-flex items-center gap-1 rounded border border-gray-200 px-1.5 py-0.5 text-[11px] text-gray-700">coming soon</span> .badge{display:inline-flex;gap:6px;align-items:center;border:1px solid var(--b);padding:6px 10px;border-radius:999px;font-size:12px;color:#111;background:#fff}
</p> .grid{display:grid;gap:12px;margin:24px 0}
<div class="mt-4 flex flex-wrap items-center justify-center gap-2"> @media(min-width:760px){.grid{grid-template-columns:repeat(3,1fr)}}
<span class="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 shadow-sm"> .feature{padding:16px}
<i data-lucide="shield" class="w-3.5 h-3.5"></i><span>Privacy-first</span> .feature i{width:18px;height:18px}
</span> small.muted{color:var(--muted)}
<span class="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 shadow-sm"> .footer{margin:26px 0;color:var(--muted);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
<i data-lucide="ban" class="w-3.5 h-3.5"></i><span>No pixel tracking</span> .modal{position:fixed;inset:0;display:grid;place-items:center;background:rgba(17,17,17,.42);padding:16px}
</span> .sheet{max-width:420px;width:100%}
<span class="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 shadow-sm"> .close{position:absolute;top:10px;right:10px;background:#fff;border:1px solid var(--b);border-radius:8px;padding:6px;cursor:pointer}
<i data-lucide="link-2" class="w-3.5 h-3.5"></i><span>Simple URL shortener</span> .disabled{opacity:.45;pointer-events:none}
</span> .kbd{font:600 12px/1 "JetBrains Mono",monospace;background:#f5f5f5;border:1px solid var(--b);border-bottom-width:2px;border-radius:6px;padding:2px 6px}
<span class="inline-flex items-center gap-1 rounded-full border border-gray-200 bg-white px-3 py-1 text-xs text-gray-700 shadow-sm"> .spinner{width:18px;height:18px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;display:inline-block;animation:s .6s linear infinite;vertical-align:middle}
<i data-lucide="infinity" class="w-3.5 h-3.5"></i><span>Links are forever (or until we go bankrupt)</span> @keyframes s{to{transform:rotate(1turn)}}
</span> .note{background:#fafafa;border:1px solid var(--b);border-radius:12px;padding:10px;font-size:13px}
</div> </style>
</section> </head>
<body x-data="app()" x-init="init()">
<section class="mt-8"> <div class=container>
<form @submit.prevent="s()" class="flex items-stretch gap-2"> <nav class=nav>
<input x-model="t" type="url" inputmode="url" placeholder="https://example.com/very/long/link" <a class=brand href="/">
class="w-full rounded-md border border-gray-300 bg-white px-3 py-3 text-[15px] placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-900/10 focus:border-gray-400" required> <i data-lucide="link-2"></i><span>4ev.link</span>
<button type="submit" :disabled="l"
class="shrink-0 inline-flex items-center justify-center rounded-md bg-black text-white px-4 py-3 text-sm font-medium hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed">
<span x-text="l?'Shortening…':'Shorten'"></span>
</button>
</form>
<p x-show="e" x-text="e" class="mt-2 text-sm text-red-600" role="alert"></p>
</section>
<section x-show="o" x-cloak class="mt-6">
<div class="rounded-md border border-gray-200 bg-white p-4 flex items-center justify-between">
<a :href="o" target="_blank" rel="noopener" class="font-medium text-gray-900 hover:underline truncate max-w-[75%]">
<i data-lucide="sparkles" class="inline w-4 h-4 mr-1 text-gray-500"></i><span x-text="o"></span>
</a> </a>
<div class="flex items-center gap-2"> <div class=actions>
<a :href="o" target="_blank" rel="noopener" class="inline-flex items-center gap-1 text-sm text-gray-700 hover:text-black px-2 py-1 rounded-md border border-gray-200"> <button class="btn ghost" @click="openAuth('login')"><i data-lucide="log-in"></i></button>
<i data-lucide="external-link" class="w-4 h-4"></i><span>Open</span> <button class="btn" @click="openAuth('signup')">Sign up</button>
</a> </div>
<button @click="c()" class="inline-flex items-center gap-1 text-sm text-gray-700 hover:text-black px-2 py-1 rounded-md border border-gray-200"> </nav>
<i data-lucide="copy" class="w-4 h-4" x-ref="copy"></i><span>Copy</span>
<header class=hero>
<h1>Get ur 4ev.link <span class=heart>❤️</span></h1>
<div class=sub>Free. No account needed. Cloudflare-fast.</div>
</header>
<section class="card form" @keydown.enter.prevent="shorten()">
<div class=row>
<div class=grow><input class=input type=url x-model.trim="url" placeholder="Paste a long URL, e.g. https://example.com/some/page?ref=4ev" aria-label="Long URL"></div>
<button class="btn primary" :disabled="busy||!url" @click="shorten()">
<span x-show="!busy"><i data-lucide="wand-2"></i> Shorten</span>
<span x-show="busy"><span class=spinner></span> Working…</span>
</button> </button>
</div> </div>
</div>
<p class="mt-2 text-xs text-gray-500">Free plan generates random slugs. Custom URLs for paid — coming soon.</p>
</section>
</main>
<footer class="border-t border-gray-200"> <div class=help>
<div class="max-w-3xl mx-auto px-4 py-8 text-sm text-gray-600 flex items-center justify-between"> • Free plan: random 4-char slugs, e.g. <span class=kbd>/aZ3b</span> • Paid: custom slugs <span class=badge><i data-lucide="clock-3"></i> coming soon</span>
<span>© <span x-text="new Date().getFullYear()"></span> 4ev.link</span> </div>
<div class="flex gap-4">
<a href="https://github.com/4ev-link/4ev.link" class="hover:text-black inline-flex items-center gap-1"><i data-lucide="github" class="w-4 h-4"></i>GitHub</a> <template x-if="err">
<a href="/pricing" class="hover:text-black">Pricing</a> <div class="note" style="color:var(--bad);margin-top:10px"><i data-lucide="alert-triangle"></i> <span x-text="err"></span></div>
</div> </template>
<template x-if="res">
<div class=result>
<div class=link>
<div>Your 4ev.link</div>
<a :href="res.shortUrl" target=_blank rel=noopener x-text="res.shortUrl"></a>
</div>
<div style="display:flex;gap:8px">
<button class=btn @click="copy(res.shortUrl)"><i data-lucide="copy"></i><span x-text="copied?'Copied!':'Copy'"></span></button>
<a class="btn" :href="res.shortUrl" target=_blank rel=noopener><i data-lucide="external-link"></i> Open</a>
</div>
</div>
</template>
<details style="margin-top:12px">
<summary class="badge"><i data-lucide="sparkles"></i> Custom URL (paid, soon)</summary>
<div class="row" style="margin-top:10px">
<span class="badge disabled"><i data-lucide="lock"></i> yourbrand</span>
<input class="input disabled" disabled placeholder="Choose your slug (paid soon)">
<button class="btn disabled"><i data-lucide="check"></i> Reserve</button>
</div>
<div class="help">Want this sooner? <a href="#" @click.prevent="openAuth('signup')">Sign up</a> to get notified.</div>
</details>
</section>
<section class="grid">
<article class="card feature">
<div class="badge"><i data-lucide="zap"></i> Fast redirects</div>
<p>Backed by Cloudflare Workers + KV, your links resolve globally with 302 redirects.</p>
</article>
<article class="card feature">
<div class="badge"><i data-lucide="shield"></i> Private by default</div>
<p>No account required to shorten. We only store your long URL and a short code.</p>
</article>
<article class="card feature">
<div class="badge"><i data-lucide="terminal"></i> Simple API</div>
<p>POST / with {"url": "..."} → {"shortUrl": "..."} — exactly what this page uses.</p>
</article>
</section>
<section class="card feature">
<div class="badge"><i data-lucide="info"></i> How it works</div>
<ol style="margin:10px 0 0;padding-left:18px">
<li>Paste your long URL.</li>
<li>We validate and create a random 4-char slug.</li>
<li>Share your 4ev.link — visitors get a 302 redirect to your target.</li>
</ol>
</section>
<footer class=footer>
<div>© <span x-text="year"></span> 4ev.link · <a href="#" @click.prevent="openAuth('login')">Log in</a></div>
<div><small class=muted>Made with <span class=heart>❤️</span> · Inspired by Cloudflare · Fonts: Inter + Space Grotesk + JetBrains Mono</small></div>
</footer>
</div> </div>
</footer>
<script>document.addEventListener('alpine:init',()=>{lucide?.createIcons()})</script> <template x-if="auth.open">
</body></html> <div class=modal @click.self="auth.open=false" x-trap.inert.noscroll="auth.open">
<div class="sheet card" style="position:relative;padding:16px">
<button class=close @click="auth.open=false"><i data-lucide="x"></i></button>
<h3 style="margin:0 0 8px;font:700 20px/1.2 Space Grotesk">[[ <span x-text="auth.mode==='login'?'Login':'Sign up'"></span> ]]</h3>
<p class=help x-show="auth.mode==='signup'">Custom URLs and teams coming soon — join the waitlist.</p>
<div class="grid" style="grid-template-columns:1fr">
<input class=input type=email x-model.trim="auth.email" placeholder="you@example.com" autocomplete="email">
<button class="btn primary" @click="notify()"><i data-lucide="mail"></i> <span x-text="auth.mode==='login'?'Send magic link':'Notify me'"></span></button>
</div>
<p class="help" style="margin-top:8px">Well email you when paid plans launch. No spam, unsubscribe anytime.</p>
</div>
</div>
</template>
<script src="//unpkg.com/alpinejs" defer></script>
<script src="https://unpkg.com/lucide@latest" defer></script>
<script>
const app=()=>({year:new Date().getFullYear(),url:'',res:null,err:'',busy:!1,copied:!1,auth:{open:!1,mode:'login',email:''},
init(){addEventListener('load',()=>window.lucide&&lucide.createIcons())},
openAuth(m){this.auth={open:!0,mode:m,email:''};setTimeout(()=>window.lucide&&lucide.createIcons(),0)},
norm(u){return/^[a-z][a-z0-9+.-]*:\/\//i.test(u)?u:'https://'+u},
async shorten(){this.err='';this.res=null;if(!this.url)return;
try{this.busy=!0;const r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.norm(this.url)})});
if(!r.ok)throw new Error((await r.text())||'Failed to shorten');this.res=await r.json();this.$nextTick(()=>window.lucide&&lucide.createIcons())
}catch(e){this.err=e.message||'Something went wrong'}finally{this.busy=!1}
},
async copy(t){try{await navigator.clipboard.writeText(t);this.copied=!0;setTimeout(()=>this.copied=!1,1200)}catch(e){this.err='Copy failed'}}
,notify(){this.auth.open=!1;alert('Thanks! Well keep you posted.')}
});
</script>
</body>
</html>