Feat: Landing page for 4ev.link

This commit is contained in:
2025-09-26 18:52:21 -07:00
parent af5a380b1b
commit 30545a2dff

View File

@@ -1,145 +1,171 @@
<!doctype html><html lang=en> <!doctype html><html lang=en>
<head> <head>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"> <meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>4ev.link — simple forever-short links</title> <title>4ev.link — Short, simple, 4ever</title>
<meta name=description content="4ev.link is a minimal, fast URL shortener. Free, no account needed. Paid custom URLs coming soon."> <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=Inter:wght@400;600;700&family=Space+Grotesk:wght@500;700&family=Roboto+Mono:wght@400;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=//unpkg.com/alpinejs defer></script>
<script src=https://unpkg.com/lucide@latest defer></script>
<style> <style>
:root{--bg:#fff;--fg:#111;--sub:#666;--mut:#888;--br:#e5e7eb;--card:#f8fafc;--ok:#111;--err:#b91c1c;--ring:0 0 0 3px rgba(0,0,0,.08)} :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}
*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.45 Inter,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,'Helvetica Neue',Arial,'Apple Color Emoji','Segoe UI Emoji';-webkit-font-smoothing:antialiased} *{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}
a{color:inherit;text-decoration:none}a:hover{text-decoration:underline} a{color:var(--acc);text-decoration:none}a:hover{text-decoration:underline}
.container{max-width:980px;margin:0 auto;padding:16px} .container{max-width:1100px;margin:auto;padding:16px}
.nav{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0} .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}
.brand{display:inline-flex;align-items:center;gap:8px;font-weight:700} .brand{display:flex;align-items:center;gap:10px;font:700 18px/1 Space Grotesk,Inter;color:var(--fg)}
.badge{font:700 12px/1 Inter;background:#111;color:#fff;border-radius:999px;padding:6px 10px} .brand i{width:20px;height:20px}
.actions{display:flex;gap:8px} .actions{display:flex;gap:8px}
.btn{appearance:none;border:1px solid var(--br);background:#fff;color:#111;border-radius:10px;padding:10px 14px;font-weight:600;cursor:pointer} .btn{appearance:none;border:1px solid var(--b);background:#fff;color:#111;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
.btn:hover{box-shadow:var(--ring)}.btn:disabled{opacity:.6;cursor:not-allowed} .btn:hover{border-color:var(--b2)}
.btn.black{background:#111;color:#fff;border-color:#111} .btn.primary{background:#111;color:#fff;border-color:#111}
.btn.ghost{background:transparent} .btn.ghost{background:transparent}
.hero{display:grid;gap:14px;place-items:center;text-align:center;padding:28px 0} .hero{padding:32px 0 18px;text-align:center}
h1{font:700 clamp(24px,5.5vw,42px)/1.1 'Space Grotesk',Inter,system-ui;margin:0} h1{margin:0 0 8px;font:700 clamp(28px,6vw,40px)/1.15 Space Grotesk,Inter}
h2{font:600 18px/1.2 Inter;margin:0} h1 .heart{font-size:1em;color:#e11d48}
.sub{color:var(--sub);font-size:14px} .sub{color:var(--muted);font-weight:600}
.card{background:var(--card);border:1px solid var(--br);border-radius:14px;padding:14px} .card{background:var(--card);border:1px solid var(--b);border-radius:14px;box-shadow:var(--shadow)}
.grid{display:grid;gap:12px} .form{margin:18px auto 0;max-width:780px;padding:14px}
@media(min-width:720px){.grid-2{grid-template-columns:1fr 1fr}} .row{display:flex;gap:8px;align-items:center}
.input{display:flex;gap:8px;align-items:center;background:#fff;border:1px solid var(--br);border-radius:12px;padding:8px 8px} .row>.grow{flex:1}
.input:focus-within{box-shadow:var(--ring)} .input{width:100%;border:1px solid var(--b);border-radius:10px;padding:12px 14px;font-size:15px}
input[type=url],input[type=text]{border:0;outline:0;background:transparent;width:100%;padding:10px 8px;font-size:16px} .input:focus{outline:none;border-color:#9ca3af;box-shadow:0 0 0 3px rgba(0,0,0,.06)}
small.mono{font:14px/1.3 'Roboto Mono',monospace;color:#333;background:#fff;border:1px dashed var(--br);border-radius:8px;padding:8px 10px;display:inline-flex;gap:6px;align-items:center} .help{margin-top:8px;color:var(--muted);font-size:13px}
.kbd{font:600 12px/1 'Roboto Mono',monospace;border:1px solid var(--br);border-radius:6px;padding:4px 6px;background:#fff;color:#333} .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}
.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap} .result .link{font:600 15px/1.4 "JetBrains Mono",monospace;word-break:break-all}
hr{border:0;border-top:1px solid var(--br);margin:16px 0} .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}
.notice{font-size:13px;color:var(--mut)} .grid{display:grid;gap:12px;margin:24px 0}
.result{display:flex;gap:8px;align-items:center;flex-wrap:wrap} @media(min-width:760px){.grid{grid-template-columns:repeat(3,1fr)}}
.copy{display:inline-flex;align-items:center;gap:6px} .feature{padding:16px}
.tag{border:1px solid var(--br);border-radius:999px;padding:6px 10px;background:#fff;color:#111;font-weight:600} .feature i{width:18px;height:18px}
.pricing .plan{display:grid;gap:10px} small.muted{color:var(--muted)}
.plan h3{margin:0;font:700 16px/1 Inter} .footer{margin:26px 0;color:var(--muted);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
.dim{color:var(--mut)} .modal{position:fixed;inset:0;display:grid;place-items:center;background:rgba(17,17,17,.42);padding:16px}
footer{color:#777;font-size:13px;padding:24px 0} .sheet{max-width:420px;width:100%}
noscript{display:block;background:#fff3f3;border:1px solid #ffdede;color:#8a1616;padding:10px;border-radius:10px} .close{position:absolute;top:10px;right:10px;background:#fff;border:1px solid var(--b);border-radius:8px;padding:6px;cursor:pointer}
.disabled{opacity:.45;pointer-events:none}
.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}
.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}
@keyframes s{to{transform:rotate(1turn)}}
.note{background:#fafafa;border:1px solid var(--b);border-radius:12px;padding:10px;font-size:13px}
</style> </style>
</head> </head>
<body x-data="{url:'',out:'',slug:'',busy:!1,err:'',copied:!1,paid:!1,shorten:async function(){this.err='';this.out='';this.copied=!1;let t=this.url?.trim();if(!t)return this.err='Enter a valid URL';try{new URL(t)}catch(_){return this.err='Enter a valid URL'}this.busy=1;try{let r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:t})});if(!r.ok)throw new Error(await r.text()||'Failed');let d=await r.json();this.out=d.shortUrl}catch(e){this.err=String(e.message||e)}this.busy=0},copy:async function(v){try{await navigator.clipboard.writeText(v||this.out);this.copied=1;setTimeout(()=>this.copied=0,1600)}catch(_){this.copied=0}}}" x-init="lucide&&lucide.createIcons()"> <body x-data="app()" x-init="init()">
<noscript>4ev.link works best with JavaScript enabled.</noscript> <div class=container>
<nav class=nav>
<a class=brand href="/">
<i data-lucide="link-2"></i><span>4ev.link</span>
</a>
<div class=actions>
<button class="btn ghost" @click="openAuth('login')"><i data-lucide="log-in"></i></button>
<button class="btn" @click="openAuth('signup')">Sign up</button>
</div>
</nav>
<div class=container> <header class=hero>
<nav class=nav> <h1>Get ur 4ev.link <span class=heart>❤️</span></h1>
<div class=brand><i data-lucide=infinity></i><a href=/ aria-label="Home">4ev.link</a><span class=badge>Free</span></div> <div class=sub>Free. No account needed. Cloudflare-fast.</div>
<div class=actions> </header>
<a class="btn ghost" href=#login>Log in</a>
<a class="btn black" href=#signup>Sign up</a>
</div>
</nav>
<header class=hero> <section class="card form" @keydown.enter.prevent="shorten()">
<h1>Get ur 4ev.link <span aria-hidden=true style=color:#e11d48>❤️</span></h1> <div class=row>
<p class=sub>Free. No account needed. Paste a link, get a short one. Custom aliases for paid users (coming soon).</p> <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>
</header> <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>
</div>
<main class=grid> <div class=help>
<section class="card grid" aria-label="Shorten form"> • 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>
<h2>Shorten a link</h2> </div>
<form class=grid @submit.prevent="shorten">
<div class=input>
<i data-lucide=link-2></i>
<input type=url x-model=url inputmode=url spellcheck=false placeholder="https://example.com/some/very/long/url" aria-label="Paste a long URL">
<button class="btn black" type=submit :disabled=busy x-text="busy?'Shortening…':'Shorten'"></button>
</div>
</form>
<template x-if="err"><div class="row" role=alert style="color:var(--err)"><i data-lucide=alert-triangle></i><span x-text=err></span></div></template>
<template x-if="out"> <template x-if="err">
<div class="result"> <div class="note" style="color:var(--bad);margin-top:10px"><i data-lucide="alert-triangle"></i> <span x-text="err"></span></div>
<i data-lucide=check-circle style=color:#16a34a></i> </template>
<a class="tag" :href=out target=_blank rel=noopener x-text=out></a>
<button class="btn copy" @click.prevent="copy()"><i data-lucide=copy></i><span x-text="copied?'Copied!':'Copy'"></span></button> <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> </div>
</template> </template>
<hr> <details style="margin-top:12px">
<summary class="badge"><i data-lucide="sparkles"></i> Custom URL (paid, soon)</summary>
<div class="row"> <div class="row" style="margin-top:10px">
<small class=mono><i data-lucide=sparkles></i>Free → random slug (ex: 4ev.link/aB9Z)</small> <span class="badge disabled"><i data-lucide="lock"></i> yourbrand</span>
<small class=mono><i data-lucide=lock></i>Custom alias → <b>Paid</b> <span class=dim>(coming soon)</span></small> <input class="input disabled" disabled placeholder="Choose your slug (paid soon)">
</div> <button class="btn disabled"><i data-lucide="check"></i> Reserve</button>
<div class="grid-2 grid" aria-hidden=true>
<div class=input title="Custom alias (Pro soon)">
<i data-lucide=tag></i>
<span class=dim style="padding:10px 8px">Alias (ex: /my-handle)</span>
<button class="btn" disabled>Pro soon</button>
</div> </div>
<div class=input> <div class="help">Want this sooner? <a href="#" @click.prevent="openAuth('signup')">Sign up</a> to get notified.</div>
<i data-lucide=eye></i> </details>
<span class=dim style="padding:10px 8px">Preview page <span class=kbd>soon</span></span>
</div>
</div>
</section> </section>
<section class="grid card" aria-label=Why> <section class="grid">
<h2>Why 4ev.link?</h2> <article class="card feature">
<div class=grid-2 style="gap:10px"> <div class="badge"><i data-lucide="zap"></i> Fast redirects</div>
<div class=row><i data-lucide=zap></i><div><b>Fast</b><div class=dim>Edge redirects in ~20ms</div></div></div> <p>Backed by Cloudflare Workers + KV, your links resolve globally with 302 redirects.</p>
<div class=row><i data-lucide=shield-check></i><div><b>Safe</b><div class=dim>Simple, open endpoints</div></div></div> </article>
<div class=row><i data-lucide=clock></i><div><b>Forever</b><div class=dim>No expiry on free links</div></div></div> <article class="card feature">
<div class=row><i data-lucide=wrench></i><div><b>Low-friction</b><div class=dim>No account required</div></div></div> <div class="badge"><i data-lucide="shield"></i> Private by default</div>
</div> <p>No account required to shorten. We only store your long URL and a short code.</p>
<p class=notice>Tip: paste a URL and hit Enter. Well give you a short link you can share instantly.</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>
<section class="card grid pricing" aria-label=Pricing> <section class="card feature">
<h2>Pricing</h2> <div class="badge"><i data-lucide="info"></i> How it works</div>
<div class="grid-2 grid"> <ol style="margin:10px 0 0;padding-left:18px">
<div class=plan> <li>Paste your long URL.</li>
<h3>Free</h3> <li>We validate and create a random 4-char slug.</li>
<ul class=dim style="margin:0 0 6px 18px"> <li>Share your 4ev.link — visitors get a 302 redirect to your target.</li>
<li>Random 4-char slugs</li><li>No account needed</li><li>Unlimited redirects</li> </ol>
</ul>
<a class="btn black" href=#start>Start free</a>
</div>
<div class=plan>
<h3>Pro <span class=dim>(coming soon)</span></h3>
<ul class=dim style="margin:0 0 6px 18px">
<li>Custom aliases</li><li>Analytics</li><li>Team access</li>
</ul>
<button class="btn" disabled>Notify me soon</button>
</div>
</div>
</section> </section>
</main>
<footer class=row style="justify-content:space-between"> <footer class=footer>
<div class=row><i data-lucide=copyright></i><span>4ev.link</span><span>·</span><span class=dim>Built on Cloudflare</span></div> <div>© <span x-text="year"></span> 4ev.link · <a href="#" @click.prevent="openAuth('login')">Log in</a></div>
<div class=row><a href=#tos>Terms</a><span>·</span><a href=#privacy>Privacy</a><span>·</span><a href=https://github.com/4ev-link/4ev.link target=_blank rel=noopener class=dim>GitHub</a></div> <div><small class=muted>Made with <span class=heart>❤️</span> · Inspired by Cloudflare · Fonts: Inter + Space Grotesk + JetBrains Mono</small></div>
</footer> </footer>
</div> </div>
<script>document.addEventListener('alpine:init',()=>{lucide&&lucide.createIcons()});</script> <template x-if="auth.open">
<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> </body>
</html> </html>