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:
184
index.html
184
index.html
@@ -1,122 +1,110 @@
|
||||
<!doctype html><html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#ffffff">
|
||||
<title>4ev.link — Shorten URLs, fast</title>
|
||||
<meta name="description" content="Get your 4ev.link ❤️ Free, no account needed. Custom URLs coming soon. Mobile-first, Cloudflare-inspired.">
|
||||
<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>
|
||||
<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&family=Sora:wght@700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#fff;--fg:#0a0a0a;--mut:#6b7280;--line:#e5e7eb;--soft:#f7f7f8;--sans:'Inter',system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;--disp:'Sora',var(--sans);--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||
*{box-sizing:border-box}html,body{height:100%}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.35 var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||||
a{color:inherit;text-decoration:none}button{font:inherit}input{font:inherit}
|
||||
[x-cloak]{display:none!important}
|
||||
.wrap{min-height:100svh;display:flex;flex-direction:column;align-items:center;gap:16px;padding:16px}
|
||||
.nav{width:100%;max-width:720px;display:flex;align-items:center;justify-content:space-between;padding:8px 4px}
|
||||
.logo{display:flex;align-items:center;gap:8px;font-weight:700}
|
||||
.badge{font-size:11px;padding:2px 6px;border:1px solid var(--line);border-radius:999px;color:var(--mut);background:var(--soft)}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#fff;color:#111;cursor:pointer;transition:.15s ease;box-shadow:0 1px 0 rgba(0,0,0,.03)}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,0,0,.07)}
|
||||
:root{--bg:#fff;--fg:#0a0a0a;--mut:#6b7280;--line:#e5e7eb;--card:#fafafa;--ring:#11182722;--red:#ef4444;--ok:#10b981;--shadow:0 10px 30px rgba(0,0,0,.06)}
|
||||
*{box-sizing:border-box}html,body{height:100%;background:var(--bg);color:var(--fg)}
|
||||
body{margin:0;font:16px/1.4 Inter,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,"Apple Color Emoji","Segoe UI Emoji";-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit;text-decoration:none}
|
||||
.btn{display:inline-flex;align-items:center;gap:.5rem;border:1px solid var(--line);background:#fff;color:#111;padding:.7rem 1rem;border-radius:10px;cursor:pointer;box-shadow:0 1px 0 #00000008;transition:.15s}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:0 6px 16px #00000014}
|
||||
.btn.pri{background:#111;color:#fff;border-color:#111}
|
||||
.btn.ghost{background:transparent}
|
||||
.btn:disabled{opacity:.6;cursor:not-allowed;transform:none;box-shadow:none}
|
||||
.ghost{background:transparent}
|
||||
.hero{width:100%;max-width:720px;text-align:center;margin-top:4px}
|
||||
h1{margin:6px 0 6px 0;font:700 26px/1.15 var(--disp);letter-spacing:-.01em}
|
||||
.sub{color:var(--mut);font-size:14px}
|
||||
.card{width:100%;max-width:720px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:0 4px 24px rgba(0,0,0,.06)}
|
||||
.row{display:flex;gap:8px;align-items:stretch}
|
||||
.input{flex:1;min-width:0;display:flex;align-items:center;gap:8px;border:1px solid var(--line);background:#fff;border-radius:10px;padding:10px 12px}
|
||||
.input:focus-within{border-color:#111;box-shadow:0 0 0 3px rgba(17,17,17,.06)}
|
||||
input[type=url],input[type=text]{border:0;outline:0;width:100%;background:transparent}
|
||||
.hint{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px;color:var(--mut);font-size:12px}
|
||||
.kbd{font:12px var(--mono);background:var(--soft);border:1px solid var(--line);padding:2px 6px;border-radius:7px}
|
||||
.result{margin-top:10px;display:flex;gap:8px;align-items:center;border:1px dashed var(--line);border-radius:10px;padding:10px 12px;background:var(--soft)}
|
||||
.url{font:14px/1.2 var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pill{font-size:11px;padding:4px 8px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--mut)}
|
||||
.features{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:6px}
|
||||
.feature{display:flex;align-items:center;gap:6px;color:#111;border:1px solid var(--line);background:#fff;border-radius:999px;padding:6px 10px;font-size:12px}
|
||||
.foot{margin-top:auto;color:var(--mut);font-size:12px;padding:8px;text-align:center}
|
||||
hr{border:0;border-top:1px solid var(--line);margin:12px 0}
|
||||
@media(min-width:768px){h1{font-size:30px}.btn{padding:10px 14px}}
|
||||
.input{width:100%;padding:.9rem 1rem;border:1px solid var(--line);border-radius:12px;background:#fff;outline:none;transition:border .15s,box-shadow .15s}
|
||||
.input:focus{border-color:#111;box-shadow:0 0 0 4px var(--ring)}
|
||||
.small{font-size:.84rem;color:var(--mut)}
|
||||
.container{max-width:860px;margin:0 auto;padding:16px}
|
||||
.header{position:relative;display:grid;place-items:center;height:64px}
|
||||
.header .login{position:absolute;right:8px;top:8px}
|
||||
.brand{display:flex;align-items:center;gap:.5rem;font-weight:700}
|
||||
.heart{color:var(--red)}
|
||||
.main{min-height:calc(100svh - 64px);display:grid;place-items:center}
|
||||
.hero{width:100%;max-width:680px;display:grid;gap:16px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);padding:14px}
|
||||
.row{display:flex;gap:8px}
|
||||
.row.col{flex-direction:column}
|
||||
.split{display:grid;grid-template-columns:1fr auto;gap:8px}
|
||||
.kbd{font-family:"Space Mono",ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
|
||||
.badge{display:inline-flex;align-items:center;gap:.35rem;border:1px solid var(--line);border-radius:999px;padding:.35rem .6rem;background:#fff}
|
||||
.plans{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.hr{height:1px;background:var(--line);margin:8px 0}
|
||||
.center{text-align:center}
|
||||
footer{color:var(--mut);font-size:.8rem}
|
||||
.hide{display:none}
|
||||
@media(min-width:720px){.hero{gap:18px}.header{height:72px}.header .login{top:14px;right:14px}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap" x-data="app()" x-cloak>
|
||||
<nav class="nav">
|
||||
<div class="logo"><i data-lucide="link-2"></i><span>4ev.link</span><span class="badge">free</span></div>
|
||||
<a href="/login" class="btn ghost"><i data-lucide="log-in"></i><span>Login</span></a>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<a href="/login" class="login btn ghost" aria-label="Log in"><i data-lucide="log-in"></i><span>Log in</span></a>
|
||||
<h1 class="brand center">Get ur 4ev.link <i class="heart" data-lucide="heart"></i></h1>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<h1>Get ur 4ev.link <i data-lucide="heart" style="color:#ef4444"></i></h1>
|
||||
<div class="sub">Free works — no account needed. Custom URLs coming soon.</div>
|
||||
</section>
|
||||
<main class="main">
|
||||
<section class="hero" x-data="{long:'',short:'',err:'',busy:0,copied:0,ex:'https://4ev.link/a1B2'}" @keyup.window.escape="long='';short='';err=''">
|
||||
<p class="center small">Free works—no account needed. Cloudflare-inspired. Black/white/gray. Light. Mobile-first.</p>
|
||||
|
||||
<section class="card" style="margin-top:8px">
|
||||
<form @submit.prevent="go">
|
||||
<div class="row">
|
||||
<label class="input" style="flex:1">
|
||||
<i data-lucide="link"></i>
|
||||
<input type="url" inputmode="url" autocomplete="url" spellcheck="false" placeholder="https://example.com/anything" x-model.trim="t" @keyup.enter="go" required>
|
||||
</label>
|
||||
<button class="btn" :disabled="loading" @click.prevent="go">
|
||||
<i :data-lucide="loading?'loader-2':'sparkles'"></i><span x-text="loading?'Shortening…':'Shorten'"></span>
|
||||
<div class="card">
|
||||
<form class="row col" @submit.prevent="
|
||||
err='';short='';busy=1;
|
||||
fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:long.trim()})})
|
||||
.then(r=>r.ok?r.json():Promise.reject(r))
|
||||
.then(d=>short=d.shortUrl)
|
||||
.catch(()=>err='Please enter a valid URL (incl. https://)')
|
||||
.finally(()=>busy=0)
|
||||
">
|
||||
<label class="small" for="long">Paste your long URL</label>
|
||||
<div class="split">
|
||||
<input id="long" class="input kbd" x-model.trim="long" type="url" inputmode="url" spellcheck="false" placeholder="https://example.com/very/long/path?with=params" required>
|
||||
<button class="btn pri" :disabled="busy||!long">
|
||||
<template x-if="!busy"><i data-lucide="link-2"></i></template>
|
||||
<template x-if="busy"><i data-lucide="loader-2" class="spin"></i></template>
|
||||
<span x-text="busy?'Shortening…':'Shorten'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<div>Free plan → random 4-char slug, e.g. <span class="kbd" x-text="sample"></span></div>
|
||||
<div class="pill">Pro: custom alias <span style="opacity:.6">(soon)</span></div>
|
||||
</div>
|
||||
<p class="small" :class="err?'':'hide'" style="color:#dc2626" x-text="err"></p>
|
||||
|
||||
<div class="row" style="margin-top:8px;opacity:.6;pointer-events:none">
|
||||
<label class="input" title="Custom aliases for paid plan (coming soon)">
|
||||
<i data-lucide="at-sign"></i>
|
||||
<input type="text" placeholder="custom-alias (paid — coming soon)" disabled>
|
||||
</label>
|
||||
<template x-if="short">
|
||||
<div class="row col" x-init="$nextTick(()=>lucide.createIcons())">
|
||||
<label class="small" for="short">Your short link</label>
|
||||
<div class="split">
|
||||
<input id="short" class="input kbd" :value="short" readonly @focus="$el.select()">
|
||||
<button type="button" class="btn" @click="navigator.clipboard.writeText(short).then(()=>{copied=1;setTimeout(()=>copied=0,1500)})">
|
||||
<i data-lucide="copy"></i><span x-text="copied?'Copied!':'Copy'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template x-if="err">
|
||||
<div class="result" style="border-color:#fecaca;background:#fff1f2;color:#991b1b">
|
||||
<i data-lucide="alert-triangle"></i>
|
||||
<div class="url" x-text="err"></div>
|
||||
<p class="small">Tip: it’s a 302 redirect. Keep your original live.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="out">
|
||||
<div class="result">
|
||||
<i data-lucide="check-circle-2" style="color:#16a34a"></i>
|
||||
<a class="url" :href="out" target="_blank" rel="noopener" x-text="out"></a>
|
||||
<button class="btn" type="button" @click="copy" style="padding:8px 10px"><i data-lucide="copy"></i><span>Copy</span></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="features">
|
||||
<span class="feature"><i data-lucide="zap"></i>Fast</span>
|
||||
<span class="feature"><i data-lucide="shield-check"></i>Privacy-first</span>
|
||||
<span class="feature"><i data-lucide="globe"></i>Works anywhere</span>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="foot">© <span x-text="new Date().getFullYear()"></span> 4ev.link · Built on the edge · Black/white/gray, light themed</footer>
|
||||
</div>
|
||||
<div class="row col">
|
||||
<div class="plans">
|
||||
<span class="badge"><i data-lucide="sparkles"></i><b>Free</b>: random 4-char slugs <span class="kbd small" style="color:#111">/a1B2</span></span>
|
||||
<span class="badge"><i data-lucide="wand-2"></i><b>Paid</b>: custom aliases <span class="small" style="color:var(--mut)">(coming soon)</span></span>
|
||||
</div>
|
||||
<div class="hr"></div>
|
||||
<p class="small"><i data-lucide="info"></i> Example: <span class="kbd">https://your.site/blog</span> → <span class="kbd" x-text="ex"></span></p>
|
||||
</div>
|
||||
|
||||
<footer class="center">© <span x-text="new Date().getFullYear()"></span> 4ev.link · Built on the edge · <a class="small" href="https://lucide.dev" target="_blank" rel="noreferrer">Lucide</a> · <a class="small" href="https://alpinejs.dev" target="_blank" rel="noreferrer">Alpine</a></footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js" defer></script>
|
||||
<script defer>
|
||||
const app=()=>({t:'',out:'',err:'',loading:!1,sample:'',C:'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',L:4,
|
||||
rnd(){return [...Array(this.L)].map(()=>this.C[Math.random()*this.C.length|0]).join('')},
|
||||
init(){this.sample=location.origin+'/'+this.rnd();setTimeout(()=>lucide.createIcons(),0)},
|
||||
async go(){
|
||||
if(this.loading)return;this.err='';this.out='';this.loading=!0;
|
||||
try{
|
||||
const r=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:this.t})});
|
||||
if(!r.ok)throw new Error((await r.text())||'Error');
|
||||
const d=await r.json();this.out=d.shortUrl;this.t=''
|
||||
}catch(e){this.err=e.message||'Invalid URL'}
|
||||
finally{this.loading=!1;lucide.createIcons()}
|
||||
},
|
||||
async copy(){try{await navigator.clipboard.writeText(this.out)}catch{}}
|
||||
});
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
document.querySelectorAll('.spin')?.forEach(el=>el.style.cssText+='animation:spin 1s linear infinite');
|
||||
(function(){const s=document.createElement('style');s.textContent='@keyframes spin{to{transform:rotate(360deg)}}';document.head.appendChild(s)})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user