mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: Minimal Tailwind+Alpine landing+shorten UI
This commit is contained in:
156
index.html
156
index.html
@@ -1 +1,157 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta name="color-scheme" content="dark light">
|
||||||
|
<title>4ev.link — shorten, 4ever</title>
|
||||||
|
<meta name="description" content="Get a forever short link. Free random slugs. Paid: custom URLs, analytics, proxy/CDN, your subdomain (soon).">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Ctext y='.9em' font-size='52'%3E🔗%3C/text%3E%3C/svg%3E">
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>[x-cloak]{display:none}</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-full bg-black text-neutral-200 selection:bg-neutral-800">
|
||||||
|
<header class="sticky top-0 z-10 border-b border-neutral-900/80 bg-black/60 backdrop-blur">
|
||||||
|
<nav class="mx-auto max-w-3xl px-4 py-3 flex items-center justify-between">
|
||||||
|
<a href="/" class="font-semibold text-white tracking-tight">4ev.link</a>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<a href="#" class="px-3 py-1.5 rounded-lg border border-neutral-800 hover:bg-neutral-900">Log in</a>
|
||||||
|
<a href="#" class="px-3 py-1.5 rounded-lg bg-white text-black hover:bg-neutral-200">Sign up</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="mx-auto max-w-3xl px-4">
|
||||||
|
<section class="pt-16 pb-10 text-center">
|
||||||
|
<h1 class="text-3xl sm:text-4xl md:text-5xl font-semibold text-white">
|
||||||
|
Get ur 4ev.link <span class="align-middle text-red-500">❤️</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-3 text-neutral-400 max-w-xl mx-auto">
|
||||||
|
Minimal, fast, forever. Random slugs for free. Paid gets custom URLs, analytics, proxy/CDN, your subdomain (soon).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section x-data="app" x-cloak x-effect="lucide.createIcons()" class="mx-auto max-w-2xl">
|
||||||
|
<form @submit.prevent="go" class="rounded-2xl border border-neutral-900 bg-neutral-950/60 shadow-xl">
|
||||||
|
<div class="p-3 sm:p-4 flex gap-2">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-neutral-500">
|
||||||
|
<i data-lucide="link-2" class="w-4 h-4"></i>
|
||||||
|
</span>
|
||||||
|
<input x-model.trim="url" type="url" inputmode="url" name="url" required
|
||||||
|
placeholder="https://example.com"
|
||||||
|
class="w-full rounded-xl border border-neutral-800 bg-transparent pl-9 pr-3 py-3 outline-none ring-0 focus:border-neutral-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" :disabled="busy"
|
||||||
|
class="inline-flex items-center gap-2 rounded-xl bg-white text-black px-4 py-3 font-medium hover:bg-neutral-200 disabled:opacity-50">
|
||||||
|
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||||
|
<span x-text="busy?'Working…':'Shorten'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<template x-if="err">
|
||||||
|
<div class="px-4 pb-4 text-left">
|
||||||
|
<div class="text-sm text-red-400 flex items-center gap-2">
|
||||||
|
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
||||||
|
<span x-text="err"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<template x-if="out">
|
||||||
|
<div class="mt-4 rounded-2xl border border-neutral-900 bg-neutral-950/60 p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-3 justify-between">
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<i data-lucide="link" class="w-4 h-4 text-neutral-400 shrink-0"></i>
|
||||||
|
<a :href="out.shortUrl" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="truncate text-white hover:underline" x-text="out.shortUrl"></a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="copy" class="px-3 py-2 rounded-lg border border-neutral-800 hover:bg-neutral-900 inline-flex items-center gap-2">
|
||||||
|
<i data-lucide="copy" class="w-4 h-4" :class="copied?'hidden':''"></i>
|
||||||
|
<i data-lucide="check" class="w-4 h-4 text-emerald-400" :class="copied?'':'hidden'"></i>
|
||||||
|
<span class="text-sm" x-text="copied?'Copied!':'Copy'"></span>
|
||||||
|
</button>
|
||||||
|
<a :href="out.shortUrl" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="px-3 py-2 rounded-lg bg-white text-black hover:bg-neutral-200 inline-flex items-center gap-2">
|
||||||
|
<i data-lucide="arrow-up-right" class="w-4 h-4"></i>
|
||||||
|
<span class="text-sm">Open</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-xs text-neutral-500">
|
||||||
|
<span class="font-mono text-neutral-400" x-text="out.slug"></span>
|
||||||
|
<span class="mx-1">→</span>
|
||||||
|
<span class="break-all" x-text="out.target"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="mt-10 grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||||
|
<div class="flex items-center gap-2 text-white">
|
||||||
|
<i data-lucide="dice-5" class="w-4 h-4"></i><b>Free</b>
|
||||||
|
</div>
|
||||||
|
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||||
|
<li class="flex gap-2"><i data-lucide="check" class="w-4 h-4 text-neutral-500"></i>Random slug</li>
|
||||||
|
<li class="flex gap-2"><i data-lucide="zap" class="w-4 h-4 text-neutral-500"></i>Instant redirect</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||||
|
<div class="flex items-center gap-2 text-white">
|
||||||
|
<i data-lucide="star" class="w-4 h-4"></i><b>Paid</b> <span class="text-xs text-neutral-400">(coming soon)</span>
|
||||||
|
</div>
|
||||||
|
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||||
|
<li class="flex gap-2"><i data-lucide="type" class="w-4 h-4 text-neutral-500"></i>Custom URLs</li>
|
||||||
|
<li class="flex gap-2"><i data-lucide="bar-chart-2" class="w-4 h-4 text-neutral-500"></i>Analytics</li>
|
||||||
|
<li class="flex gap-2"><i data-lucide="shield" class="w-4 h-4 text-neutral-500"></i>Proxy/CDN</li>
|
||||||
|
<li class="flex gap-2"><i data-lucide="globe" class="w-4 h-4 text-neutral-500"></i>Your subdomain</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-neutral-900 p-4 bg-neutral-950/60">
|
||||||
|
<div class="flex items-center gap-2 text-white">
|
||||||
|
<i data-lucide="info" class="w-4 h-4"></i><b>How it works</b>
|
||||||
|
</div>
|
||||||
|
<ul class="mt-2 text-sm text-neutral-400 space-y-1">
|
||||||
|
<li>Paste a URL → Shorten</li>
|
||||||
|
<li>We store target & redirect</li>
|
||||||
|
<li>No account needed (free)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 text-center text-sm text-neutral-500">
|
||||||
|
Worker: <code class="text-neutral-400">ev.awww.workers.dev</code> · Built on Cloudflare
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="mt-16 pb-10 text-center text-xs text-neutral-500">
|
||||||
|
© <span x-data x-text="new Date().getFullYear()"></span> 4ev.link — keep it short, keep it 4ev
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
|
<script src="//unpkg.com/alpinejs" defer></script>
|
||||||
|
<script>
|
||||||
|
const API='https://ev.awww.workers.dev';
|
||||||
|
document.addEventListener('alpine:init',()=>Alpine.data('app',()=>({
|
||||||
|
api:API,url:'',out:null,err:'',busy:!1,copied:!1,
|
||||||
|
n(s){s=s.trim();if(!s)return s;if(!/^https?:\/\//i.test(s))s='https://'+s;return s},
|
||||||
|
async go(){
|
||||||
|
this.err=''; let s=this.n(this.url);
|
||||||
|
try{new URL(s)}catch{this.err='Enter a valid URL';return}
|
||||||
|
this.busy=!0;
|
||||||
|
try{
|
||||||
|
const r=await fetch(this.api+'/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:s})});
|
||||||
|
if(!r.ok)throw new Error((await r.text())||r.statusText);
|
||||||
|
this.out=await r.json(); await this.$nextTick(()=>lucide.createIcons());
|
||||||
|
}catch(e){this.err=e.message||'Something went wrong'}
|
||||||
|
finally{this.busy=!1}
|
||||||
|
},
|
||||||
|
async copy(){if(!this.out)return;try{await navigator.clipboard.writeText(this.out.shortUrl);this.copied=!0;setTimeout(()=>this.copied=!1,1200)}catch{}}
|
||||||
|
})));
|
||||||
|
document.addEventListener('DOMContentLoaded',()=>lucide.createIcons());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user