Add QR codes, link expiry, password protection, previews, audit trail and bulk operations
This commit is contained in:
1 parent
157aa81252
commit
bcbcc90d98
88 files changed
+5354
-815
No files matched your search
+64
-4
@@ -49,6 +49,21 @@
|
||||
<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>
|
||||
@@ -74,6 +89,7 @@
|
||||
<tr>
|
||||
<th>Short Link</th>
|
||||
<th>Destination</th>
|
||||
<th>QR Code</th>
|
||||
<th>Health</th>
|
||||
<th>Tags</th>
|
||||
<th>Created</th>
|
||||
@@ -83,7 +99,7 @@
|
||||
<tbody>
|
||||
{% if urls.is_empty() %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
<td colspan="7" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||
No shortened URLs registered. Create one to get started!
|
||||
</td>
|
||||
</tr>
|
||||
@@ -91,9 +107,16 @@
|
||||
{% for url in urls %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/{{ url.code }}" target="_blank" style="color: var(--accent-color); font-weight: 700; text-decoration: none; font-family: monospace; font-size: 1rem;">
|
||||
bzo.in/{{ url.code }}
|
||||
</a>
|
||||
<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>
|
||||
@@ -109,6 +132,43 @@
|
||||
</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 }}">
|
||||
|
||||
Reference in new issue
Block a user