mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: Add mobile-first landing page for 4ev.link
This commit is contained in:
212
index.html
212
index.html
@@ -1,92 +1,152 @@
|
||||
<!doctype html><html lang="en"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<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).">
|
||||
<link rel="icon" href="data:,"><style>[x-cloak]{display:none!important}</style>
|
||||
<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">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<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>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script src="https://unpkg.com/lucide@latest" defer></script>
|
||||
</head><body class="bg-white text-gray-900 antialiased">
|
||||
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||
<nav class="max-w-3xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||
<a href="/" class="font-semibold tracking-tight text-xl">4ev.link</a>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/login" class="text-sm text-gray-700 hover:text-black">Log in</a>
|
||||
<a href="/signup" class="text-sm bg-black text-white px-3 py-1.5 rounded-md hover:bg-gray-900">Sign up</a>
|
||||
<!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 — Short links that last</title>
|
||||
<meta name="description" content="Shorten URLs fast. Free, no account needed. Custom slugs for paid (coming soon).">
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 24 24' fill='none' stroke='%23111' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L11 4'%3E%3C/path%3E%3Cpath d='M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07L13 20'%3E%3C/path%3E%3C/svg%3E">
|
||||
<style>
|
||||
:root{--bg:#fff;--fg:#111;--mut:#6b7280;--mut2:#9ca3af;--bd:#e5e7eb;--bd2:#d1d5db;--card:#fff;--btn:#111;--btnh:#000;--ok:#16a34a;--warn:#f97316;--err:#dc2626}
|
||||
*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial}
|
||||
a{color:inherit}button{cursor:pointer;border:0;background:transparent;color:inherit}
|
||||
.container{max-width:1000px;margin:0 auto;padding:20px}
|
||||
.header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}
|
||||
.brand{display:flex;align-items:center;gap:8px;font-weight:700}
|
||||
.badge{font-size:12px;padding:.2rem .45rem;border:1px solid var(--bd);border-radius:999px;color:var(--mut)}
|
||||
.nav a{font-weight:600;margin-left:12px;text-decoration:none}
|
||||
.nav .cta{background:var(--btn);color:#fff;padding:.5rem .8rem;border-radius:8px;border:1px solid #000}
|
||||
.nav .cta:hover{background:var(--btnh)}
|
||||
.hero{padding:40px 0 12px;text-align:center}
|
||||
h1{font-size:34px;margin:0 0 8px;letter-spacing:-.02em}
|
||||
.sub{color:var(--mut);font-size:14px}
|
||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:14px;padding:16px}
|
||||
.stack{display:grid;gap:12px}
|
||||
.input{display:flex;gap:8px;align-items:center}
|
||||
.input input{flex:1;padding:14px 14px;border:1px solid var(--bd2);border-radius:12px;font:inherit;background:#fff}
|
||||
.input input:focus{outline:2px solid #1111;border-color:#111}
|
||||
.btn{background:var(--btn);color:#fff;padding:12px 16px;border-radius:12px;border:1px solid #000;display:inline-flex;align-items:center;gap:8px}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||
.btn:hover{background:var(--btnh)}
|
||||
.btn.secondary{background:#fff;color:#111;border:1px solid var(--bd)}
|
||||
.row{display:flex;gap:10px;flex-wrap:wrap}
|
||||
.kbd{font-family:ui-monospace,Menlo,monospace;background:#f8f8f8;border:1px solid var(--bd);padding:.15rem .4rem;border-radius:6px}
|
||||
.small{font-size:12px;color:var(--mut)}
|
||||
.result{display:flex;align-items:center;gap:10px;justify-content:space-between;padding:12px;border:1px dashed var(--bd2);border-radius:12px;background:#fafafa}
|
||||
.result a{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.copy{display:inline-flex;align-items:center;gap:6px;padding:.45rem .6rem;border-radius:10px;border:1px solid var(--bd);background:#fff}
|
||||
.copy.ok{border-color:#bbf7d0;background:#f0fdf4}
|
||||
.grid{display:grid;gap:12px}
|
||||
@media(min-width:720px){.grid{grid-template-columns:1.2fr .8fr}}
|
||||
.feature{display:flex;gap:10px;align-items:flex-start}
|
||||
.feature i{color:#111}
|
||||
.muted{color:var(--mut)}
|
||||
.footer{margin:40px auto 20px;text-align:center;color:var(--mut)}
|
||||
.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(1turn)}}
|
||||
.notice{display:inline-flex;gap:6px;align-items:center;padding:.2rem .45rem;border:1px solid var(--bd);border-radius:999px;color:var(--mut);background:#fff}
|
||||
.tag{font-size:12px;padding:.2rem .45rem;border:1px solid var(--bd);border-radius:999px}
|
||||
hr{border:0;border-top:1px solid var(--bd);margin:16px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#make" class="badge" style="position:absolute;left:10px;top:8px">Skip to main</a>
|
||||
<div class="container" x-data="shorty()">
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<i data-lucide="link-2" aria-hidden="true"></i><span>4ev.link</span>
|
||||
<span class="badge">free • no account</span>
|
||||
</div>
|
||||
<nav class="nav" aria-label="Account">
|
||||
<a href="#" class="small">Log in</a>
|
||||
<a href="#" class="cta">Sign up</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<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(_){}}">
|
||||
<section class="text-center">
|
||||
<h1 class="font-display text-3xl sm:text-4xl font-semibold tracking-tight inline-flex items-center gap-2 justify-center">
|
||||
<span>Get ur 4ev.link</span>
|
||||
<span aria-hidden="true" class="inline-block align-[-2px]">
|
||||
<svg viewBox="0 0 24 24" class="w-6 h-6 text-red-500" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<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"/>
|
||||
</svg>
|
||||
</span>
|
||||
</h1>
|
||||
<p class="mt-3 text-gray-600 text-sm">Free: random short link • Paid: custom URLs
|
||||
<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>
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
<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">
|
||||
<i data-lucide="shield" class="w-3.5 h-3.5"></i><span>Privacy-first</span>
|
||||
</span>
|
||||
<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">
|
||||
<i data-lucide="ban" class="w-3.5 h-3.5"></i><span>No pixel tracking</span>
|
||||
</span>
|
||||
<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">
|
||||
<i data-lucide="link-2" class="w-3.5 h-3.5"></i><span>Simple URL shortener</span>
|
||||
</span>
|
||||
<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">
|
||||
<i data-lucide="infinity" class="w-3.5 h-3.5"></i><span>Links are forever (or until we go bankrupt)</span>
|
||||
</span>
|
||||
</div>
|
||||
<section class="hero">
|
||||
<h1>Get ur 4ev.link <span title="love"><i data-lucide="heart" style="color:#ef4444"></i></span></h1>
|
||||
<p class="sub">Shorten URLs in seconds. Works free without an account. Paid custom names coming soon.</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<form @submit.prevent="s()" class="flex items-stretch gap-2">
|
||||
<input x-model="t" type="url" inputmode="url" placeholder="https://example.com/very/long/link"
|
||||
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>
|
||||
<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>
|
||||
<main id="make" class="grid" aria-live="polite">
|
||||
<section class="card stack">
|
||||
<div class="notice"><i data-lucide="zap"></i><span>Random slugs, always fast. ex: <b>4ev.link/aB3Z</b></span></div>
|
||||
<form class="stack" @submit.prevent="go()">
|
||||
<label class="small muted" for="u">Paste a long URL</label>
|
||||
<div class="input">
|
||||
<input id="u" name="url" x-model.trim="url" inputmode="url" autocomplete="url" placeholder="https://example.com/very/long/link" aria-label="Long URL to shorten">
|
||||
<button class="btn" :disabled="busy">
|
||||
<i :data-lucide="busy?'loader-2':'scissors'" :class="busy&&'spin'"></i>
|
||||
<span x-text="busy?'Working…':'Shorten'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<template x-if="err">
|
||||
<div class="small" style="color:var(--err)"><i data-lucide="alert-triangle"></i> <span x-text="err"></span></div>
|
||||
</template>
|
||||
</form>
|
||||
<p x-show="e" x-text="e" class="mt-2 text-sm text-red-600" role="alert"></p>
|
||||
|
||||
<template x-if="res">
|
||||
<div class="stack" x-init="$nextTick(()=>window.lucide&&lucide.createIcons())">
|
||||
<div class="result">
|
||||
<a :href="res.shortUrl" target="_blank" rel="noopener" x-text="res.shortUrl"></a>
|
||||
<div class="row">
|
||||
<button class="copy" :class="copied&&'ok'" @click="copy(res.shortUrl)">
|
||||
<i :data-lucide="copied?'check':'copy'"></i><span class="small" x-text="copied?'Copied':'Copy'"></span>
|
||||
</button>
|
||||
<a class="btn secondary" :href="res.shortUrl" target="_blank" rel="noopener">
|
||||
<i data-lucide="external-link"></i><span>Open</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small muted">Target: <span class="kbd" x-text="res.target"></span> • Slug: <span class="kbd" x-text="res.slug"></span></div>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
<div class="flex items-center gap-2">
|
||||
<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">
|
||||
<i data-lucide="external-link" class="w-4 h-4"></i><span>Open</span>
|
||||
</a>
|
||||
<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">
|
||||
<i data-lucide="copy" class="w-4 h-4" x-ref="copy"></i><span>Copy</span>
|
||||
</button>
|
||||
<aside class="stack">
|
||||
<div class="card stack">
|
||||
<div class="row" style="justify-content:space-between;align-items:center">
|
||||
<strong>Plans</strong><span class="tag">light theme</span>
|
||||
</div>
|
||||
<div class="feature"><i data-lucide="badge-check"></i><div><b>Free</b><div class="small muted">Random 4‑char slug. No account required.</div></div></div>
|
||||
<div class="feature"><i data-lucide="lock"></i><div><b>Paid</b> <span class="tag" style="color:#b45309;border-color:#fed7aa;background:#fffbeb">coming soon</span><div class="small muted">Choose your own slug and manage links.</div></div></div>
|
||||
<div class="row">
|
||||
<button class="btn secondary" @click="toast('Free is ready—just paste above!')"><i data-lucide="smile"></i><span>Use free</span></button>
|
||||
<button class="btn" disabled title="Coming soon"><i data-lucide="wand-sparkles"></i><span>Get custom</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">Free plan generates random slugs. Custom URLs for paid — coming soon.</p>
|
||||
</section>
|
||||
|
||||
<div class="card stack">
|
||||
<strong>Why 4ev.link?</strong>
|
||||
<div class="feature"><i data-lucide="timer"></i><div><b>Instant</b><div class="small muted">Create in ~1 round‑trip. Optimized edge logic.</div></div></div>
|
||||
<div class="feature"><i data-lucide="shield"></i><div><b>Private</b><div class="small muted">No signup needed to shorten links.</div></div></div>
|
||||
<div class="feature"><i data-lucide="sparkles"></i><div><b>Simple</b><div class="small muted">One input, one click, done.</div></div></div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<footer class="border-t border-gray-200">
|
||||
<div class="max-w-3xl mx-auto px-4 py-8 text-sm text-gray-600 flex items-center justify-between">
|
||||
<span>© <span x-text="new Date().getFullYear()"></span> 4ev.link</span>
|
||||
<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>
|
||||
<a href="/pricing" class="hover:text-black">Pricing</a>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer small">
|
||||
<div>© <span x-text="year"></span> 4ev.link • Built for speed • <a href="https://github.com/4ev-link/4ev.link" target="_blank" rel="noopener">GitHub</a></div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>document.addEventListener('alpine:init',()=>{lucide?.createIcons()})</script>
|
||||
</body></html>
|
||||
<noscript><div class="container"><div class="card">JavaScript is required to shorten links. Please enable it.</div></div></noscript>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js" defer></script>
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script>
|
||||
function shorty(){return{
|
||||
url:'',busy:false,err:'',res:null,copied:false,year:new Date().getFullYear(),
|
||||
fix(u){u=u.trim();return/^https?:\/\//i.test(u)?u:'https://'+u},
|
||||
async go(){
|
||||
this.err='';this.res=null;let t=this.url;if(!t){this.err='Please paste a URL.';return}
|
||||
this.busy=true;try{
|
||||
const r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.fix(t)})})
|
||||
if(!r.ok)throw new Error(await r.text()||'Failed to shorten');this.res=await r.json();this.url=''
|
||||
this.$nextTick(()=>window.lucide&&lucide.createIcons())
|
||||
}catch(e){this.err=(e.message||e||'Error')+''}finally{this.busy=false}
|
||||
},
|
||||
async copy(t){try{await navigator.clipboard.writeText(t);this.copied=true;setTimeout(()=>this.copied=false,900)}catch{}},
|
||||
toast(m){this.err=m;setTimeout(()=>this.err='',1200)}
|
||||
}}
|
||||
document.addEventListener('DOMContentLoaded',()=>{try{window.lucide&&lucide.createIcons()}catch{}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user