Files
4ev.link/index.html

169 lines
9.2 KiB
HTML

<!doctype html><html lang=en>
<head>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>4ev.link — simple, fast URL shortener</title>
<meta name=description content="Get your 4ev.link in seconds. Free, no account needed. Pro custom URLs coming soon.">
<meta property=og:title content="4ev.link — simple, fast URL shortener">
<meta property=og:description content="Free works. No account needed. Pro custom slugs (coming soon).">
<meta property=og:type content=website><meta property=og:url content="https://4ev.link/">
<meta name=color-scheme content=light only>
<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;500;600&family=Poppins:wght@600;700&family=JetBrains+Mono:wght@500&display=swap" rel=stylesheet>
<style>
:root{--bg:#fff;--fg:#111;--mut:#6b7280;--card:#f7f7f7;--bor:#e5e7eb;--ink:#111;--acc:#111;--heart:#e11d48}
*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:16px/1.4 Inter,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;background:#fff;color:var(--fg);-webkit-font-smoothing:antialiased}
a{color:var(--ink);text-decoration:none}a:hover{text-decoration:underline}
.container{max-width:960px;margin:0 auto;padding:20px}
.header{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
.brand{display:flex;gap:10px;align-items:center;font-weight:700}
.brand i{width:22px;height:22px}
.brand span{font-family:Poppins,Inter,sans-serif;letter-spacing:.2px}
.nav{display:flex;gap:8px;align-items:center}
.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 14px;border:1px solid var(--bor);border-radius:10px;background:#fff;color:#111;font-weight:600;cursor:pointer;transition:.15s}
.btn:hover{transform:translateY(-1px)}
.btn.primary{background:#111;color:#fff;border-color:#111}
.btn.ghost{background:#fff}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
.hero{padding:36px 0 10px;text-align:center}
.hero h1{margin:0 auto 8px;font:700 clamp(24px,5.5vw,44px)/1.1 Poppins,Inter,sans-serif;letter-spacing:.2px}
.sub{color:var(--mut);font-weight:500}
.panel{margin:18px auto;padding:16px;border:1px solid var(--bor);border-radius:14px;background:var(--card)}
.form{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center}
@media (max-width:640px){.form{grid-template-columns:1fr}.brand span{font-size:16px}.brand i{width:20px;height:20px}}
.input{width:100%;padding:12px 14px;border:1px solid var(--bor);border-radius:10px;background:#fff;outline:none}
.input:focus{border-color:#111;box-shadow:0 0 0 3px rgba(17,17,17,.08)}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.meta{font-size:13px;color:var(--mut)}
.kbd{font:500 13px JetBrains Mono,monospace;background:#fff;border:1px solid var(--bor);border-radius:6px;padding:2px 6px}
.hr{height:1px;background:var(--bor);margin:18px 0;border:0}
.card{background:#fff;border:1px solid var(--bor);border-radius:14px;padding:16px}
.grid{display:grid;gap:10px}
@media(min-width:720px){.grid.cols-2{grid-template-columns:1fr 1fr}.grid.cols-3{grid-template-columns:repeat(3,1fr)}}
.mono{font:600 14px/1.3 JetBrains Mono,monospace}
.badge{display:inline-flex;gap:6px;align-items:center;font:600 12px/1 Inter;border:1px solid var(--bor);border-radius:999px;padding:6px 10px;background:#fff;color:#111}
.footer{padding:24px 0 40px;color:var(--mut);text-align:center}
.note{font-size:13px;color:var(--mut)}
.copy{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
h2{margin:0 0 8px;font:700 18px/1.2 Poppins,Inter,sans-serif}
ul{margin:0;padding-left:18px}li{margin:6px 0}
.small{font-size:12px;color:var(--mut)}
</style>
</head>
<body>
<header class=container>
<div class=header>
<a class=brand href=/ aria-label="4ev.link Home"><i data-lucide=link-2></i><span>4ev.link</span></a>
<nav class=nav>
<a class=btn ghost href=#login><i data-lucide=log-in></i><span>Log in</span></a>
<a class="btn primary" href=#signup><i data-lucide=user-plus></i><span>Sign up</span></a>
</nav>
</div>
</header>
<main class=container x-data="app()">
<section class=hero>
<h1>Get ur 4ev.link <span aria-hidden=true style="color:var(--heart)">❤️</span></h1>
<div class=sub>Free works. No account needed. Cloudflare-inspired, fast, and private.</div>
</section>
<section class="panel" @keyup.enter="go()">
<div class=form>
<input class=input type=url inputmode=url autocomplete=url placeholder="Paste a long URL, e.g. https://example.com/very/long/path?with=stuff" x-model.trim=url aria-label="Long URL">
<button class="btn primary" :disabled="!url||busy" @click=go()>
<i :data-lucide="busy?'loader-2':'wand-2'"></i><span x-text="busy?'Shortening…':'Shorten'"></span>
</button>
</div>
<div class="row" style="margin-top:10px">
<span class=badge title="Random 4-char slug like the worker generates"><i data-lucide=hash></i><span>Free: random slug</span></span>
<span class=badge title="Custom slugs coming soon"><i data-lucide=lock></i><span>Pro: custom URLs (soon)</span></span>
<span class=meta x-text="'e.g. '+location.origin+'/'+ex"></span>
</div>
<template x-if=err><p class="meta" style="color:#b91c1c;margin:12px 0 0" x-text=err role=alert></p></template>
<template x-if=res>
<div class="card" style="margin-top:12px" aria-live=polite>
<div class="row" style="justify-content:space-between">
<div>
<div class=meta>Short URL</div>
<div class="mono" style="word-break:break-all"><a :href=res target=_blank rel=noopener x-text=res></a></div>
</div>
<div class=copy>
<button class=btn @click=copy()><i data-lucide=copy></i><span>Copy</span></button>
<a class="btn" :href=res target=_blank rel=noopener><i data-lucide=external-link></i><span>Open</span></a>
</div>
</div>
<div class="hr"></div>
<div class="row">
<span class=small>Destination</span>
<span class="mono" style="word-break:break-all" x-text=pretty(url)></span>
</div>
</div>
</template>
</section>
<section class="grid cols-3" style="margin-top:14px">
<div class=card><div class=row><i data-lucide=user-check></i><h2>No account needed</h2></div><div class=meta>Just paste and go. Works instantly from your phone.</div></div>
<div class=card><div class=row><i data-lucide=zap></i><h2>Edge-fast</h2></div><div class=meta>Backed by Cloudflare KV + Workers for global speed.</div></div>
<div class=card><div class=row><i data-lucide=shield-check></i><h2>Private</h2></div><div class=meta>We only store the URL mapping. No tracking pixels.</div></div>
</section>
<section class="grid cols-2" style="margin-top:14px">
<div class=card>
<div class=row><i data-lucide=gift></i><h2>Free</h2></div>
<ul class=meta>
<li>Random 4-char slugs</li>
<li>No login required</li>
<li>Copy and share in 1 click</li>
</ul>
<div class="hr"></div>
<button class="btn primary" @click="$el.blur();document.querySelector('input[type=url]').focus()"><i data-lucide=sparkles></i><span>Start free</span></button>
</div>
<div class=card>
<div class=row><i data-lucide=badge-dollar-sign></i><h2>Pro (coming soon)</h2></div>
<ul class=meta>
<li>Custom slugs (your brand)</li>
<li>Link management</li>
<li>Analytics</li>
</ul>
<div class="hr"></div>
<button class="btn" disabled><i data-lucide=clock></i><span>Notify me</span></button>
</div>
</section>
<section class="panel" style="margin-top:14px">
<div class=row>
<span class=meta>Tip: press <span class=kbd>Enter</span> to shorten faster. Works cross-origin via CORS.</span>
</div>
</section>
</main>
<footer class="container footer">
<div>UI inspired by Cloudflare. Colors: black/white/gray. Light theme. Mobile first.</div>
<div class=note style="margin-top:6px"><a href="https://github.com/4ev-link/4ev.link" target=_blank rel=noopener><i data-lucide=github></i> GitHub</a><a href=#terms>Terms</a><a href=#privacy>Privacy</a></div>
</footer>
<script src="//unpkg.com/alpinejs" defer></script>
<script src="https://unpkg.com/lucide@latest" defer></script>
<script>
const C='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',L=4,r=()=>[...Array(L)].map(()=>C[Math.random()*C.length|0]).join('')
function app(){return{
url:'',res:'',err:'',busy:0,ex:r(),
pretty:u=>{try{return new URL(u).href}catch{return u}},
copy(){navigator.clipboard?.writeText(this.res)},
async go(){
this.err='';if(!this.url)return this.err='Please paste a URL.'
try{new URL(this.url)}catch(e){return this.err='That does not look like a valid URL.'}
this.busy=1;try{
const x=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.url})})
if(!x.ok)throw new Error(await x.text())
const j=await x.json();this.res=j.shortUrl||location.origin+'/'+(j.slug||'');this.ex=r()
}catch(e){this.err=(e&&e.message)||'Something went wrong.'}finally{this.busy=0}
}
}}
addEventListener('load',()=>{try{lucide.createIcons()}catch{}})
</script>
</body>
</html>