Refactor responsive UI and build metadata
This commit is contained in:
1 parent
f49698bb5c
commit
b66703082f
10 files changed
+1524
-211
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||||
|
println!("cargo:rerun-if-changed=.git/refs");
|
||||||
|
|
||||||
|
if let Ok(output) = Command::new("git")
|
||||||
|
.args(["rev-parse", "--short=12", "HEAD"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if !commit.is_empty() {
|
||||||
|
println!("cargo:rustc-env=BZOD_GIT_COMMIT={commit}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
pub const GIT_COMMIT: &str = match option_env!("BZOD_GIT_COMMIT") {
|
||||||
|
Some(commit) => commit,
|
||||||
|
None => "unknown",
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod build_info;
|
||||||
pub mod charts;
|
pub mod charts;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|||||||
@@ -773,8 +773,8 @@ pub async fn status_get(State(state): State<AppState>, jar: CookieJar) -> Respon
|
|||||||
queue_size,
|
queue_size,
|
||||||
memory_usage,
|
memory_usage,
|
||||||
uptime,
|
uptime,
|
||||||
version: "0.1.0",
|
version: crate::build_info::APP_VERSION,
|
||||||
git_commit: "unknown",
|
git_commit: crate::build_info::GIT_COMMIT,
|
||||||
urls,
|
urls,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -825,8 +825,8 @@ pub async fn user_status_get(State(state): State<AppState>, jar: CookieJar) -> R
|
|||||||
queue_size,
|
queue_size,
|
||||||
memory_usage,
|
memory_usage,
|
||||||
uptime,
|
uptime,
|
||||||
version: "0.1.0",
|
version: crate::build_info::APP_VERSION,
|
||||||
git_commit: "unknown",
|
git_commit: crate::build_info::GIT_COMMIT,
|
||||||
urls,
|
urls,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -79,8 +79,8 @@ pub async fn status_endpoint(
|
|||||||
queue_size: 0,
|
queue_size: 0,
|
||||||
memory_usage: get_memory_usage(),
|
memory_usage: get_memory_usage(),
|
||||||
uptime_seconds: uptime,
|
uptime_seconds: uptime,
|
||||||
version: "0.1.0",
|
version: crate::build_info::APP_VERSION,
|
||||||
git_commit: "unknown",
|
git_commit: crate::build_info::GIT_COMMIT,
|
||||||
})
|
})
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|||||||
+1240
-70
File diff suppressed because it is too large.
Load diff
+42
-39
@@ -13,8 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start;">
|
<div class="urls-dashboard">
|
||||||
<!-- Create Landing Page Form -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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>
|
<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>
|
||||||
@@ -24,7 +23,7 @@
|
|||||||
<form action="/admin/pages/create" method="POST">
|
<form action="/admin/pages/create" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
<div class="page-form-grid-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Page Title *</label>
|
<label for="title">Page Title *</label>
|
||||||
<input type="text" id="title" name="title" class="form-input" placeholder="e.g. Summer Campaign" required>
|
<input type="text" id="title" name="title" class="form-input" placeholder="e.g. Summer Campaign" required>
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem;">
|
<div class="page-form-grid-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="code">Short Code (4-Hex, optional)</label>
|
<label for="code">Short Code (4-Hex, optional)</label>
|
||||||
<input type="text" id="code" name="code" class="form-input" placeholder="e.g. a1b2" pattern="[0-9a-fA-F]{4}" title="Must be exactly 4 hex characters">
|
<input type="text" id="code" name="code" class="form-input" placeholder="e.g. a1b2" pattern="[0-9a-fA-F]{4}" title="Must be exactly 4 hex characters">
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="state">Publish State</label>
|
<label for="state">Publish State</label>
|
||||||
<select id="state" name="state">
|
<select id="state" name="state" class="form-input">
|
||||||
<option value="draft">Draft</option>
|
<option value="draft">Draft</option>
|
||||||
<option value="published" selected>Published</option>
|
<option value="published" selected>Published</option>
|
||||||
<option value="archived">Archived</option>
|
<option value="archived">Archived</option>
|
||||||
@@ -59,16 +58,15 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="html_content">Raw HTML Document *</label>
|
<label for="html_content">Raw HTML Document *</label>
|
||||||
<textarea id="html_content" name="html_content" class="form-input" style="font-family: monospace; font-size: 0.85rem; height: 300px; background-color: #05070f; border-color: rgba(255,255,255,0.05);" placeholder="<!DOCTYPE html> <html> <head> <title>Landing Page</title> </head> <body> <h1>Welcome!</h1> </body> </html>" required></textarea>
|
<textarea id="html_content" name="html_content" class="form-input page-html-editor" placeholder="<!DOCTYPE html> <html> <head> <title>Landing Page</title> </head> <body> <h1>Welcome!</h1> </body> </html>" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Save Page</button>
|
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Save Page</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Landing Pages Table -->
|
<div class="card registered-links-shell" style="padding: 0; overflow: hidden;">
|
||||||
<div class="card" style="padding: 0; overflow: hidden;">
|
<div class="registered-links-header">
|
||||||
<div style="padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border-color);">
|
|
||||||
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
Registered Pages
|
Registered Pages
|
||||||
@@ -76,69 +74,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table class="registered-pages-table">
|
||||||
|
<colgroup>
|
||||||
|
<col class="col-page-title">
|
||||||
|
<col class="col-page-paths">
|
||||||
|
<col class="col-page-status">
|
||||||
|
<col class="col-page-qr">
|
||||||
|
<col class="col-page-actions">
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Page Title</th>
|
<th>Page</th>
|
||||||
<th>Short Path</th>
|
<th>Paths</th>
|
||||||
<th>SEO Preview Path</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Analytics</th>
|
|
||||||
<th>QR Code</th>
|
<th>QR Code</th>
|
||||||
<th>Created</th>
|
<th>Actions</th>
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if pages.is_empty() %}
|
{% if pages.is_empty() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||||
No landing pages registered. Create one to get started!
|
No landing pages registered. Create one to get started!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for page in pages %}
|
{% for page in pages %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="Page">
|
||||||
<div style="font-weight: 700; color: var(--text-primary);">
|
<div class="page-title-block">
|
||||||
{{ page.title }}
|
<div class="page-title-heading">{{ page.title }}</div>
|
||||||
</div>
|
<div class="page-title-meta">UUID: {{ page.id[0..8] }}...</div>
|
||||||
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
|
<div class="page-title-meta">Created {{ page.created_at[0..10] }}</div>
|
||||||
UUID: {{ page.id[0..8] }}...
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Paths">
|
||||||
<a href="/p/{{ page.code }}" target="_blank" style="color: var(--accent-color); font-weight: 700; text-decoration: none; font-family: monospace;">
|
<div class="page-paths">
|
||||||
|
<div class="page-path-group">
|
||||||
|
<span class="page-path-caption">Short Path</span>
|
||||||
|
<a href="/p/{{ page.code }}" target="_blank" class="page-path-link">
|
||||||
/p/{{ page.code }}
|
/p/{{ page.code }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="page-path-group">
|
||||||
<a href="/p/{{ page.code }}/{{ page.slug }}" target="_blank" style="color: var(--text-secondary); text-decoration: none; font-size: 0.85rem; font-family: monospace;">
|
<span class="page-path-caption">SEO Preview Path</span>
|
||||||
|
<a href="/p/{{ page.code }}/{{ page.slug }}" target="_blank" class="page-path-link secondary">
|
||||||
/p/{{ page.code }}/{{ page.slug }}
|
/p/{{ page.code }}/{{ page.slug }}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Status" class="status-cell">
|
||||||
<span class="badge badge-{{ page.state }}">
|
<span class="badge badge-{{ page.state }}">
|
||||||
{{ page.state }}
|
{{ page.state }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
{% let code = page.code.as_str() %}
|
||||||
|
{% include "components/qr_preview.html" %}
|
||||||
|
<td data-label="Actions" class="actions-cell">
|
||||||
|
<div class="action-stack">
|
||||||
<a href="/admin/analytics/page/{{ page.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
<a href="/admin/analytics/page/{{ page.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||||
📊 Analytics
|
📊 Analytics
|
||||||
</a>
|
</a>
|
||||||
</td>
|
|
||||||
{% let code = page.code.as_str() %}
|
|
||||||
{% include "components/qr_preview.html" %}
|
|
||||||
<td style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
||||||
{{ page.created_at[0..10] }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form action="/admin/pages/delete/{{ page.id }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this page?');">
|
<form action="/admin/pages/delete/{{ page.id }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this page?');">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<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;">
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.6rem; font-size: 0.8rem;">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -147,8 +151,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<div class="pagination">
|
||||||
<div class="pagination" style="display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1rem; padding: 1rem; border-top: 1px solid var(--border-color);">
|
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a href="/admin/pages?page=1" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
<a href="/admin/pages?page=1" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
||||||
<a href="/admin/pages?page={{ current_page - 1 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
<a href="/admin/pages?page={{ current_page - 1 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
||||||
|
|||||||
+54
-37
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
{% block active_urls %}active{% endblock %}
|
{% block active_urls %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
#utm_fields > div {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block header_title %}Short URL Registry{% endblock %}
|
{% block header_title %}Short URL Registry{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -13,9 +21,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; align-items: start;">
|
<div class="urls-dashboard">
|
||||||
<!-- Create Link Form -->
|
<div class="card url-form-card">
|
||||||
<div class="card">
|
|
||||||
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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>
|
<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
|
Shorten a New URL
|
||||||
@@ -97,9 +104,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Links Table -->
|
<div class="card registered-links-shell" style="padding: 0; overflow: hidden;">
|
||||||
<div class="card" style="padding: 0; overflow: hidden;">
|
<div class="registered-links-header">
|
||||||
<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;">
|
<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>
|
<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
|
Registered Links
|
||||||
@@ -113,7 +119,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Short Link</th>
|
<th>Short Link</th>
|
||||||
@@ -121,51 +135,53 @@
|
|||||||
<th>QR Code</th>
|
<th>QR Code</th>
|
||||||
<th>Health</th>
|
<th>Health</th>
|
||||||
<th>Tags</th>
|
<th>Tags</th>
|
||||||
<th>Analytics</th>
|
<th>Actions</th>
|
||||||
<th>Created</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if urls.is_empty() %}
|
{% if urls.is_empty() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||||
No shortened URLs registered. Create one to get started!
|
No shortened URLs registered. Create one to get started!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for url in urls %}
|
{% for url in urls %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="Short Link">
|
||||||
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
<div class="short-link-wrap">
|
||||||
<a href="/{{ url.code }}" target="_blank" style="color: var(--accent-color); font-weight: 700; text-decoration: none; font-family: monospace; font-size: 1rem;">
|
<div class="short-link-primary">
|
||||||
|
<a href="/{{ url.code }}" target="_blank" class="short-link-anchor">
|
||||||
{{ base_url.replace("https://", "").replace("http://", "") }}/{{ url.code }}
|
{{ base_url.replace("https://", "").replace("http://", "") }}/{{ url.code }}
|
||||||
</a>
|
</a>
|
||||||
{% if url.is_password_protected() %}
|
{% if url.is_password_protected() %}
|
||||||
<span title="Password Protected" style="color: #fbbf24; display: inline-flex; align-items: center;">
|
<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>
|
<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>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.25rem; font-weight: 500;">
|
{% if let Some(title) = url.title.as_deref() %}
|
||||||
{{ url.title.as_deref().unwrap_or("") }}
|
{% 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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
<td data-label="Destination">
|
||||||
<a href="{{ url.destination }}" target="_blank" style="color: var(--text-secondary); text-decoration: none; font-size: 0.85rem;" title="{{ url.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 }}
|
{{ url.destination }}
|
||||||
</a>
|
</a>
|
||||||
{% if let Some(desc) = url.description.as_deref() %}
|
{% if let Some(desc) = url.description.as_deref() %}
|
||||||
{% if !desc.is_empty() %}
|
{% if !desc.is_empty() %}
|
||||||
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem;">
|
<div class="destination-description" title="{{ desc }}">{{ desc }}</div>
|
||||||
{{ desc }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if let Some(expires_at) = url.expires_at.as_deref() %}
|
{% if let Some(expires_at) = url.expires_at.as_deref() %}
|
||||||
{% if !expires_at.is_empty() %}
|
{% 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;">
|
<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>
|
<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] }}
|
Expires: {{ expires_at[0..10] }} {{ expires_at[11..16] }}
|
||||||
{% if url.expired %}
|
{% if url.expired %}
|
||||||
@@ -176,7 +192,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if let Some(max) = url.max_access_count %}
|
{% 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;">
|
<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>
|
<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 }}
|
Clicks: {{ url.access_count }} / {{ max }}
|
||||||
{% if url.is_access_exhausted() %}
|
{% if url.is_access_exhausted() %}
|
||||||
@@ -185,43 +201,45 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if url.access_count > 0 %}
|
{% if url.access_count > 0 %}
|
||||||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;">
|
<div class="destination-meta neutral">
|
||||||
Clicks: {{ url.access_count }}
|
Clicks: {{ url.access_count }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{% let code = url.code.as_str() %}
|
{% let code = url.code.as_str() %}
|
||||||
{% include "components/qr_preview.html" %}
|
{% include "components/qr_preview.html" %}
|
||||||
<td>
|
<td data-label="Health" class="status-cell">
|
||||||
<span class="badge badge-{{ url.status }}">
|
<span class="badge badge-{{ url.status }}">
|
||||||
{{ url.status }}
|
{{ url.status }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Tags">
|
||||||
<div style="display: flex; gap: 0.25rem; flex-wrap: wrap; max-width: 150px;">
|
{% if url.tags.is_empty() %}
|
||||||
|
<span class="tags-empty">—</span>
|
||||||
|
{% else %}
|
||||||
|
<div class="tags-wrap">
|
||||||
{% for t in url.tags %}
|
{% 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);">
|
<a href="/admin/urls?tag={{ t }}" class="badge tag-badge">
|
||||||
{{ t }}
|
{{ t }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Actions" class="actions-cell">
|
||||||
|
<div class="action-stack">
|
||||||
<a href="/admin/analytics/url/{{ url.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
<a href="/admin/analytics/url/{{ url.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||||
📊 Analytics
|
📊 Analytics
|
||||||
</a>
|
</a>
|
||||||
</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?');">
|
<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 }}">
|
<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;">
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.6rem; font-size: 0.8rem;">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -230,8 +248,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Controls -->
|
<div class="pagination">
|
||||||
<div class="pagination" style="display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1rem; padding: 1rem; border-top: 1px solid var(--border-color);">
|
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a href="/admin/urls?page=1{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
<a href="/admin/urls?page=1{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
||||||
<a href="/admin/urls?page={{ current_page - 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
<a href="/admin/urls?page={{ current_page - 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
||||||
|
|||||||
+44
-34
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start;">
|
<div class="urls-dashboard">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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>
|
<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>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
<form action="/user/pages/create" method="POST">
|
<form action="/user/pages/create" method="POST">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
|
||||||
|
<div class="page-form-grid-2">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Page Title *</label>
|
<label for="title">Page Title *</label>
|
||||||
<input type="text" id="title" name="title" class="form-input" placeholder="e.g. Summer Campaign" required>
|
<input type="text" id="title" name="title" class="form-input" placeholder="e.g. Summer Campaign" required>
|
||||||
@@ -31,6 +32,8 @@
|
|||||||
<label for="slug">SEO Slug *</label>
|
<label for="slug">SEO Slug *</label>
|
||||||
<input type="text" id="slug" name="slug" class="form-input" placeholder="e.g. summer-promo" required pattern="[a-zA-Z0-9\-_]+" title="Only alphanumeric characters, dashes, and underscores allowed">
|
<input type="text" id="slug" name="slug" class="form-input" placeholder="e.g. summer-promo" required pattern="[a-zA-Z0-9\-_]+" title="Only alphanumeric characters, dashes, and underscores allowed">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-form-grid-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="code">Short Code (4-Hex, optional)</label>
|
<label for="code">Short Code (4-Hex, optional)</label>
|
||||||
<input type="text" id="code" name="code" class="form-input" placeholder="e.g. a1b2" pattern="[0-9a-fA-F]{4}" title="Must be exactly 4 hex characters">
|
<input type="text" id="code" name="code" class="form-input" placeholder="e.g. a1b2" pattern="[0-9a-fA-F]{4}" title="Must be exactly 4 hex characters">
|
||||||
@@ -47,16 +50,17 @@
|
|||||||
<option value="archived">Archived</option>
|
<option value="archived">Archived</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="html_content">HTML Content *</label>
|
<label for="html_content">HTML Content *</label>
|
||||||
<textarea id="html_content" name="html_content" class="form-input" style="height: 260px; font-family: monospace;" placeholder="<!DOCTYPE html> <html> <head> <title>My Page</title> </head> <body> <h1>Hello</h1> </body> </html>" required></textarea>
|
<textarea id="html_content" name="html_content" class="form-input page-html-editor" placeholder="<!DOCTYPE html> <html> <head> <title>My Page</title> </head> <body> <h1>Hello</h1> </body> </html>" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">Save Page</button>
|
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Save Page</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="padding: 0; overflow: hidden;">
|
<div class="card registered-links-shell" 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;">
|
<div class="registered-links-header">
|
||||||
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
My Landing Pages
|
My Landing Pages
|
||||||
@@ -64,69 +68,75 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table class="registered-pages-table">
|
||||||
|
<colgroup>
|
||||||
|
<col class="col-page-title">
|
||||||
|
<col class="col-page-paths">
|
||||||
|
<col class="col-page-status">
|
||||||
|
<col class="col-page-qr">
|
||||||
|
<col class="col-page-actions">
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Page Title</th>
|
<th>Page</th>
|
||||||
<th>Short Path</th>
|
<th>Paths</th>
|
||||||
<th>SEO Preview Path</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Analytics</th>
|
|
||||||
<th>QR Code</th>
|
<th>QR Code</th>
|
||||||
<th>Created</th>
|
<th>Actions</th>
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if pages.is_empty() %}
|
{% if pages.is_empty() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
<td colspan="5" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||||
No landing pages created yet.
|
No landing pages created yet.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for page in pages %}
|
{% for page in pages %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td data-label="Page">
|
||||||
<div style="font-weight: 700; color: var(--text-primary);">
|
<div class="page-title-block">
|
||||||
{{ page.title }}
|
<div class="page-title-heading">{{ page.title }}</div>
|
||||||
</div>
|
<div class="page-title-meta">UUID: {{ page.id[0..8] }}...</div>
|
||||||
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
|
<div class="page-title-meta">Created {{ page.created_at[0..10] }}</div>
|
||||||
UUID: {{ page.id[0..8] }}...
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Paths">
|
||||||
<a href="/p/{{ page.code }}" target="_blank" style="color: var(--accent-color); font-weight: 700; text-decoration: none; font-family: monospace;">
|
<div class="page-paths">
|
||||||
|
<div class="page-path-group">
|
||||||
|
<span class="page-path-caption">Short Path</span>
|
||||||
|
<a href="/p/{{ page.code }}" target="_blank" class="page-path-link">
|
||||||
/p/{{ page.code }}
|
/p/{{ page.code }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="page-path-group">
|
||||||
<a href="/p/{{ page.code }}/{{ page.slug }}" target="_blank" style="color: var(--text-secondary); text-decoration: none; font-size: 0.85rem; font-family: monospace;">
|
<span class="page-path-caption">SEO Preview Path</span>
|
||||||
|
<a href="/p/{{ page.code }}/{{ page.slug }}" target="_blank" class="page-path-link secondary">
|
||||||
/p/{{ page.code }}/{{ page.slug }}
|
/p/{{ page.code }}/{{ page.slug }}
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td data-label="Status" class="status-cell">
|
||||||
<span class="badge badge-{{ page.state }}">
|
<span class="badge badge-{{ page.state }}">
|
||||||
{{ page.state }}
|
{{ page.state }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
{% let code = page.code.as_str() %}
|
||||||
|
{% include "components/qr_preview.html" %}
|
||||||
|
<td data-label="Actions" class="actions-cell">
|
||||||
|
<div class="action-stack">
|
||||||
<a href="/user/analytics/page/{{ page.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
<a href="/user/analytics/page/{{ page.id }}" class="btn btn-secondary" style="padding: 0.4rem 0.6rem; font-size: 0.8rem; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||||
📊 Analytics
|
📊 Analytics
|
||||||
</a>
|
</a>
|
||||||
</td>
|
|
||||||
{% let code = page.code.as_str() %}
|
|
||||||
{% include "components/qr_preview.html" %}
|
|
||||||
<td style="font-size: 0.8rem; color: var(--text-secondary);">
|
|
||||||
{{ page.created_at[0..10] }}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<form action="/user/pages/delete/{{ page.id }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this page?');">
|
<form action="/user/pages/delete/{{ page.id }}" method="POST" onsubmit="return confirm('Are you sure you want to delete this page?');">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<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;">
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.6rem; font-size: 0.8rem;">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -135,7 +145,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" style="display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1rem; padding: 1rem; border-top: 1px solid var(--border-color);">
|
<div class="pagination">
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a href="/user/pages?page=1" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;"><< First</a>
|
<a href="/user/pages?page=1" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;"><< First</a>
|
||||||
<a href="/user/pages?page={{ current_page - 1 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">< Prev</a>
|
<a href="/user/pages?page={{ current_page - 1 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">< Prev</a>
|
||||||
|
|||||||
+114
-25
@@ -13,8 +13,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; align-items: start;">
|
<div class="urls-dashboard">
|
||||||
<div class="card">
|
<div class="card url-form-card">
|
||||||
<h3 style="font-size: 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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>
|
<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
|
Create a New URL
|
||||||
@@ -47,20 +47,34 @@
|
|||||||
<label for="tags">Tags (comma-separated, optional)</label>
|
<label for="tags">Tags (comma-separated, optional)</label>
|
||||||
<input type="text" id="tags" name="tags" class="form-input" placeholder="e.g. blog, tech, personal">
|
<input type="text" id="tags" name="tags" class="form-input" placeholder="e.g. blog, tech, personal">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn">Create Link</button>
|
<button type="submit" class="btn" style="width: 100%; margin-top: 0.5rem;">Create Link</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="padding: 0; overflow: hidden;">
|
<div class="card registered-links-shell" 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;">
|
<div class="registered-links-header">
|
||||||
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
<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>
|
<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
|
My Short Links
|
||||||
</h3>
|
</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>
|
||||||
|
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Short Link</th>
|
<th>Short Link</th>
|
||||||
@@ -68,36 +82,111 @@
|
|||||||
<th>QR Code</th>
|
<th>QR Code</th>
|
||||||
<th>Health</th>
|
<th>Health</th>
|
||||||
<th>Tags</th>
|
<th>Tags</th>
|
||||||
<th>Analytics</th>
|
<th>Actions</th>
|
||||||
<th>Created</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if urls.is_empty() %}
|
{% if urls.is_empty() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
||||||
No short URLs created yet.
|
No short URLs created yet.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for url in urls %}
|
{% for url in urls %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="/{{ url.code }}" target="_blank" style="color: var(--accent-color); text-decoration: none; font-family: monospace;">/{{ url.code }}</a></td>
|
<td data-label="Short Link">
|
||||||
<td style="max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ url.destination }}</td>
|
<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() %}
|
{% let code = url.code.as_str() %}
|
||||||
{% include "components/qr_preview.html" %}
|
{% include "components/qr_preview.html" %}
|
||||||
<td>{{ url.status }}</td>
|
<td data-label="Health" class="status-cell">
|
||||||
<td>{% if url.tags.is_empty() %}-{% else %}{{ url.tags.join(", ") }}{% endif %}</td>
|
<span class="badge badge-{{ url.status }}">
|
||||||
<td>
|
{{ url.status }}
|
||||||
<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;">View</a>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ url.created_at[0..10] }}</td>
|
<td data-label="Tags">
|
||||||
<td>
|
{% 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?');">
|
<form action="/user/urls/delete/{{ url.id }}" method="POST" onsubmit="return confirm('Delete this link?');">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<button type="submit" class="btn btn-secondary" style="padding: 0.4rem 0.75rem;">Delete</button>
|
<button type="submit" class="btn btn-danger" style="padding: 0.4rem 0.75rem;">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -106,10 +195,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" style="display: flex; justify-content: center; align-items: center; gap: 0.5rem; margin-top: 1rem; padding: 1rem; border-top: 1px solid var(--border-color);">
|
<div class="pagination">
|
||||||
{% if current_page > 1 %}
|
{% if current_page > 1 %}
|
||||||
<a href="/user/urls?page=1" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;"><< First</a>
|
<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 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">< Prev</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 %}
|
{% 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;"><< First</span>
|
||||||
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">< Prev</span>
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">< Prev</span>
|
||||||
@@ -119,13 +208,13 @@
|
|||||||
{% if self.is_current(p) %}
|
{% if self.is_current(p) %}
|
||||||
<span class="btn" style="padding: 0.4rem 0.8rem; font-weight: 700;">{{ p }}</span>
|
<span class="btn" style="padding: 0.4rem 0.8rem; font-weight: 700;">{{ p }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/user/urls?page={{ p }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">{{ p }}</a>
|
<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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if current_page < total_pages %}
|
{% if current_page < total_pages %}
|
||||||
<a href="/user/urls?page={{ current_page + 1 }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">Next ></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;">Next ></a>
|
||||||
<a href="/user/urls?page={{ total_pages }}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem;">Last >></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 %}
|
{% 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;">Next ></span>
|
||||||
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">Last >></span>
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem;">Last >></span>
|
||||||
|
|||||||
Reference in new issue
Block a user