Files
4ev.link/index.html

153 lines
9.2 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,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>
<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>
</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="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>
<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 4char 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>
<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 roundtrip. 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="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>
<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>