mirror of
https://github.com/multipleof4/4ev.link.git
synced 2026-01-13 15:57:53 +00:00
Refactor: Replace AlpineJS with vanilla JS & tweak UI
This commit is contained in:
104
index.html
104
index.html
@@ -5,11 +5,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>4ev.link - Simple & Fast URL Shortener</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/umd/lucide.min.js"></script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
[x-cloak] { display: none !important; }
|
||||
.hidden { display: none; }
|
||||
</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -23,73 +22,92 @@
|
||||
<p class="mt-3 text-lg text-gray-600">Shorten. Share. Forever.</p>
|
||||
</header>
|
||||
|
||||
<div x-data="urlShortener()" class="bg-white p-6 sm:p-8 rounded-xl shadow-md">
|
||||
<form @submit.prevent="shorten()">
|
||||
<div class="bg-white p-6 sm:p-8 rounded-xl shadow-md">
|
||||
<form id="shorten-form">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<label for="url-input" class="sr-only">URL to shorten</label>
|
||||
<input x-model="longUrl" type="url" id="url-input" placeholder="https://your-long-url.com/goes-here" required class="flex-grow w-full px-4 py-3 bg-gray-100 border border-gray-200 rounded-lg focus:ring-2 focus:ring-gray-600 focus:outline-none transition">
|
||||
<button type="submit" :disabled="loading" class="inline-flex items-center justify-center px-6 py-3 font-semibold text-white bg-gray-800 rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-800 focus:ring-offset-2 disabled:bg-gray-400 transition">
|
||||
<i x-show="loading" x-cloak class="animate-spin -ml-1 mr-3 h-5 w-5" data-lucide="loader-2"></i>
|
||||
<span x-text="loading ? 'Working...' : 'Shorten Link'">Shorten Link</span>
|
||||
<input type="url" id="url-input" placeholder="https://your-long-url.com/goes-here" required class="flex-grow w-full px-4 py-3 bg-gray-100 border border-gray-200 rounded-lg focus:ring-2 focus:ring-gray-600 focus:outline-none transition">
|
||||
<button type="submit" id="submit-btn" class="inline-flex items-center justify-center px-6 py-3 font-semibold text-white bg-gray-800 rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-800 focus:ring-offset-2 disabled:bg-gray-400 transition">
|
||||
<i id="loader" class="hidden animate-spin -ml-1 mr-3 h-5 w-5" data-lucide="loader-2"></i>
|
||||
<span id="btn-text">Shorten Link</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div x-show="error" x-cloak class="mt-4 p-3 text-sm text-red-700 bg-red-100 rounded-lg" x-text="error"></div>
|
||||
<div x-show="result" x-cloak class="mt-4 p-3 bg-gray-100 rounded-lg">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<a :href="result?.shortUrl" x-text="result?.shortUrl" target="_blank" class="font-mono text-gray-700 hover:underline truncate"></a>
|
||||
<button @click="copyToClipboard(result?.shortUrl)" class="flex-shrink-0 inline-flex items-center justify-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-md hover:bg-gray-50">
|
||||
<i :class="{'text-green-500': copied}" :data-lucide="copied ? 'check' : 'copy'" class="w-4 h-4"></i>
|
||||
<span x-text="copied ? 'Copied!' : 'Copy'">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="error-box" class="hidden mt-4 p-3 text-sm text-red-700 bg-red-100 rounded-lg"></div>
|
||||
<div id="result-box" class="hidden mt-4 p-3 bg-gray-100 rounded-lg"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4">
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-6 text-center">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-100 rounded-lg mb-4"><i data-lucide="shuffle" class="w-6 h-6 text-gray-700"></i></div>
|
||||
<h3 class="text-xl font-bold text-gray-800">Free Plan</h3>
|
||||
<p class="mt-2 text-gray-600">Perfect for quick sharing. Get a random, memorable short URL instantly and for free.</p>
|
||||
<p class="mt-2 text-gray-600 font-mono">Perfect for quick sharing. Get a random, memorable short URL instantly and for free.</p>
|
||||
</div>
|
||||
<div class="bg-white border-2 border-gray-800 rounded-xl p-6 text-center shadow-lg">
|
||||
<div class="inline-flex items-center justify-center w-12 h-12 bg-gray-800 rounded-lg mb-4"><i data-lucide="crown" class="w-6 h-6 text-white"></i></div>
|
||||
<h3 class="text-xl font-bold text-gray-800">Paid Plan</h3>
|
||||
<p class="mt-2 text-gray-600">Brand your links with custom slugs. More control for professionals and businesses.</p>
|
||||
<p class="mt-2 text-gray-600 font-mono">Brand your links with custom slugs. More control for professionals and businesses.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center text-gray-500 text-sm !mt-16">
|
||||
<p>© <span x-text="new Date().getFullYear()"></span> 4ev.link</p>
|
||||
<p>© <span id="year"></span> 4ev.link</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('urlShortener', () => ({
|
||||
longUrl: '', result: null, loading: false, error: '', copied: false,
|
||||
async shorten() {
|
||||
this.loading = true; this.error = ''; this.result = null; this.copied = false;
|
||||
try {
|
||||
new URL(this.longUrl);
|
||||
const r = await fetch('/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.longUrl }) });
|
||||
if (!r.ok) throw new Error(await r.text() || 'Server error');
|
||||
this.result = await r.json(); this.longUrl = '';
|
||||
} catch (e) { this.error = e.message.includes('Invalid URL') ? 'Please enter a valid URL.' : 'An unexpected error occurred.'; }
|
||||
finally { this.loading = false; this.$nextTick(() => lucide.createIcons()); }
|
||||
},
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 2000);
|
||||
this.$nextTick(() => lucide.createIcons());
|
||||
});
|
||||
}
|
||||
}))
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const D=document, form=D.getElementById('shorten-form'), urlIn=D.getElementById('url-input'),
|
||||
submitBtn=D.getElementById('submit-btn'), btnTxt=D.getElementById('btn-text'),
|
||||
loader=D.getElementById('loader'), errBox=D.getElementById('error-box'),
|
||||
resBox=D.getElementById('result-box');
|
||||
|
||||
const copy = (txt, btn) => {
|
||||
navigator.clipboard.writeText(txt).then(() => {
|
||||
btn.innerHTML = `<i data-lucide="check" class="w-4 h-4 text-green-500"></i><span>Copied!</span>`;
|
||||
lucide.createIcons();
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = `<i data-lucide="copy" class="w-4 h-4"></i><span>Copy</span>`;
|
||||
lucide.createIcons();
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
submitBtn.disabled=true; loader.classList.remove('hidden'); btnTxt.textContent='Working...';
|
||||
errBox.classList.add('hidden'); resBox.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const url = urlIn.value; new URL(url);
|
||||
const r = await fetch('/', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});
|
||||
if (!r.ok) throw new Error(await r.text() || 'Server error');
|
||||
const d = await r.json();
|
||||
|
||||
resBox.innerHTML = `
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<a href="${d.shortUrl}" target="_blank" class="font-mono text-gray-700 hover:underline truncate">${d.shortUrl}</a>
|
||||
<button id="copy-btn" class="flex-shrink-0 inline-flex items-center justify-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-md hover:bg-gray-50">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i><span>Copy</span>
|
||||
</button>
|
||||
</div>`;
|
||||
resBox.classList.remove('hidden');
|
||||
D.getElementById('copy-btn').onclick = function(){ copy(d.shortUrl, this) };
|
||||
urlIn.value = '';
|
||||
lucide.createIcons();
|
||||
} catch (err) {
|
||||
errBox.textContent = err.message.includes('Invalid URL') ? 'Please enter a valid URL.' : 'An unexpected error occurred.';
|
||||
errBox.classList.remove('hidden');
|
||||
} finally {
|
||||
submitBtn.disabled=false; loader.classList.add('hidden'); btnTxt.textContent='Shorten Link';
|
||||
}
|
||||
});
|
||||
|
||||
D.getElementById('year').textContent = new Date().getFullYear();
|
||||
lucide.createIcons();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user