add pages
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
{% extends "base.htm" %}
|
||||
|
||||
{% block title %}NanoShare - Files{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta name="description" content="NanoShare is a lightweight self-hosted file sharing service built on Python Quart.">
|
||||
<meta name="keywords" content="NanoShare, file sharing, self-hosted, Python Quart, open source">
|
||||
<meta name="robots" content="index, follow" />
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
/* ====== Design Tokens ====== */
|
||||
:root{
|
||||
--bg: #0f1221;
|
||||
--panel: #161a2f;
|
||||
--panel-2: #1b2140;
|
||||
--text: #e7eaf6;
|
||||
--muted: #a7b0d1;
|
||||
--accent: #7c9eff;
|
||||
--accent-2: #8ef6ff;
|
||||
--border: rgba(255,255,255,.08);
|
||||
--shadow: 0 10px 30px rgba(0,0,0,.25);
|
||||
--radius: 14px;
|
||||
}
|
||||
@media (prefers-color-scheme: light){
|
||||
:root{
|
||||
--bg: #f6f7fb;
|
||||
--panel: #ffffff;
|
||||
--panel-2: #f3f6ff;
|
||||
--text: #111321;
|
||||
--muted: #495273;
|
||||
--accent: #385cff;
|
||||
--accent-2: #00bcd4;
|
||||
--border: rgba(17,19,33,.08);
|
||||
--shadow: 0 12px 24px rgba(17,19,33,.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== Base ====== */
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
color:var(--text);
|
||||
background:
|
||||
radial-gradient(1200px 600px at 10% -10%, rgba(124,158,255,.18), transparent 60%),
|
||||
radial-gradient(900px 500px at 110% 10%, rgba(142,246,255,.18), transparent 60%),
|
||||
var(--bg);
|
||||
font: 500 system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji";
|
||||
}
|
||||
|
||||
/* ====== Layout ====== */
|
||||
.file-list{
|
||||
padding: clamp(16px, 3vw, 28px);
|
||||
}
|
||||
.card{
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: saturate(120%) blur(6px);
|
||||
-webkit-backdrop-filter: saturate(120%) blur(6px);
|
||||
}
|
||||
|
||||
/* ====== Header ====== */
|
||||
.page-title{
|
||||
margin: 0 0 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: .2px;
|
||||
font-size: clamp(22px, 2.4vw, 32px);
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.subtle{
|
||||
color: var(--muted);
|
||||
font-size: .95rem;
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
|
||||
/* ====== Table Wrapper ====== */
|
||||
.table-wrap{
|
||||
overflow: auto;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.table{
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.table thead th{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .3px;
|
||||
padding: 12px 14px;
|
||||
color: var(--muted);
|
||||
background: color-mix(in srgb, var(--panel) 86%, black 0%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.table tbody td{
|
||||
padding: 14px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table tbody tr{
|
||||
transition: background .25s ease, transform .06s ease;
|
||||
}
|
||||
.table tbody tr:hover{
|
||||
background: color-mix(in srgb, var(--panel-2) 88%, transparent);
|
||||
}
|
||||
.table .cell--name a{
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.table .cell--name a:hover{
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.table .cell--right{ text-align: right; }
|
||||
|
||||
/* Zebra for readability on dense lists */
|
||||
.table tbody tr:nth-child(2n){
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
}
|
||||
.table tbody tr:hover:nth-child(2n){
|
||||
background: color-mix(in srgb, var(--panel-2) 86%, transparent);
|
||||
}
|
||||
|
||||
/* ====== Badges (e.g., file size) ====== */
|
||||
.badge{
|
||||
display:inline-block;
|
||||
padding: .25rem .5rem;
|
||||
border-radius: 999px;
|
||||
font-size: .78rem;
|
||||
line-height: 1;
|
||||
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
color: var(--accent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--border));
|
||||
}
|
||||
|
||||
/* ====== Icon Buttons ====== */
|
||||
.actions{ display:flex; justify-content:flex-end; gap:8px; }
|
||||
.icon-btn{
|
||||
--btn-bg: color-mix(in srgb, var(--panel-2) 70%, transparent);
|
||||
--btn-br: color-mix(in srgb, var(--accent) 22%, var(--border));
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:.4ch;
|
||||
padding: .45rem .6rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--btn-br);
|
||||
background: var(--btn-bg);
|
||||
color: var(--text);
|
||||
font-size: .9rem;
|
||||
cursor: pointer;
|
||||
transition: transform .06s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease;
|
||||
box-shadow: 0 2px 0 rgba(0,0,0,.08);
|
||||
user-select: none;
|
||||
}
|
||||
.icon-btn:hover{
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--panel-2));
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,.18);
|
||||
}
|
||||
.icon-btn:active{ transform: translateY(1px) scale(.98); }
|
||||
.icon-btn[data-variant="ghost"]{
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.icon-btn[data-variant="primary"]{
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 22%, transparent), transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||
}
|
||||
|
||||
/* ====== Small screens: wrap long text and stack actions ====== */
|
||||
@media (max-width: 720px){
|
||||
.table tbody td{ white-space: normal; }
|
||||
.actions{ flex-wrap: wrap; }
|
||||
.badge{ margin-top: 4px; }
|
||||
}
|
||||
|
||||
/* ====== Utility ====== */
|
||||
.sr-only{
|
||||
position:absolute !important; width:1px; height:1px; padding:0; margin:-1px;
|
||||
overflow:hidden; clip:rect(0,0,0,0); border:0;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="file-list">
|
||||
<section class="card" style="padding: clamp(18px, 2.6vw, 28px);">
|
||||
<h2 class="page-title">Files</h2>
|
||||
<p class="subtle">Your uploaded files at a glance. Click a filename to open, or use the actions on the right.</p>
|
||||
|
||||
<div class="table-wrap" role="region" aria-label="Files table" tabindex="0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Filename</th>
|
||||
<th scope="col">Note</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Uploaded</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col" class="cell--right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td class="cell--name">
|
||||
<a href="{{ url_for('side_main.serve_file', file_id=file.file_id) }}">{{ file.file_name }}</a>
|
||||
</td>
|
||||
<td>{{ file.note }}</td>
|
||||
<td><span class="badge">{{ file.file_size }}</span></td>
|
||||
<td><time datetime="{{ file.uploaded_at }}">{{ file.uploaded_at }}</time></td>
|
||||
<td><time datetime="{{ file.expires_at }}">{{ file.expires_at }}</time></td>
|
||||
<td class="cell--right">
|
||||
<div class="actions">
|
||||
<button class="icon-btn" title="Info" data-action="info" aria-label="Info for {{ file.file_name }}">ℹ️ <span class="sr-only">Info</span></button>
|
||||
<button class="icon-btn" data-variant="primary" title="Edit" data-action="edit" aria-label="Edit {{ file.file_name }}">✏️ <span class="sr-only">Edit</span></button>
|
||||
<button class="icon-btn" data-variant="ghost" title="Copy link" data-action="copy" aria-label="Copy link to {{ file.file_name }}">📋 <span class="sr-only">Copy</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Minimal progressive enhancement for "Copy" action
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-action="copy"]');
|
||||
if(!btn) return;
|
||||
const row = btn.closest('tr');
|
||||
const link = row?.querySelector('.cell--name a')?.href;
|
||||
if(!link) return;
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = '✅';
|
||||
setTimeout(() => btn.innerHTML = original, 1200);
|
||||
}).catch(() => {
|
||||
alert('Could not copy link.');
|
||||
});
|
||||
});
|
||||
|
||||
// Optional: placeholder handlers for info/edit hooks
|
||||
document.addEventListener('click', (e) => {
|
||||
const info = e.target.closest('button[data-action="info"]');
|
||||
const edit = e.target.closest('button[data-action="edit"]');
|
||||
if(info){
|
||||
const name = info.closest('tr')?.querySelector('.cell--name a')?.textContent?.trim();
|
||||
// Hook into your modal/toast here
|
||||
console.log('Info for:', name);
|
||||
}
|
||||
if(edit){
|
||||
const id = edit.closest('tr')?.querySelector('.cell--name a')?.getAttribute('href');
|
||||
// Navigate or open editor route
|
||||
if(id) window.location.href = id + '/edit';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user