zkt26/sk1/index.html

313 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ShortLink — URL Shortener</title>
<style>
:root {
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--accent: #6366f1;
--accent2: #4f46e5;
--text: #f1f5f9;
--muted: #94a3b8;
--success: #22c55e;
--error: #ef4444;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg); color: var(--text); min-height: 100vh;
}
/* ── Header ── */
header {
background: var(--surface); border-bottom: 1px solid var(--border);
padding: 1rem 2rem; display: flex; align-items: center; gap: .75rem;
}
.logo {
width: 34px; height: 34px; background: var(--accent); border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-weight: 700; font-size: 1.1rem; color: #fff; flex-shrink: 0;
}
header h1 { font-size: 1.4rem; font-weight: 700; }
header h1 span { color: var(--accent); }
.tagline { margin-left: auto; color: var(--muted); font-size: .85rem; }
/* ── Layout ── */
main { max-width: 860px; margin: 0 auto; padding: 2rem 1rem; }
/* ── Cards ── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem;
}
.card-title {
font-size: .75rem; font-weight: 600; letter-spacing: .08em;
color: var(--muted); text-transform: uppercase; margin-bottom: 1rem;
}
/* ── Form controls ── */
.input-row { display: flex; gap: .5rem; }
input[type="text"] {
flex: 1; background: var(--bg); border: 1px solid var(--border);
border-radius: 8px; padding: .7rem 1rem; color: var(--text);
font-size: 1rem; outline: none; transition: border-color .15s;
}
input[type="text"]:focus { border-color: var(--accent); }
input::placeholder { color: var(--muted); }
.btn {
background: var(--accent); color: #fff; border: none; border-radius: 8px;
padding: .7rem 1.4rem; font-size: 1rem; font-weight: 600; cursor: pointer;
transition: background .15s; white-space: nowrap;
}
.btn:hover { background: var(--accent2); }
.btn:active { transform: scale(.98); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-sm {
padding: .4rem .9rem; font-size: .85rem; font-weight: 500;
}
.btn-ghost {
background: transparent; color: var(--muted); border: 1px solid var(--border);
}
.btn-ghost:hover { background: var(--border); color: var(--text); }
.toggle-link {
display: inline-block; margin-top: .6rem; color: var(--muted);
font-size: .85rem; cursor: pointer; user-select: none;
}
.toggle-link:hover { color: var(--text); }
.advanced { margin-top: .75rem; display: none; }
.advanced label { display: block; font-size: .85rem; color: var(--muted); margin-bottom: .3rem; }
/* ── Result ── */
.result-box {
margin-top: 1rem; background: var(--bg); border-radius: 8px;
padding: .9rem 1rem; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
}
.result-box a { color: var(--accent); font-weight: 600; word-break: break-all; flex: 1; }
.result-box a:hover { text-decoration: underline; }
.msg {
margin-top: .6rem; padding: .45rem .9rem; border-radius: 6px; font-size: .88rem;
}
.msg.ok { background: rgba(34,197,94,.1); color: var(--success); }
.msg.err { background: rgba(239,68,68,.1); color: var(--error); }
/* ── Stats bar ── */
.stats-bar {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 1rem;
}
.pills { display: flex; gap: .75rem; }
.pill {
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
padding: .4rem .9rem; text-align: center; font-size: .82rem;
}
.pill strong { display: block; font-size: 1.2rem; color: var(--accent); }
/* ── Table ── */
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: .88rem; }
th {
text-align: left; padding: .5rem .75rem; color: var(--muted);
font-weight: 500; border-bottom: 1px solid var(--border);
}
td { padding: .7rem .75rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(99,102,241,.04); }
.code-tag {
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
padding: .15rem .45rem; font-family: monospace; font-size: .82rem;
}
.url-cell { max-width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.visits-badge {
display: inline-block; background: var(--accent); color: #fff;
border-radius: 99px; padding: .15rem .55rem; font-size: .75rem; font-weight: 600;
}
.go-btn {
color: var(--accent); border: 1px solid var(--accent); border-radius: 4px;
padding: .2rem .55rem; font-size: .78rem; cursor: pointer; background: transparent;
transition: background .15s, color .15s;
}
.go-btn:hover { background: var(--accent); color: #fff; }
.del-btn {
color: var(--error); border: 1px solid var(--error); border-radius: 4px;
padding: .2rem .55rem; font-size: .78rem; cursor: pointer; background: transparent;
}
.del-btn:hover { background: var(--error); color: #fff; }
.empty { color: var(--muted); text-align: center; padding: 2.5rem; }
/* ── Footer ── */
footer {
text-align: center; color: var(--muted); font-size: .8rem;
padding: 2rem 1rem; border-top: 1px solid var(--border); margin-top: 1rem;
}
</style>
</head>
<body>
<header>
<div class="logo">S</div>
<h1>Short<span>Link</span></h1>
<span class="tagline">Shorten, share & track URLs</span>
</header>
<main>
<!-- ── Shorten form ── -->
<div class="card">
<p class="card-title">✂ Shorten a URL</p>
<div class="input-row">
<input type="text" id="urlInput" placeholder="https://very-long-url.example.com/path/to/page" />
<button class="btn" onclick="shorten()" id="shortenBtn">Shorten</button>
</div>
<span class="toggle-link" onclick="toggleAdvanced()">⚙ Custom code / title</span>
<div class="advanced" id="advPanel">
<div style="display:flex;gap:.75rem;flex-wrap:wrap;margin-top:.5rem">
<div style="flex:1;min-width:160px">
<label>Custom short code (optional)</label>
<input type="text" id="customCode" placeholder="my-link" />
</div>
<div style="flex:1;min-width:160px">
<label>Title / description (optional)</label>
<input type="text" id="titleInput" placeholder="My awesome page" />
</div>
</div>
</div>
<div id="resultArea"></div>
</div>
<!-- ── Analytics ── -->
<div class="card">
<div class="stats-bar">
<p class="card-title" style="margin:0">📊 Analytics</p>
<button class="btn btn-sm btn-ghost" onclick="loadStats()">↻ Refresh</button>
</div>
<div class="pills" id="pills"></div>
<div class="tbl-wrap" id="tblArea" style="margin-top:1rem"><div class="empty">Loading…</div></div>
</div>
</main>
<footer>ShortLink — Cloud Deployment Assignment · Built with FastAPI + PostgreSQL + Nginx</footer>
<script>
const API = '';
async function shorten() {
const url = document.getElementById('urlInput').value.trim();
const code = document.getElementById('customCode').value.trim();
const title = document.getElementById('titleInput').value.trim();
const out = document.getElementById('resultArea');
const btn = document.getElementById('shortenBtn');
if (!url) { out.innerHTML = '<div class="msg err">Please enter a URL.</div>'; return; }
btn.disabled = true; btn.textContent = 'Shortening…';
try {
const resp = await fetch(`${API}/api/shorten`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ url, custom_code: code || undefined, title: title || undefined })
});
const data = await resp.json();
if (!resp.ok) {
out.innerHTML = `<div class="msg err">Error: ${data.detail}</div>`;
return;
}
out.innerHTML = `
<div class="result-box">
<a href="${data.short_url}" target="_blank" rel="noopener">${data.short_url}</a>
<button class="btn btn-sm" onclick="copy('${data.short_url}', this)">Copy</button>
</div>
<div class="msg ok">✓ Short link created successfully!</div>`;
document.getElementById('urlInput').value = '';
loadStats();
} catch (e) {
out.innerHTML = `<div class="msg err">Network error: ${e.message}</div>`;
} finally {
btn.disabled = false; btn.textContent = 'Shorten';
}
}
async function copy(url, btn) {
await navigator.clipboard.writeText(url);
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 2000);
}
function toggleAdvanced() {
const p = document.getElementById('advPanel');
p.style.display = p.style.display === 'block' ? 'none' : 'block';
}
async function loadStats() {
try {
const resp = await fetch(`${API}/api/stats`);
const data = await resp.json();
document.getElementById('pills').innerHTML = `
<div class="pill"><strong>${data.total_links}</strong>Links</div>
<div class="pill"><strong>${data.total_visits}</strong>Visits</div>`;
if (!data.links.length) {
document.getElementById('tblArea').innerHTML = '<div class="empty">No links yet. Shorten your first URL above!</div>';
return;
}
const rows = data.links.map(l => `
<tr>
<td><span class="code-tag">${l.code}</span></td>
<td class="url-cell" title="${l.original_url}">${l.title || l.original_url}</td>
<td class="url-cell" title="${l.original_url}" style="color:var(--muted);font-size:.8rem">${l.original_url}</td>
<td><span class="visits-badge">${l.visit_count}</span></td>
<td style="color:var(--muted);font-size:.78rem">${new Date(l.created_at).toLocaleDateString()}</td>
<td>
<button class="go-btn" onclick="window.open('${l.short_url}','_blank')">Open</button>
<button class="del-btn" onclick="deleteLink('${l.code}')" style="margin-left:4px">Del</button>
</td>
</tr>`).join('');
document.getElementById('tblArea').innerHTML = `
<table>
<thead><tr>
<th>Code</th><th>Title</th><th>Original URL</th>
<th>Visits</th><th>Created</th><th>Actions</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
} catch (e) {
document.getElementById('tblArea').innerHTML = '<div class="empty">Could not load statistics.</div>';
}
}
async function deleteLink(code) {
const token = prompt('Enter admin token to delete:');
if (!token) return;
const resp = await fetch(`${API}/api/links/${code}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` }
});
if (resp.ok) loadStats();
else alert('Delete failed wrong token or link not found.');
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('urlInput').addEventListener('keydown', e => {
if (e.key === 'Enter') shorten();
});
loadStats();
setInterval(loadStats, 30000);
});
</script>
</body>
</html>