mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: Add mobile-first landing page
This commit is contained in:
247
index.html
247
index.html
@@ -1,152 +1,145 @@
|
||||
<!doctype html><html lang="en">
|
||||
<!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">
|
||||
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>4ev.link — simple forever-short links</title>
|
||||
<meta name=description content="4ev.link is a minimal, fast URL shortener. Free, no account needed. Paid custom URLs 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=Roboto+Mono:wght@400;700&display=swap" rel=stylesheet>
|
||||
<script src=//unpkg.com/alpinejs defer></script>
|
||||
<script src=https://unpkg.com/lucide@latest defer></script>
|
||||
<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}
|
||||
: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)}
|
||||
*{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}
|
||||
a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}
|
||||
.container{max-width:980px;margin:0 auto;padding:16px}
|
||||
.nav{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 0}
|
||||
.brand{display:inline-flex;align-items:center;gap:8px;font-weight:700}
|
||||
.badge{font:700 12px/1 Inter;background:#111;color:#fff;border-radius:999px;padding:6px 10px}
|
||||
.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:hover{box-shadow:var(--ring)}.btn:disabled{opacity:.6;cursor:not-allowed}
|
||||
.btn.black{background:#111;color:#fff;border-color:#111}
|
||||
.btn.ghost{background:transparent}
|
||||
.hero{display:grid;gap:14px;place-items:center;text-align:center;padding:28px 0}
|
||||
h1{font:700 clamp(24px,5.5vw,42px)/1.1 'Space Grotesk',Inter,system-ui;margin:0}
|
||||
h2{font:600 18px/1.2 Inter;margin:0}
|
||||
.sub{color:var(--sub);font-size:14px}
|
||||
.card{background:var(--card);border:1px solid var(--br);border-radius:14px;padding:14px}
|
||||
.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}
|
||||
@media(min-width:720px){.grid-2{grid-template-columns:1fr 1fr}}
|
||||
.input{display:flex;gap:8px;align-items:center;background:#fff;border:1px solid var(--br);border-radius:12px;padding:8px 8px}
|
||||
.input:focus-within{box-shadow:var(--ring)}
|
||||
input[type=url],input[type=text]{border:0;outline:0;background:transparent;width:100%;padding:10px 8px;font-size:16px}
|
||||
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}
|
||||
.kbd{font:600 12px/1 'Roboto Mono',monospace;border:1px solid var(--br);border-radius:6px;padding:4px 6px;background:#fff;color:#333}
|
||||
.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
hr{border:0;border-top:1px solid var(--br);margin:16px 0}
|
||||
.notice{font-size:13px;color:var(--mut)}
|
||||
.result{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.copy{display:inline-flex;align-items:center;gap:6px}
|
||||
.tag{border:1px solid var(--br);border-radius:999px;padding:6px 10px;background:#fff;color:#111;font-weight:600}
|
||||
.pricing .plan{display:grid;gap:10px}
|
||||
.plan h3{margin:0;font:700 16px/1 Inter}
|
||||
.dim{color:var(--mut)}
|
||||
footer{color:#777;font-size:13px;padding:24px 0}
|
||||
noscript{display:block;background:#fff3f3;border:1px solid #ffdede;color:#8a1616;padding:10px;border-radius:10px}
|
||||
</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>
|
||||
<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()">
|
||||
<noscript>4ev.link works best with JavaScript enabled.</noscript>
|
||||
|
||||
<div class=container>
|
||||
<nav class=nav>
|
||||
<div class=brand><i data-lucide=infinity></i><a href=/ aria-label="Home">4ev.link</a><span class=badge>Free</span></div>
|
||||
<div class=actions>
|
||||
<a class="btn ghost" href=#login>Log in</a>
|
||||
<a class="btn black" href=#signup>Sign up</a>
|
||||
</div>
|
||||
<nav class="nav" aria-label="Account">
|
||||
<a href="#" class="small">Log in</a>
|
||||
<a href="#" class="cta">Sign up</a>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<header class=hero>
|
||||
<h1>Get ur 4ev.link <span aria-hidden=true style=color:#e11d48>❤️</span></h1>
|
||||
<p class=sub>Free. No account needed. Paste a link, get a short one. Custom aliases for paid users (coming soon).</p>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<main class=grid>
|
||||
<section class="card grid" aria-label="Shorten form">
|
||||
<h2>Shorten a link</h2>
|
||||
<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>
|
||||
<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>
|
||||
<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="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>
|
||||
<template x-if="out">
|
||||
<div class="result">
|
||||
<i data-lucide=check-circle style=color:#16a34a></i>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<small class=mono><i data-lucide=sparkles></i>Free → random slug (ex: 4ev.link/aB9Z)</small>
|
||||
<small class=mono><i data-lucide=lock></i>Custom alias → <b>Paid</b> <span class=dim>(coming soon)</span></small>
|
||||
</div>
|
||||
|
||||
<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 class=input>
|
||||
<i data-lucide=eye></i>
|
||||
<span class=dim style="padding:10px 8px">Preview page <span class=kbd>soon</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<section class="grid card" aria-label=Why>
|
||||
<h2>Why 4ev.link?</h2>
|
||||
<div class=grid-2 style="gap:10px">
|
||||
<div class=row><i data-lucide=zap></i><div><b>Fast</b><div class=dim>Edge redirects in ~20ms</div></div></div>
|
||||
<div class=row><i data-lucide=shield-check></i><div><b>Safe</b><div class=dim>Simple, open endpoints</div></div></div>
|
||||
<div class=row><i data-lucide=clock></i><div><b>Forever</b><div class=dim>No expiry on free links</div></div></div>
|
||||
<div class=row><i data-lucide=wrench></i><div><b>Low-friction</b><div class=dim>No account required</div></div></div>
|
||||
</div>
|
||||
<p class=notice>Tip: paste a URL and hit Enter. We’ll give you a short link you can share instantly.</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>
|
||||
<section class="card grid pricing" aria-label=Pricing>
|
||||
<h2>Pricing</h2>
|
||||
<div class="grid-2 grid">
|
||||
<div class=plan>
|
||||
<h3>Free</h3>
|
||||
<ul class=dim style="margin:0 0 6px 18px">
|
||||
<li>Random 4-char slugs</li><li>No account needed</li><li>Unlimited redirects</li>
|
||||
</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>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<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 class=row style="justify-content:space-between">
|
||||
<div class=row><i data-lucide=copyright></i><span>4ev.link</span><span>·</span><span class=dim>Built on Cloudflare</span></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>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<script>document.addEventListener('alpine:init',()=>{lucide&&lucide.createIcons()});</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user