Files
4ev.link/index.html

172 lines
9.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html><html lang=en>
<head>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<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 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">
<style>
: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.5 Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}
a{color:var(--acc);text-decoration:none}a:hover{text-decoration:underline}
.container{max-width:1100px;margin:auto;padding:16px}
.nav{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}
.brand{display:flex;align-items:center;gap:10px;font:700 18px/1 Space Grotesk,Inter;color:var(--fg)}
.brand i{width:20px;height:20px}
.actions{display:flex;gap:8px}
.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{border-color:var(--b2)}
.btn.primary{background:#111;color:#fff;border-color:#111}
.btn.ghost{background:transparent}
.hero{padding:32px 0 18px;text-align:center}
h1{margin:0 0 8px;font:700 clamp(28px,6vw,40px)/1.15 Space Grotesk,Inter}
h1 .heart{font-size:1em;color:#e11d48}
.sub{color:var(--muted);font-weight:600}
.card{background:var(--card);border:1px solid var(--b);border-radius:14px;box-shadow:var(--shadow)}
.form{margin:18px auto 0;max-width:780px;padding:14px}
.row{display:flex;gap:8px;align-items:center}
.row>.grow{flex:1}
.input{width:100%;border:1px solid var(--b);border-radius:10px;padding:12px 14px;font-size:15px}
.input:focus{outline:none;border-color:#9ca3af;box-shadow:0 0 0 3px rgba(0,0,0,.06)}
.help{margin-top:8px;color:var(--muted);font-size:13px}
.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}
.result .link{font:600 15px/1.4 "JetBrains Mono",monospace;word-break:break-all}
.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}
.grid{display:grid;gap:12px;margin:24px 0}
@media(min-width:760px){.grid{grid-template-columns:repeat(3,1fr)}}
.feature{padding:16px}
.feature i{width:18px;height:18px}
small.muted{color:var(--muted)}
.footer{margin:26px 0;color:var(--muted);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
.modal{position:fixed;inset:0;display:grid;place-items:center;background:rgba(17,17,17,.42);padding:16px}
.sheet{max-width:420px;width:100%}
.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>
</head>
<body x-data="app()" x-init="init()">
<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>
<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>
</div>
<div class=help>
• 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>
</div>
<template x-if="err">
<div class="note" style="color:var(--bad);margin-top:10px"><i data-lucide="alert-triangle"></i> <span x-text="err"></span></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>
<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>
</html>