226 lines
14 KiB
HTML
226 lines
14 KiB
HTML
{% extends "user_layout.html" %}
|
|
|
|
{% block title %}My Short URLs - BZOD{% endblock %}
|
|
|
|
{% block active_urls %}active{% endblock %}
|
|
|
|
{% block header_title %}My Short URL Library{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if let Some(err) = error %}
|
|
<div class="alert alert-error">
|
|
{{ err }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="urls-dashboard">
|
|
<div class="card url-form-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>
|
|
Create a New URL
|
|
</h3>
|
|
|
|
<form action="/user/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="custom_slug">Custom Slug (optional)</label>
|
|
<input type="text" id="custom_slug" name="custom_slug" class="form-input" placeholder="e.g. !home" pattern="![a-z0-9\-_]{1,24}" title="Must start with ! followed by 1-24 characters (a-z, 0-9, -, _)">
|
|
</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" rows="2" placeholder="Notes or summary..."></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>
|
|
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Create Link</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card registered-links-shell" style="padding: 0; overflow: hidden;">
|
|
<div class="registered-links-header">
|
|
<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>
|
|
My Short Links
|
|
</h3>
|
|
{% if let Some(tag) = tag_filter %}
|
|
<span class="badge badge-healthy" style="text-transform: none;">
|
|
Filtered by tag: {{ tag }}
|
|
<a href="/user/urls" style="color: inherit; text-decoration: none; margin-left: 0.5rem; font-weight: bold;">×</a>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="registered-links-table">
|
|
<colgroup>
|
|
<col class="col-short">
|
|
<col class="col-destination">
|
|
<col class="col-qr">
|
|
<col class="col-health">
|
|
<col class="col-tags">
|
|
<col class="col-actions">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>Short Link</th>
|
|
<th>Destination</th>
|
|
<th>QR Code</th>
|
|
<th>Health</th>
|
|
<th>Tags</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if urls.is_empty() %}
|
|
<tr>
|
|
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
|
No short URLs created yet.
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
{% for url in urls %}
|
|
<tr>
|
|
<td data-label="Short Link">
|
|
<div class="short-link-wrap">
|
|
<div class="short-link-primary">
|
|
<a href="/{{ url.code }}" target="_blank" class="short-link-anchor">
|
|
{{ base_url.replace("https://", "").replace("http://", "") }}/{{ url.code }}
|
|
</a>
|
|
{% if url.is_password_protected() %}
|
|
<span title="Password Protected" aria-label="Password protected link" style="color: var(--warning-color); 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>
|
|
{% if let Some(title) = url.title.as_deref() %}
|
|
{% if !title.is_empty() %}
|
|
<div class="short-link-title">{{ title }}</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
<div class="short-link-meta">Created {{ url.created_at[0..10] }}</div>
|
|
</div>
|
|
</td>
|
|
<td data-label="Destination">
|
|
<div class="destination-block">
|
|
<a href="{{ url.destination }}" target="_blank" class="destination-link" title="{{ url.destination }}" aria-label="Destination URL: {{ url.destination }}">
|
|
{{ url.destination }}
|
|
</a>
|
|
{% if let Some(desc) = url.description.as_deref() %}
|
|
{% if !desc.is_empty() %}
|
|
<div class="destination-description" title="{{ desc }}">{{ desc }}</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if let Some(expires_at) = url.expires_at.as_deref() %}
|
|
{% if !expires_at.is_empty() %}
|
|
<div class="destination-meta expiry">
|
|
<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 class="destination-meta access">
|
|
<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 class="destination-meta neutral">
|
|
Clicks: {{ url.access_count }}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
{% let code = url.code.as_str() %}
|
|
{% include "components/qr_preview.html" %}
|
|
<td data-label="Health" class="status-cell">
|
|
<span class="badge badge-{{ url.status }}">
|
|
{{ url.status }}
|
|
</span>
|
|
</td>
|
|
<td data-label="Tags">
|
|
{% if url.tags.is_empty() %}
|
|
<span class="tags-empty">—</span>
|
|
{% else %}
|
|
<div class="tags-wrap">
|
|
{% for t in url.tags %}
|
|
<a href="/user/urls?tag={{ t }}" class="badge tag-badge">
|
|
{{ t }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td data-label="Actions" class="actions-cell">
|
|
<div class="action-stack">
|
|
<a href="/user/analytics/url/{{ url.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.75rem; font-size: 0.85rem; text-decoration: none; display: inline-flex; align-items: center; justify-content: center;">
|
|
📊 Analytics
|
|
</a>
|
|
<form action="/user/urls/delete/{{ url.id }}" method="POST" onsubmit="return confirm('Delete this link?');">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.75rem;">
|
|
Delete
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination">
|
|
{% if current_page > 1 %}
|
|
<a href="/user/urls?page=1{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;"><< First</a>
|
|
<a href="/user/urls?page={{ current_page - 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">< Prev</a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;"><< First</span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">< Prev</span>
|
|
{% endif %}
|
|
|
|
{% for p in visible_pages %}
|
|
{% if self.is_current(p) %}
|
|
<span class="btn" style="padding: 0.4rem 0.8rem; font-weight: 700;">{{ p }}</span>
|
|
{% else %}
|
|
<a href="/user/urls?page={{ p }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">{{ p }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if current_page < total_pages %}
|
|
<a href="/user/urls?page={{ current_page + 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">Next ></a>
|
|
<a href="/user/urls?page={{ total_pages }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">Last >></a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">Next ></span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">Last >></span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|