Files
4ev.link/index.html

147 lines
6.0 KiB
HTML

<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>4ev.link — Fast, Free & Permanent URL Shortener</title>
<meta name="description" content="Create short, memorable, and permanent links for free. 4ev.link is a simple, fast, and reliable URL shortener for all your needs.">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#f9fafb" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0a0a0a" media="(prefers-color-scheme: dark)">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body class="bg-gray-50 dark:bg-neutral-950 text-gray-800 dark:text-gray-300 antialiased">
<div class="flex flex-col min-h-screen">
<header class="py-4">
<nav class="container mx-auto px-6 flex justify-between items-center">
<a href="/" class="font-light text-xl text-black dark:text-white">4ev.link</a>
<button class="text-sm font-semibold py-2 px-4 rounded-lg bg-gray-200 dark:bg-neutral-800 text-black dark:text-white hover:opacity-80 transition-opacity">
Sign In
</button>
</nav>
</header>
<main class="flex-grow container mx-auto px-6 text-center flex flex-col justify-center">
<section class="py-16 sm:py-20">
<h1 class="text-4xl sm:text-6xl font-extrabold tracking-tight text-black dark:text-white">
Links that last <span class="text-gray-400 dark:text-neutral-500">forever</span>.
</h1>
<p class="mt-4 max-w-2xl mx-auto text-lg text-gray-600 dark:text-neutral-400">
A simple, fast, and reliable URL shortener for everyone.
</p>
<form id="shorten-form" class="mt-8 max-w-xl mx-auto flex flex-col sm:flex-row gap-3">
<label class="relative flex-1">
<i data-lucide="link" class="absolute top-1/2 left-4 -translate-y-1/2 w-5 h-5 text-gray-400 dark:text-neutral-500"></i>
<input id="url-input" type="text" placeholder="your-long-link.com" autocapitalize="off" spellcheck="false" class="w-full pl-11 pr-4 py-3 bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg focus:ring-2 focus:ring-black dark:focus:ring-white focus:outline-none transition">
</label>
<button id="submit-button" type="submit" class="px-6 py-3 font-semibold bg-black dark:bg-white text-white dark:text-black rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-wait">
Shorten
</button>
</form>
<div id="result-container" hidden class="mt-4 max-w-xl mx-auto flex gap-2">
<input id="result-output" type="text" readonly class="flex-1 p-3 bg-gray-100 dark:bg-neutral-900 border border-gray-200 dark:border-neutral-800 rounded-lg text-sm text-center truncate">
<button id="copy-button" type="button" title="Copy to clipboard" class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-lg hover:opacity-80 transition-opacity">
<i data-lucide="copy" class="w-5 h-5"></i>
</button>
<a id="open-link-button" target="_blank" rel="noopener" title="Open link in new tab" class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-lg hover:opacity-80 transition-opacity">
<i data-lucide="arrow-up-right" class="w-5 h-5"></i>
</a>
</div>
<p id="message-area" class="mt-3 text-red-500 text-sm h-5"></p>
</section>
</main>
<footer class="text-center py-8">
<p class="text-sm text-gray-500 dark:text-neutral-500">&copy; 2024 4ev.link. All Rights Reserved.</p>
</footer>
</div>
<script>
const $ = q => document.querySelector(q);
const form = $('#shorten-form');
const urlInput = $('#url-input');
const submitButton = $('#submit-button');
const messageArea = $('#message-area');
const resultContainer = $('#result-container');
const resultOutput = $('#result-output');
const copyButton = $('#copy-button');
const openLinkButton = $('#open-link-button');
const normalizeUrl = (str) => {
try { return new URL(str).href; }
catch (_) {
try { return new URL('https://' + str).href; }
catch (_) {}
}
};
form.onsubmit = async (e) => {
e.preventDefault();
messageArea.textContent = '';
const url = normalizeUrl(urlInput.value.trim());
if (!url) {
messageArea.textContent = 'Please enter a valid URL.';
return;
}
submitButton.disabled = true;
submitButton.textContent = '…';
try {
const response = await fetch('/api/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
if (!response.ok) {
throw new Error(await response.text() || response.statusText);
}
const data = await response.json();
resultContainer.hidden = false;
resultOutput.value = data.shortUrl;
openLinkButton.href = data.shortUrl;
} catch (err) {
resultContainer.hidden = true;
messageArea.textContent = err.message || 'Something went wrong.';
}
submitButton.disabled = false;
submitButton.textContent = 'Shorten';
};
copyButton.onclick = async () => {
resultOutput.select();
try {
await navigator.clipboard.writeText(resultOutput.value);
copyButton.innerHTML = '<i data-lucide="check" class="w-5 h-5 text-green-500"></i>';
lucide.createIcons();
setTimeout(() => {
copyButton.innerHTML = '<i data-lucide="copy" class="w-5 h-5"></i>';
lucide.createIcons();
}, 1000);
} catch (err) {
// Handle clipboard write error
}
};
resultOutput.onclick = () => resultOutput.select();
urlInput.oninput = () => {
resultContainer.hidden = true;
messageArea.textContent = '';
};
lucide.createIcons();
</script>
</body>
</html>