Files
4ev.link/index.html

115 lines
5.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<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>
<main x-data="shortener()">
<h1>4ev.link</h1>
<p>A simple, fast, and permanent URL shortener.</p>
<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>
<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>
</div>
</template>
</main>
<footer>
<a href="https://github.com/4ev-link/4ev.link" target="_blank" rel="noopener noreferrer">GitHub</a>
</footer>
<script>
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 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();
}
},
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>