207 lines
13 KiB
HTML
207 lines
13 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block title %}Manage Short URLs - BZOD{% endblock %}
|
|
|
|
{% block active_urls %}active{% endblock %}
|
|
|
|
{% block header_title %}Short URL Registry{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if let Some(err) = error %}
|
|
<div class="alert alert-error">
|
|
{{ err }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; align-items: start;">
|
|
<!-- Create Link Form -->
|
|
<div class="card">
|
|
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
Shorten a New URL
|
|
</h3>
|
|
|
|
<form action="/admin/urls/create" method="POST">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
|
|
<div class="form-group">
|
|
<label for="destination">Destination URL *</label>
|
|
<input type="url" id="destination" name="destination" class="form-input" placeholder="https://example.com/very-long-path" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="code">Short Code (6-Hex, optional)</label>
|
|
<input type="text" id="code" name="code" class="form-input" placeholder="e.g. 4f8c1a (auto-generated if empty)" pattern="[0-9a-fA-F]{6}" title="Must be exactly 6 hex characters (0-9, a-f)">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="title">Title (optional)</label>
|
|
<input type="text" id="title" name="title" class="form-input" placeholder="e.g. My Blog Post">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="description">Description (optional)</label>
|
|
<textarea id="description" name="description" class="form-input" placeholder="Notes or summary..." rows="2"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="tags">Tags (comma-separated, optional)</label>
|
|
<input type="text" id="tags" name="tags" class="form-input" placeholder="e.g. blog, tech, personal">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="expires_at">Expiration Date & Time (optional)</label>
|
|
<input type="datetime-local" id="expires_at" name="expires_at" class="form-input">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password Protection (optional)</label>
|
|
<input type="password" id="password" name="password" class="form-input" placeholder="Leave blank for public access">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="max_access_count">Access Limit / One-Time (optional)</label>
|
|
<input type="number" id="max_access_count" name="max_access_count" class="form-input" placeholder="e.g. 10 (auto-expires after N clicks)" min="1">
|
|
</div>
|
|
|
|
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Create Link</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Links Table -->
|
|
<div class="card" style="padding: 0; overflow: hidden;">
|
|
<div style="padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center;">
|
|
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
Registered Links
|
|
</h3>
|
|
{% if let Some(tag) = tag_filter %}
|
|
<span class="badge badge-healthy" style="text-transform: none;">
|
|
Filtered by tag: {{ tag }}
|
|
<a href="/admin/urls" style="color: inherit; text-decoration: none; margin-left: 0.5rem; font-weight: bold;">×</a>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Short Link</th>
|
|
<th>Destination</th>
|
|
<th>QR Code</th>
|
|
<th>Health</th>
|
|
<th>Tags</th>
|
|
<th>Created</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if urls.is_empty() %}
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
|
No shortened URLs registered. Create one to get started!
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
{% for url in urls %}
|
|
<tr>
|
|
<td>
|
|
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
<a href="/{{ url.code }}" target="_blank" style="color: var(--accent-color); font-weight: 700; text-decoration: none; font-family: monospace; font-size: 1rem;">
|
|
{{ base_url.replace("https://", "").replace("http://", "") }}/{{ url.code }}
|
|
</a>
|
|
{% if url.is_password_protected() %}
|
|
<span title="Password Protected" style="color: #fbbf24; display: inline-flex; align-items: center;">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.25rem; font-weight: 500;">
|
|
{{ url.title.as_deref().unwrap_or("") }}
|
|
</div>
|
|
</td>
|
|
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
<a href="{{ url.destination }}" target="_blank" style="color: var(--text-secondary); text-decoration: none; font-size: 0.85rem;" title="{{ url.destination }}">
|
|
{{ url.destination }}
|
|
</a>
|
|
{% if let Some(desc) = url.description.as_deref() %}
|
|
{% if !desc.is_empty() %}
|
|
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;">
|
|
{{ desc }}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if let Some(expires_at) = url.expires_at.as_deref() %}
|
|
{% if !expires_at.is_empty() %}
|
|
<div style="font-size: 0.75rem; color: #fca5a5; margin-top: 0.25rem; display: flex; align-items: center; gap: 0.25rem;">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
|
Expires: {{ expires_at[0..10] }} {{ expires_at[11..16] }}
|
|
{% if url.expired %}
|
|
<span class="badge badge-dead" style="font-size: 0.6rem; padding: 0.1rem 0.25rem; margin-left: 0.25rem;">Expired</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if let Some(max) = url.max_access_count %}
|
|
<div style="font-size: 0.75rem; color: #38bdf8; margin-top: 0.25rem; display: flex; align-items: center; gap: 0.25rem;">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
|
Clicks: {{ url.access_count }} / {{ max }}
|
|
{% if url.is_access_exhausted() %}
|
|
<span class="badge badge-dead" style="font-size: 0.6rem; padding: 0.1rem 0.25rem; margin-left: 0.25rem;">Limit Reached</span>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
{% if url.access_count > 0 %}
|
|
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;">
|
|
Clicks: {{ url.access_count }}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</td>
|
|
<td style="text-align: center; vertical-align: middle;">
|
|
<a href="/api/qr/{{ url.code }}.png" target="_blank" title="View QR Code">
|
|
<img src="/api/qr/{{ url.code }}.svg" alt="QR" style="width: 32px; height: 32px; border-radius: 4px; border: 1px solid var(--border-color); background: white; padding: 1px;">
|
|
</a>
|
|
<div style="margin-top: 0.25rem; display: flex; gap: 0.25rem; justify-content: center;">
|
|
<a href="/api/qr/{{ url.code }}.png" download class="badge" style="font-size: 0.65rem; background-color: rgba(99, 102, 241, 0.1); color: #818cf8; text-decoration: none; padding: 0.1rem 0.25rem;">PNG</a>
|
|
<a href="/api/qr/{{ url.code }}.svg" download class="badge" style="font-size: 0.65rem; background-color: rgba(99, 102, 241, 0.1); color: #818cf8; text-decoration: none; padding: 0.1rem 0.25rem;">SVG</a>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-{{ url.status }}">
|
|
{{ url.status }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap; max-width: 150px;">
|
|
{% for t in url.tags %}
|
|
<a href="/admin/urls?tag={{ t }}" class="badge" style="background-color: rgba(255,255,255,0.05); color: var(--text-secondary); text-decoration: none; font-size: 0.7rem; border: 1px solid var(--border-color);">
|
|
{{ t }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</td>
|
|
<td style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
{{ url.created_at[0..10] }}
|
|
</td>
|
|
<td>
|
|
<form action="/admin/urls/delete/{{ url.id }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this link?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.6rem; font-size: 0.8rem;">
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|