-
This commit is contained in:
+295
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
require_once __DIR__ . '/../includes/layout.php';
|
||||
admin_require_auth();
|
||||
|
||||
$db = get_db();
|
||||
|
||||
// Handle quick-actions
|
||||
$msg = ''; $msg_type = 'success';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'delete' && !empty($_POST['id'])) {
|
||||
$id = (int)$_POST['id'];
|
||||
$v = get_video_by_id($id);
|
||||
if ($v) {
|
||||
// Remove source files + thumbnail
|
||||
foreach ($v['sources'] as $s) {
|
||||
$f = MEDIA_DIR . $s['file_path'];
|
||||
if (file_exists($f)) unlink($f);
|
||||
}
|
||||
if ($v['thumbnail']) {
|
||||
$t = MEDIA_DIR . $v['thumbnail'];
|
||||
if (file_exists($t)) unlink($t);
|
||||
}
|
||||
$db->prepare("DELETE FROM videos WHERE id=?")->execute([$id]);
|
||||
$msg = 'Video deleted.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'toggle_published' && !empty($_POST['id'])) {
|
||||
$id = (int)$_POST['id'];
|
||||
$row = $db->prepare("SELECT published FROM videos WHERE id=?");
|
||||
$row->execute([$id]);
|
||||
$cur = $row->fetchColumn();
|
||||
$db->prepare("UPDATE videos SET published=? WHERE id=?")->execute([$cur ? 0 : 1, $id]);
|
||||
$msg = 'Video ' . ($cur ? 'unpublished' : 'published') . '.';
|
||||
}
|
||||
}
|
||||
|
||||
// Search + pagination
|
||||
$search = trim($_GET['q'] ?? '');
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
$per_page = (int)setting('catalogue_per_page', '10');
|
||||
|
||||
$where_sql = $search ? "WHERE v.title LIKE :q OR v.description LIKE :q" : '';
|
||||
$bind = $search ? [':q' => '%' . $search . '%'] : [];
|
||||
|
||||
$total_stmt = $db->prepare("SELECT COUNT(*) FROM videos v $where_sql");
|
||||
$total_stmt->execute($bind);
|
||||
$total = (int)$total_stmt->fetchColumn();
|
||||
$total_pages = max(1, (int)ceil($total / $per_page));
|
||||
$page = min($page, $total_pages);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$list_stmt = $db->prepare("
|
||||
SELECT v.id, v.title, v.slug, v.thumbnail, v.duration, v.published, v.sort_order,
|
||||
v.created_at,
|
||||
COUNT(vs.id) AS source_count
|
||||
FROM videos v
|
||||
LEFT JOIN video_sources vs ON vs.video_id = v.id
|
||||
$where_sql
|
||||
GROUP BY v.id
|
||||
ORDER BY v.sort_order ASC, v.id DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
");
|
||||
foreach ($bind as $k => $val) $list_stmt->bindValue($k, $val);
|
||||
$list_stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
|
||||
$list_stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$list_stmt->execute();
|
||||
$videos = $list_stmt->fetchAll();
|
||||
|
||||
$qs = fn($p) => '?' . http_build_query(array_filter(['q' => $search, 'page' => $p]));
|
||||
|
||||
render_head('Admin — TyClifford.com', '
|
||||
.admin-layout { display:grid; grid-template-columns:1fr; gap:1.5rem; }
|
||||
@media(min-width:900px) { .admin-layout { grid-template-columns:220px 1fr; } }
|
||||
|
||||
.admin-nav {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:calc(var(--r)+2px); padding:1.2rem;
|
||||
display:flex; flex-direction:column; gap:.4rem;
|
||||
align-self:start; position:sticky; top:1rem;
|
||||
}
|
||||
.admin-nav-label { font-family:"Share Tech Mono",monospace; font-size:.58rem; letter-spacing:.18em; text-transform:uppercase; color:var(--text-3); margin-bottom:.3rem; padding-left:.5rem; }
|
||||
.admin-nav a {
|
||||
display:flex; align-items:center; gap:.6rem;
|
||||
padding:.55rem .75rem; border-radius:var(--r);
|
||||
font-size:.82rem; color:var(--text-2); text-decoration:none;
|
||||
transition:background .15s, color .15s;
|
||||
}
|
||||
.admin-nav a:hover { background:rgba(255,255,255,.05); color:var(--text); }
|
||||
.admin-nav a.active { background:rgba(176,96,255,.12); color:var(--purple); }
|
||||
.admin-nav a svg { flex-shrink:0; }
|
||||
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
|
||||
|
||||
.admin-main { display:flex; flex-direction:column; gap:1rem; }
|
||||
.admin-toolbar { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; justify-content:space-between; }
|
||||
.search-form { display:flex; gap:.5rem; flex:1; min-width:0; max-width:360px; }
|
||||
.search-form input { flex:1; }
|
||||
|
||||
.video-table { width:100%; border-collapse:collapse; }
|
||||
.video-table th, .video-table td {
|
||||
padding:.65rem .85rem; text-align:left;
|
||||
border-bottom:1px solid var(--border);
|
||||
font-size:.82rem;
|
||||
}
|
||||
.video-table th {
|
||||
font-family:"Share Tech Mono",monospace; font-size:.62rem; letter-spacing:.12em;
|
||||
text-transform:uppercase; color:var(--text-3); background:var(--surface-2);
|
||||
}
|
||||
.video-table tr:hover td { background:rgba(255,255,255,.02); }
|
||||
.video-table .thumb-cell { width:80px; }
|
||||
.video-table .thumb-cell img { width:72px; height:40px; object-fit:cover; border-radius:4px; display:block; background:var(--surface-2); }
|
||||
.video-table .thumb-cell .no-thumb-sm { width:72px; height:40px; border-radius:4px; background:var(--surface-2); display:flex; align-items:center; justify-content:center; font-family:"Share Tech Mono",monospace; font-size:.45rem; color:var(--text-3); }
|
||||
.video-table .title-cell { max-width:250px; }
|
||||
.video-table .title-cell a { color:var(--text); text-decoration:none; font-weight:600; }
|
||||
.video-table .title-cell a:hover { color:var(--purple); }
|
||||
.video-table .title-cell .slug { font-family:"Share Tech Mono",monospace; font-size:.65rem; color:var(--text-3); display:block; margin-top:.15rem; }
|
||||
.video-table .actions-cell { white-space:nowrap; }
|
||||
.badge { display:inline-flex; align-items:center; padding:.18em .55em; border-radius:99px; font-family:"Share Tech Mono",monospace; font-size:.6rem; letter-spacing:.08em; }
|
||||
.badge-green { background:rgba(0,232,122,.12); color:var(--green); border:1px solid rgba(0,232,122,.25); }
|
||||
.badge-gray { background:rgba(255,255,255,.05); color:var(--text-3); border:1px solid var(--border-2); }
|
||||
|
||||
.table-wrap { overflow-x:auto; background:var(--surface); border:1px solid var(--border); border-radius:calc(var(--r)+2px); }
|
||||
|
||||
.admin-footer-bar { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; }
|
||||
.rec-count { font-family:"Share Tech Mono",monospace; font-size:.63rem; color:var(--text-3); letter-spacing:.06em; }
|
||||
');
|
||||
?>
|
||||
<main class="page" role="main">
|
||||
<header class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<div>
|
||||
<div class="brand-name">Ty Clifford</div>
|
||||
<div class="brand-sub">admin / dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="topbar-social">
|
||||
<a class="social-pill" href="../index.php" target="_blank">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
View Site
|
||||
</a>
|
||||
<a class="social-pill" href="logout.php">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16,17 21,12 16,7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
Logout
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<?php if ($msg): ?>
|
||||
<div class="notice notice-<?= $msg_type ?>"><?= h($msg) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="admin-layout">
|
||||
|
||||
<!-- Sidebar nav -->
|
||||
<aside class="admin-nav">
|
||||
<div class="admin-nav-label">Navigation</div>
|
||||
<a href="index.php" class="active">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
Videos
|
||||
</a>
|
||||
<a href="add.php">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
Add Video
|
||||
</a>
|
||||
<hr class="nav-divider">
|
||||
<a href="settings.php">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M4.93 4.93l1.41 1.41M19.07 19.07l-1.41-1.41M4.93 19.07l1.41-1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
|
||||
Settings
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="admin-main">
|
||||
|
||||
<div class="admin-toolbar">
|
||||
<form class="search-form" method="get" action="index.php">
|
||||
<input class="form-input" type="search" name="q"
|
||||
placeholder="Search videos…" value="<?= h($search) ?>">
|
||||
<button class="btn btn-secondary btn-sm" type="submit">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
Search
|
||||
</button>
|
||||
<?php if ($search): ?>
|
||||
<a href="index.php" class="btn btn-secondary btn-sm">✕ Clear</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<a href="add.php" class="btn btn-primary btn-sm">
|
||||
<svg width="13" height="13" 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>
|
||||
Add Video
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="video-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="thumb-cell">Thumb</th>
|
||||
<th class="title-cell">Title / Slug</th>
|
||||
<th>Duration</th>
|
||||
<th>Sources</th>
|
||||
<th>Status</th>
|
||||
<th>Added</th>
|
||||
<th class="actions-cell">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($videos)): ?>
|
||||
<tr><td colspan="7" style="text-align:center;padding:3rem;font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-3);">
|
||||
<?= $search ? 'No videos match "' . h($search) . '"' : 'No videos yet. <a href="add.php" style="color:var(--purple)">Add one!</a>' ?>
|
||||
</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($videos as $v): ?>
|
||||
<tr>
|
||||
<td class="thumb-cell">
|
||||
<?php if ($v['thumbnail']): ?>
|
||||
<img src="<?= h('../' . MEDIA_URL . $v['thumbnail']) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<div class="no-thumb-sm">NO IMG</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="title-cell">
|
||||
<a href="edit.php?id=<?= $v['id'] ?>"><?= h($v['title']) ?></a>
|
||||
<span class="slug">/<?= h($v['slug']) ?></span>
|
||||
</td>
|
||||
<td style="font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-3);">
|
||||
<?= $v['duration'] ? format_duration((int)$v['duration']) : '—' ?>
|
||||
</td>
|
||||
<td style="font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-2);">
|
||||
<?= $v['source_count'] ?> file<?= $v['source_count'] != 1 ? 's' : '' ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?= $v['published'] ? 'badge-green' : 'badge-gray' ?>">
|
||||
<?= $v['published'] ? 'Published' : 'Draft' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-family:'Share Tech Mono',monospace;font-size:.65rem;color:var(--text-3);">
|
||||
<?= substr($v['created_at'], 0, 10) ?>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<div style="display:flex;gap:.4rem;align-items:center;">
|
||||
<a href="edit.php?id=<?= $v['id'] ?>" class="btn btn-secondary btn-sm" title="Edit">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
Edit
|
||||
</a>
|
||||
<form method="post" style="display:inline" onsubmit="return confirm('Delete this video and all its files?')">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= $v['id'] ?>">
|
||||
<button class="btn btn-danger btn-sm" type="submit" title="Delete">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" style="display:inline">
|
||||
<input type="hidden" name="action" value="toggle_published">
|
||||
<input type="hidden" name="id" value="<?= $v['id'] ?>">
|
||||
<button class="btn btn-secondary btn-sm" type="submit" title="<?= $v['published'] ? 'Unpublish' : 'Publish' ?>">
|
||||
<?php if ($v['published']): ?>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
||||
<?php else: ?>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if ($total_pages > 1 || $total > 0): ?>
|
||||
<div class="admin-footer-bar">
|
||||
<span class="rec-count">
|
||||
<?= $total ?> video<?= $total != 1 ? 's' : '' ?>
|
||||
<?= $search ? '· filtered' : '' ?>
|
||||
· page <?= $page ?> of <?= $total_pages ?>
|
||||
</span>
|
||||
<?php
|
||||
render_pagination($page, $total_pages, 'index.php?' . ($search ? 'q=' . urlencode($search) . '&' : '') . 'page=%d');
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /admin-main -->
|
||||
</div><!-- /admin-layout -->
|
||||
|
||||
<?php render_footer(); ?>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user