mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Feat: Add Cloudflare-inspired landing page
This commit is contained in:
255
index.html
255
index.html
@@ -1,177 +1,114 @@
|
||||
<!doctype html><html lang=en class="h-full">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>4ev.link — Short links that live 4ev</title>
|
||||
<meta name=description content="Create durable, fast, privacy-friendly short links. Simple API. Free.">
|
||||
<link rel=canonical href="https://4ev.link/">
|
||||
<meta name=color-scheme content="light dark">
|
||||
<meta name=theme-color content="#0ea5e9">
|
||||
<link rel="icon" href='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="%230ea5e9"/><stop offset="1" stop-color="%238b5cf6"/></linearGradient></defs><rect width="64" height="64" rx="14" fill="white"/><g fill="url(%23g)"><path d="M17 35a8 8 0 0 1 0-11l7-7a8 8 0 0 1 11 0l2 2-4 4-2-2a3 3 0 0 0-4 0l-7 7a3 3 0 0 0 0 4l2 2-4 4-1-1Z"/><path d="M47 29a8 8 0 0 1 0 11l-7 7a8 8 0 0 1-11 0l-2-2 4-4 2 2a3 3 0 0 0 4 0l7-7a3 3 0 0 0 0-4l-2-2 4-4 1 1Z"/></g></svg>'>
|
||||
<meta property="og:title" content="4ev.link — Short links that live 4ev">
|
||||
<meta property="og:description" content="Create durable, fast, privacy-friendly short links.">
|
||||
<meta property="og:url" content="https://4ev.link/">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<link rel="preconnect" href="https://cdn.tailwindcss.com" crossorigin>
|
||||
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
||||
<script>try{let t=localStorage.theme;if(!t)t=matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';document.documentElement.classList.toggle('dark',t=='dark')}catch{}</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config={darkMode:'class',theme:{extend:{fontFamily:{sans:['Inter','ui-sans-serif','system-ui','Segoe UI',Arial,'Apple Color Emoji','Segoe UI Emoji']},colors:{brand:{50:'#ecfeff',100:'#cffafe',200:'#a5f3fc',300:'#67e8f9',400:'#22d3ee',500:'#06b6d4',600:'#0891b2',700:'#0e7490',800:'#155e75',900:'#164e63'}},boxShadow:{soft:'0 12px 30px rgba(2,8,23,.08), inset 0 1px 0 rgba(255,255,255,.5)',softd:'0 12px 30px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.06)'}}}}</script>
|
||||
<style>html,body{height:100%}::selection{background:#22d3ee20}::-moz-selection{background:#22d3ee20}</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>4ev.link | A Simple, Fast URL Shortener</title>
|
||||
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
:root{--bg:#fff;--fg:#111;--border:#e5e7eb;--muted-fg:#6b7280;--accent:#0051c3;--accent-fg:#fff;--error-bg:#fef2f2;--error-fg:#991b1b;--success-bg:#f0fdf4;--success-fg:#166534}
|
||||
*,::before,::after{box-sizing:border-box}
|
||||
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background-color:var(--bg);color:var(--fg);display:grid;place-items:center;min-height:100vh;padding:1rem}
|
||||
main{width:100%;max-width:560px;text-align:center}
|
||||
h1{font-size:2.25rem;font-weight:700;margin:0 0 .5rem}
|
||||
p{color:var(--muted-fg);margin:0 0 2rem}
|
||||
form{display:flex;gap:.5rem;margin-bottom:1.5rem}
|
||||
input[type=url]{flex-grow:1;width:100%;padding:.75rem 1rem;font-size:1rem;border:1px solid var(--border);border-radius:6px;transition:box-shadow .15s ease,border-color .15s ease}
|
||||
input[type=url]:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 25%,transparent)}
|
||||
input:disabled{background-color:#f9fafb;cursor:not-allowed}
|
||||
button{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.75rem 1.25rem;font-size:1rem;font-weight:600;border:1px solid var(--accent);border-radius:6px;background-color:var(--accent);color:var(--accent-fg);cursor:pointer;transition:background-color .15s ease}
|
||||
button:hover{background-color:color-mix(in srgb,var(--accent) 85%,#000)}
|
||||
button:disabled{background-color:#a1a1aa;border-color:#a1a1aa;cursor:not-allowed}
|
||||
.result,.feedback{padding:1rem;border-radius:6px;text-align:left;display:flex;align-items:center;justify-content:space-between;gap:1rem;word-break:break-all}
|
||||
.feedback.error{background-color:var(--error-bg);color:var(--error-fg)}
|
||||
.result{background-color:var(--success-bg);border:1px solid #bbf7d0}
|
||||
.result a{color:var(--success-fg);font-weight:500;text-decoration:none}
|
||||
.result a:hover{text-decoration:underline}
|
||||
.result button{padding:.5rem;font-size:.875rem;background-color:transparent;color:var(--success-fg);border:1px solid #6ee7b7}
|
||||
.result button:hover{background-color:color-mix(in srgb,var(--success-fg) 10%,transparent)}
|
||||
.result button:disabled{background-color:transparent;border-color:transparent;color:var(--success-fg)}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.spinner{animation:spin 1s linear infinite}
|
||||
footer{position:absolute;bottom:1rem;color:var(--muted-fg);font-size:.875rem}
|
||||
footer a{color:var(--muted-fg);text-decoration:none}
|
||||
footer a:hover{text-decoration:underline}
|
||||
</style>
|
||||
</head>
|
||||
<body class="h-full text-slate-800 dark:text-slate-100 bg-gradient-to-b from-slate-50 to-white dark:from-slate-900 dark:to-slate-950 antialiased">
|
||||
<div class="absolute inset-0 -z-10 opacity-70 pointer-events-none [mask-image:radial-gradient(60%_60%_at_50%_0%,#000_30%,transparent_80%)]">
|
||||
<div class="absolute -top-24 left-1/2 -translate-x-1/2 w-[1200px] h-[600px] bg-gradient-to-r from-cyan-400/30 via-indigo-400/20 to-fuchsia-400/20 blur-3xl"></div>
|
||||
</div>
|
||||
<body>
|
||||
<main x-data="shortener()">
|
||||
<h1>4ev.link</h1>
|
||||
<p>A simple, fast, and permanent URL shortener.</p>
|
||||
|
||||
<header class="sticky top-0 z-30 backdrop-blur supports-[backdrop-filter]:bg-white/50 dark:supports-[backdrop-filter]:bg-slate-900/40 border-b border-white/60 dark:border-white/5">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
|
||||
<a href="/" class="group inline-flex items-center gap-2 text-slate-900 dark:text-white font-semibold tracking-tight">
|
||||
<i data-lucide="link-2" class="w-5 h-5 text-brand-500 group-hover:scale-110 transition"></i>
|
||||
<span>4ev.link</span>
|
||||
</a>
|
||||
<nav class="flex items-center gap-2">
|
||||
<a href="https://github.com/4ev-link/4ev.link" target="_blank" rel="noopener" class="p-2 rounded-xl bg-white/70 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd hover:-translate-y-0.5 transition">
|
||||
<i data-lucide="github" class="w-5 h-5"></i>
|
||||
</a>
|
||||
<button id="theme" class="p-2 rounded-xl bg-white/70 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd hover:-translate-y-0.5 transition" aria-label="Toggle theme">
|
||||
<i data-lucide="moon" class="w-5 h-5 dark:hidden"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 hidden dark:inline"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<section class="py-14 sm:py-20">
|
||||
<div class="max-w-3xl">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm bg-white/70 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<i data-lucide="zap" class="w-4 h-4 text-brand-500"></i><span>Edge-fast • Privacy-first</span>
|
||||
</div>
|
||||
<h1 class="mt-6 text-4xl sm:text-6xl font-extrabold tracking-tight">Short links that live 4ev</h1>
|
||||
<p class="mt-4 text-lg text-slate-600 dark:text-slate-300">Create durable, fast, privacy-friendly short links. No tracking pixels. Simple API.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 sm:mt-12 max-w-3xl">
|
||||
<div class="p-4 sm:p-6 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd backdrop-blur">
|
||||
<form id="f" class="grid gap-3 sm:grid-cols-[1fr_auto] items-center" autocomplete="off">
|
||||
<label for="u" class="sr-only">URL</label>
|
||||
<div class="relative">
|
||||
<i data-lucide="link" class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input id="u" name="url" type="url" inputmode="url" required placeholder="https://example.com/very/long/url" class="w-full pl-10 pr-4 py-3 rounded-2xl bg-white/80 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-inner outline-none focus:ring-2 focus:ring-brand-400/60" />
|
||||
</div>
|
||||
<button id="b" class="inline-flex justify-center items-center gap-2 px-5 py-3 rounded-2xl bg-gradient-to-br from-cyan-500 to-indigo-500 text-white font-semibold shadow-[0_10px_25px_rgba(14,165,233,.35)] hover:shadow-[0_14px_30px_rgba(14,165,233,.45)] active:scale-[.99] transition disabled:opacity-60 disabled:cursor-not-allowed">
|
||||
<i data-lucide="scissors" class="w-5 h-5"></i><span>Shorten</span>
|
||||
<form @submit.prevent="submitUrl">
|
||||
<input type="url" x-model="urlInput" placeholder="https://your-very-long-url.com/to-shorten" required :disabled="loading">
|
||||
<button type="submit" :disabled="loading">
|
||||
<i x-show="!loading" data-lucide="link-2" style="width:20px;height:20px"></i>
|
||||
<i x-show="loading" class="spinner" data-lucide="loader-2" style="width:20px;height:20px"></i>
|
||||
<span x-text="loading ? '...' : 'Shorten'"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="res" class="mt-4 sm:mt-6 hidden">
|
||||
<div class="grid gap-3 sm:grid-cols-[1fr_auto_auto] items-center">
|
||||
<div class="relative">
|
||||
<i data-lucide="check-circle-2" class="w-5 h-5 absolute left-3 top-1/2 -translate-y-1/2 text-emerald-500"></i>
|
||||
<input id="o" class="w-full pl-10 pr-4 py-3 rounded-2xl bg-white/80 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-inner text-emerald-700 dark:text-emerald-300" readonly>
|
||||
</div>
|
||||
<button id="cpy" class="inline-flex justify-center items-center gap-2 px-4 py-3 rounded-2xl bg-white/80 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd hover:-translate-y-0.5 transition">
|
||||
<i data-lucide="copy" class="w-5 h-5"></i><span>Copy</span>
|
||||
<template x-if="error">
|
||||
<div class="feedback error" x-text="error"></div>
|
||||
</template>
|
||||
|
||||
<template x-if="shortenedUrl">
|
||||
<div class="result">
|
||||
<a :href="shortenedUrl" x-text="shortenedUrl" target="_blank" rel="noopener noreferrer"></a>
|
||||
<button @click="copyToClipboard()" :disabled="copyText !== 'Copy'">
|
||||
<i :data-lucide="copyText === 'Copy' ? 'copy' : 'check'" style="width:16px;height:16px"></i>
|
||||
<span x-text="copyText"></span>
|
||||
</button>
|
||||
<a id="go" target="_blank" rel="noopener" class="inline-flex justify-center items-center gap-2 px-4 py-3 rounded-2xl bg-white/80 dark:bg-white/5 border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd hover:-translate-y-0.5 transition">
|
||||
<i data-lucide="external-link" class="w-5 h-5"></i><span>Open</span>
|
||||
</a>
|
||||
</div>
|
||||
<p id="msg" class="mt-2 text-sm text-slate-500 dark:text-slate-400" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<p id="e" class="mt-3 text-sm text-rose-600 dark:text-rose-400 hidden">Invalid URL. Please enter a full http(s) link.</p>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs text-slate-500 dark:text-slate-400">API: POST / with JSON { "url": "https://…" } → { "slug","target","shortUrl" }</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-8 sm:py-10">
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="zap" class="w-5 h-5 text-brand-500"></i>Edge-fast</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">Global redirects with 302 from the edge.</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="hard-drive" class="w-5 h-5 text-brand-500"></i>Durable</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">Stored in KV, designed to last.</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="shield" class="w-5 h-5 text-brand-500"></i>Privacy-first</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">No trackers, minimal logs.</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="qr-code" class="w-5 h-5 text-brand-500"></i>QR ready</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">Save/share QR in a click (soon).</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="code-2" class="w-5 h-5 text-brand-500"></i>Simple API</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">POST / with JSON body, get JSON back.</p>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<div class="flex items-center gap-2 font-semibold"><i data-lucide="tag" class="w-5 h-5 text-brand-500"></i>Custom slugs</div>
|
||||
<p class="mt-1 text-sm text-slate-600 dark:text-slate-300">Personalized links (soon).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-8 sm:py-12">
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<h2 class="font-semibold flex items-center gap-2"><i data-lucide="workflow" class="w-5 h-5 text-brand-500"></i>How it works</h2>
|
||||
<ol class="mt-2 space-y-1 text-sm text-slate-600 dark:text-slate-300 list-decimal list-inside">
|
||||
<li>POST / with { "url": "https://…" }</li>
|
||||
<li>Receive { "slug","target","shortUrl" }</li>
|
||||
<li>Visit /:slug → 302 to target</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="p-5 rounded-3xl bg-white/80 dark:bg-white/[.04] border border-white/80 dark:border-white/10 shadow-soft dark:shadow-softd">
|
||||
<h2 class="font-semibold flex items-center gap-2"><i data-lucide="help-circle" class="w-5 h-5 text-brand-500"></i>FAQ</h2>
|
||||
<ul class="mt-2 space-y-1 text-sm text-slate-600 dark:text-slate-300">
|
||||
<li><b>What URLs?</b> Any http(s) URL.</li>
|
||||
<li><b>Free?</b> Yes, fair-use.</li>
|
||||
<li><b>Data?</b> No tracking, minimal logs.</li>
|
||||
<li><b>Export?</b> Copy/share instantly.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
|
||||
<footer class="mt-10 border-t border-white/70 dark:border-white/10">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6 text-sm text-slate-500 dark:text-slate-400 flex flex-wrap gap-x-4 gap-y-2 items-center justify-between">
|
||||
<span>© <span id="y"></span> 4ev.link</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<a class="inline-flex items-center gap-1 hover:text-slate-700 dark:hover:text-slate-200" href="https://github.com/4ev-link/4ev.link" target="_blank" rel="noopener"><i data-lucide="github" class="w-4 h-4"></i>GitHub</a>
|
||||
<a class="inline-flex items-center gap-1 hover:text-slate-700 dark:hover:text-slate-200" href="/"><i data-lucide="api" class="w-4 h-4"></i>API</a>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://github.com/4ev-link/4ev.link" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</footer>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script>
|
||||
(()=>{
|
||||
const $=q=>document.querySelector(q),I=()=>window.lucide&&lucide.createIcons(),D=m=>{const el=$("#msg");el.textContent=m||""}
|
||||
const f=$("#f"),u=$("#u"),b=$("#b"),res=$("#res"),o=$("#o"),cpy=$("#cpy"),go=$("#go"),err=$("#e"),y=$("#y"),th=$("#theme")
|
||||
y.textContent=new Date().getFullYear()
|
||||
addEventListener("DOMContentLoaded",I,{once:1});document.fonts?.ready?.then(I)
|
||||
th.onclick=()=>{const d=document.documentElement.classList.toggle("dark");try{localStorage.theme=d?"dark":"light"}catch{};I()}
|
||||
f.onsubmit=async e=>{
|
||||
e.preventDefault();err.classList.add("hidden");D("");res.classList.add("hidden");b.disabled=1
|
||||
let v=u.value.trim()
|
||||
if(!/^https?:\/\//i.test(v))v="https://"+v
|
||||
function shortener() {
|
||||
return {
|
||||
urlInput: '',
|
||||
shortenedUrl: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
copyText: 'Copy',
|
||||
async submitUrl() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.shortenedUrl = null;
|
||||
try {
|
||||
const r=await fetch("/",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:v})})
|
||||
if(!r.ok)throw 0
|
||||
const j=await r.json()
|
||||
o.value=j.shortUrl;go.href=j.shortUrl;go.target="_blank";res.classList.remove("hidden");D("Link created. It redirects with 302.");I()
|
||||
}catch{err.classList.remove("hidden")}
|
||||
b.disabled=0
|
||||
const res = await fetch('/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: this.urlInput })
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text() || 'Failed to shorten URL');
|
||||
const data = await res.json();
|
||||
this.shortenedUrl = data.shortUrl;
|
||||
this.urlInput = '';
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
lucide.createIcons();
|
||||
}
|
||||
cpy.onclick=async()=>{try{await navigator.clipboard.writeText(o.value);D("Copied!")}catch{D("Copy failed")}}
|
||||
})()
|
||||
},
|
||||
copyToClipboard() {
|
||||
navigator.clipboard.writeText(this.shortenedUrl);
|
||||
this.copyText = 'Copied!';
|
||||
lucide.createIcons();
|
||||
setTimeout(() => {
|
||||
this.copyText = 'Copy';
|
||||
lucide.createIcons();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => lucide.createIcons());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user