mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: New display font, heart, feature bubbles
This commit is contained in:
178
index.html
178
index.html
@@ -1,110 +1,92 @@
|
||||
<!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>
|
||||
<!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=Inter:wght@400;600&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
: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}
|
||||
.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="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>
|
||||
<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>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="small" :class="err?'':'hide'" style="color:#dc2626" x-text="err"></p>
|
||||
|
||||
<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>
|
||||
<p class="small">Tip: it’s a 302 redirect. Keep your original live.</p>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
</div>
|
||||
<p x-show="e" x-text="e" class="mt-2 text-sm text-red-600" role="alert"></p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<p class="mt-2 text-xs text-gray-500">Free plan generates random slugs. Custom URLs for paid — coming soon.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="//unpkg.com/alpinejs" defer></script>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<script>document.addEventListener('alpine:init',()=>{lucide?.createIcons()})</script>
|
||||
</body></html>
|
||||
|
||||
Reference in New Issue
Block a user