- Media player. Multiple formats

This commit is contained in:
Ty Clifford
2026-06-03 01:28:23 -04:00
parent 1962390bb8
commit acfb4b3a05
16 changed files with 2346 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/layout.php';
admin_require_auth();
$db = get_db();
$error = '';
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$duration = (int)($_POST['duration'] ?? 0);
$sort_order = (int)($_POST['sort_order'] ?? 0);
$published = isset($_POST['published']) ? 1 : 0;
if (!$title) {
$error = 'Title is required.';
} else {
$slug = make_slug($title);
// Handle thumbnail upload
$thumbnail = '';
if (!empty($_FILES['thumbnail']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','gif'];
if (in_array($ext, $allowed)) {
$thumb_dir = MEDIA_DIR . 'thumbs/';
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
$fname = $slug . '_thumb_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
$thumbnail = 'thumbs/' . $fname;
}
} else {
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
}
}
if (!$error) {
// Insert video
$db->prepare("
INSERT INTO videos (title, description, slug, thumbnail, duration, sort_order, published)
VALUES (?, ?, ?, ?, ?, ?, ?)
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $published]);
$video_id = (int)$db->lastInsertId();
// Handle video source files
$source_labels = $_POST['source_label'] ?? [];
$source_quality = $_POST['source_quality'] ?? [];
$source_order = $_POST['source_sort'] ?? [];
$source_files = $_FILES['source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$inserted_sources = 0;
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
if (!empty($source_files['tmp_name'])) {
foreach ($source_files['tmp_name'] as $i => $tmp) {
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig_name = $source_files['name'][$i];
$ext = strtolower(pathinfo($orig_name, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_video)) continue;
$fname = $slug . '_' . ($i+1) . '_' . time() . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$label = trim($source_labels[$i] ?? '');
$quality = trim($source_quality[$i] ?? '');
$mime = mime_from_ext($fname);
$sort = (int)($source_order[$i] ?? $i);
$db->prepare("
INSERT INTO video_sources (video_id, label, file_path, mime_type, quality, sort_order)
VALUES (?,?,?,?,?,?)
")->execute([$video_id, $label, 'videos/' . $fname, $mime, $quality, $sort]);
$inserted_sources++;
}
}
}
header('Location: edit.php?id=' . $video_id . '&saved=1');
exit;
}
}
}
render_head('Add Video — Admin', '
.form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; }
@media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } }
.form-grid .span2 { grid-column:1/-1; }
.sources-section { display:flex; flex-direction:column; gap:.85rem; }
.source-row {
background:var(--surface-2); border:1px solid var(--border);
border-radius:var(--r); padding:1rem;
display:grid; grid-template-columns:1fr 1fr 1fr auto;
gap:.75rem; align-items:end;
}
@media(max-width:700px) { .source-row { grid-template-columns:1fr 1fr; } }
.add-source-btn { align-self:flex-start; }
.section-divider {
border:none; border-top:1px solid var(--border); margin:1.5rem 0 .5rem;
}
.section-heading {
font-family:"Share Tech Mono",monospace; font-size:.65rem;
letter-spacing:.15em; text-transform:uppercase; color:var(--text-3);
margin-bottom:.75rem;
}
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; }
.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); }
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
');
?>
<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 / add video</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 ($error): ?>
<div class="notice notice-error"><?= h($error) ?></div>
<?php endif; ?>
<div class="admin-layout">
<aside class="admin-nav">
<div class="admin-nav-label">Navigation</div>
<a href="index.php">
<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" class="active">
<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>
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;">
<div>
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">New Entry</p>
<h1 style="font-size:1.05rem;font-weight:700;color:var(--text);">Add Video</h1>
</div>
<a href="index.php" class="btn btn-secondary btn-sm">← Back</a>
</div>
<form method="post" action="add.php" enctype="multipart/form-data">
<!-- ── Video details ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Video Details</p>
<div class="form-grid">
<div class="form-group span2">
<label class="form-label" for="title">Title *</label>
<input class="form-input" type="text" id="title" name="title"
value="<?= h($_POST['title'] ?? '') ?>" required placeholder="My Awesome Video">
</div>
<div class="form-group span2">
<label class="form-label" for="description">Description</label>
<textarea class="form-textarea" id="description" name="description" rows="3"><?= h($_POST['description'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label" for="duration">Duration (seconds)</label>
<input class="form-input" type="number" id="duration" name="duration"
value="<?= h($_POST['duration'] ?? '0') ?>" min="0" placeholder="0">
<span class="hint">e.g. 3600 = 1 hour. Leave 0 if unknown.</span>
</div>
<div class="form-group">
<label class="form-label" for="sort_order">Sort Order</label>
<input class="form-input" type="number" id="sort_order" name="sort_order"
value="<?= h($_POST['sort_order'] ?? '0') ?>" min="0">
<span class="hint">Lower numbers appear first.</span>
</div>
<div class="form-group">
<label class="form-label" for="thumbnail">Thumbnail Image</label>
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
<span class="hint">JPG, PNG, WebP or GIF. Shown as 16:9 preview.</span>
</div>
<div class="form-group" style="justify-content:flex-end;padding-bottom:.2rem;">
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;">
<input type="checkbox" name="published" value="1" <?= isset($_POST['published']) || !isset($_POST['title']) ? 'checked' : '' ?>>
<span class="form-label" style="margin:0">Published</span>
</label>
<span class="hint">Uncheck to save as draft.</span>
</div>
</div>
</div>
<!-- ── Video sources ── -->
<div class="card">
<p class="section-heading">Video Sources (Multiple Formats)</p>
<p class="hint" style="margin-bottom:1rem;">
Upload the same video in multiple formats/qualities. The browser will automatically pick the best one it can play.
Accepted: <strong>MP4, WebM, OGV, MOV, MKV, AVI</strong>.
</p>
<div class="sources-section" id="sources-container">
<!-- Row template (JS clones this) -->
<div class="source-row" data-source-row="0">
<div class="form-group">
<label class="form-label">Video File</label>
<input class="form-input" type="file" name="source_file[]"
accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;">
</div>
<div class="form-group">
<label class="form-label">Quality Label</label>
<select class="form-select" name="source_quality[]">
<option value="1080p">1080p</option>
<option value="720p" selected>720p</option>
<option value="480p">480p</option>
<option value="360p">360p</option>
<option value="4K">4K</option>
<option value="">—</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Display Label</label>
<input class="form-input" type="text" name="source_label[]" placeholder="e.g. HD">
</div>
<div class="form-group">
<label class="form-label">Sort</label>
<input class="form-input" type="number" name="source_sort[]" value="0" min="0" style="width:70px;">
</div>
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm add-source-btn" id="add-source" style="margin-top:.75rem;align-self:flex-start;">
<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 Another Format
</button>
</div>
<div style="display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap;">
<button class="btn btn-primary" type="submit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Video
</button>
<a href="index.php" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<?php render_footer(); ?>
</main>
<script>
document.getElementById('add-source').addEventListener('click', function() {
var container = document.getElementById('sources-container');
var rows = container.querySelectorAll('[data-source-row]');
var idx = rows.length;
var first = rows[0].cloneNode(true);
first.setAttribute('data-source-row', idx);
// Clear file inputs (can't clone values for file inputs)
first.querySelectorAll('input[type="file"]').forEach(function(el){ el.value=''; });
first.querySelectorAll('input[type="text"]').forEach(function(el){ el.value=''; });
first.querySelectorAll('input[type="number"]').forEach(function(el){ el.value=idx; });
// Add remove button if not already there
if (!first.querySelector('.remove-row')) {
var btn = document.createElement('button');
btn.type='button';
btn.className='btn btn-danger btn-sm remove-row';
btn.style='align-self:flex-end';
btn.innerHTML='✕';
btn.onclick=function(){ this.closest('[data-source-row]').remove(); };
first.appendChild(btn);
}
container.appendChild(first);
});
</script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* auth.php — simple session-based admin auth
*/
require_once __DIR__ . '/../includes/db.php';
session_start();
define('ADMIN_SESSION_KEY', 'tc_admin_auth');
function admin_is_logged_in(): bool {
return !empty($_SESSION[ADMIN_SESSION_KEY]);
}
function admin_require_auth(): void {
if (!admin_is_logged_in()) {
header('Location: login.php');
exit;
}
}
function admin_login(string $password): bool {
$hash = setting('admin_password');
if (password_verify($password, $hash)) {
session_regenerate_id(true);
$_SESSION[ADMIN_SESSION_KEY] = true;
return true;
}
return false;
}
function admin_logout(): void {
$_SESSION[ADMIN_SESSION_KEY] = false;
session_destroy();
}
+381
View File
@@ -0,0 +1,381 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/layout.php';
admin_require_auth();
$db = get_db();
$id = (int)($_GET['id'] ?? 0);
$video = get_video_by_id($id);
if (!$video) {
header('Location: index.php');
exit;
}
$error = '';
$msg = '';
if (isset($_GET['saved'])) $msg = 'Video saved successfully.';
// Handle delete source
if (isset($_GET['del_src'])) {
$sid = (int)$_GET['del_src'];
$src = $db->prepare("SELECT * FROM video_sources WHERE id=? AND video_id=?");
$src->execute([$sid, $id]);
$s = $src->fetch();
if ($s) {
$f = MEDIA_DIR . $s['file_path'];
if (file_exists($f)) unlink($f);
$db->prepare("DELETE FROM video_sources WHERE id=?")->execute([$sid]);
$msg = 'Source file removed.';
}
$video = get_video_by_id($id); // refresh
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'save';
if ($action === 'save') {
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$duration = (int)($_POST['duration'] ?? 0);
$sort_order = (int)($_POST['sort_order'] ?? 0);
$published = isset($_POST['published']) ? 1 : 0;
if (!$title) {
$error = 'Title is required.';
} else {
$slug = make_slug($title, $id);
// Handle thumbnail replacement
$thumbnail = $video['thumbnail'];
if (!empty($_FILES['thumbnail']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['thumbnail']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','webp','gif'];
if (in_array($ext, $allowed)) {
$thumb_dir = MEDIA_DIR . 'thumbs/';
if (!is_dir($thumb_dir)) mkdir($thumb_dir, 0755, true);
// Remove old thumb
if ($thumbnail && file_exists(MEDIA_DIR . $thumbnail)) unlink(MEDIA_DIR . $thumbnail);
$fname = $slug . '_thumb_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['thumbnail']['tmp_name'], $thumb_dir . $fname)) {
$thumbnail = 'thumbs/' . $fname;
}
} else {
$error = 'Thumbnail must be JPG, PNG, WebP, or GIF.';
}
}
if (!$error) {
$db->prepare("
UPDATE videos SET title=?, description=?, slug=?, thumbnail=?,
duration=?, sort_order=?, published=?,
updated_at=datetime('now')
WHERE id=?
")->execute([$title, $description, $slug, $thumbnail, $duration, $sort_order, $published, $id]);
// Update existing sources sort/label/quality
$upd_labels = $_POST['src_label'] ?? [];
$upd_quality = $_POST['src_quality'] ?? [];
$upd_sort = $_POST['src_sort'] ?? [];
$upd_ids = $_POST['src_id'] ?? [];
foreach ($upd_ids as $i => $sid) {
$db->prepare("UPDATE video_sources SET label=?, quality=?, sort_order=? WHERE id=? AND video_id=?")
->execute([
$upd_labels[$i] ?? '',
$upd_quality[$i] ?? '',
(int)($upd_sort[$i] ?? $i),
(int)$sid, $id
]);
}
// Add new source files
$new_labels = $_POST['new_source_label'] ?? [];
$new_quality = $_POST['new_source_quality'] ?? [];
$new_sort = $_POST['new_source_sort'] ?? [];
$new_files = $_FILES['new_source_file'] ?? [];
$videos_dir = MEDIA_DIR . 'videos/';
if (!is_dir($videos_dir)) mkdir($videos_dir, 0755, true);
$allowed_video = ['mp4','webm','ogv','ogg','mov','mkv','avi'];
if (!empty($new_files['tmp_name'])) {
foreach ($new_files['tmp_name'] as $i => $tmp) {
if (empty($tmp) || !is_uploaded_file($tmp)) continue;
$orig = $new_files['name'][$i];
$ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_video)) continue;
$fname = $slug . '_' . time() . '_' . $i . '.' . $ext;
if (move_uploaded_file($tmp, $videos_dir . $fname)) {
$db->prepare("INSERT INTO video_sources (video_id,label,file_path,mime_type,quality,sort_order) VALUES (?,?,?,?,?,?)")
->execute([$id,
trim($new_labels[$i] ?? ''),
'videos/' . $fname,
mime_from_ext($fname),
trim($new_quality[$i] ?? '720p'),
(int)($new_sort[$i] ?? $i),
]);
}
}
}
header('Location: edit.php?id=' . $id . '&saved=1');
exit;
}
}
}
}
// Refresh
$video = get_video_by_id($id);
render_head('Edit Video — Admin', '
.form-grid { display:grid; grid-template-columns:1fr; gap:1.2rem; }
@media(min-width:700px) { .form-grid { grid-template-columns:1fr 1fr; } }
.form-grid .span2 { grid-column:1/-1; }
.section-heading { font-family:"Share Tech Mono",monospace; font-size:.65rem; letter-spacing:.15em; text-transform:uppercase; color:var(--text-3); margin-bottom:.75rem; }
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; }
.source-list { display:flex; flex-direction:column; gap:.75rem; }
.source-item {
background:var(--surface-2); border:1px solid var(--border);
border-radius:var(--r); padding:.85rem 1rem;
}
.source-item-header { display:flex; align-items:center; justify-content:space-between; gap:.75rem; margin-bottom:.75rem; flex-wrap:wrap; }
.source-item-file { font-family:"Share Tech Mono",monospace; font-size:.68rem; color:var(--text-2); word-break:break-all; }
.source-item-mime { font-family:"Share Tech Mono",monospace; font-size:.6rem; color:var(--text-3); }
.source-item-fields { display:grid; grid-template-columns:1fr 1fr 80px; gap:.6rem; }
@media(max-width:600px) { .source-item-fields { grid-template-columns:1fr 1fr; } }
.add-source-row { background:var(--surface-2); border:1px solid var(--border-2); border-radius:var(--r); padding:1rem; display:grid; grid-template-columns:1fr 1fr 1fr auto; gap:.75rem; align-items:end; }
@media(max-width:700px) { .add-source-row { grid-template-columns:1fr 1fr; } }
.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); }
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
.thumb-preview { width:100%; max-width:240px; aspect-ratio:16/9; object-fit:cover; border-radius:var(--r); border:1px solid var(--border); background:var(--surface-2); display:block; margin-bottom:.5rem; }
');
?>
<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 / edit video</div>
</div>
</div>
<nav class="topbar-social">
<a class="social-pill" href="../index.php?v=<?= urlencode($video['slug']) ?>" 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>
Preview
</a>
<a class="social-pill" href="logout.php">Logout</a>
</nav>
</header>
<?php if ($msg): ?>
<div class="notice notice-success"><?= h($msg) ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="notice notice-error"><?= h($error) ?></div>
<?php endif; ?>
<div class="admin-layout">
<aside class="admin-nav">
<div class="admin-nav-label">Navigation</div>
<a href="index.php">
<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>
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;flex-wrap:wrap;gap:.75rem;">
<div>
<p class="eyebrow eyebrow-purple" style="margin-bottom:.3rem">Editing</p>
<h1 style="font-size:1.05rem;font-weight:700;color:var(--text);"><?= h($video['title']) ?></h1>
</div>
<a href="index.php" class="btn btn-secondary btn-sm">← Back</a>
</div>
<form method="post" action="edit.php?id=<?= $id ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="save">
<!-- ── Details ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Video Details</p>
<div class="form-grid">
<div class="form-group span2">
<label class="form-label" for="title">Title *</label>
<input class="form-input" type="text" id="title" name="title"
value="<?= h($video['title']) ?>" required>
</div>
<div class="form-group span2">
<label class="form-label" for="description">Description</label>
<textarea class="form-textarea" id="description" name="description" rows="3"><?= h($video['description']) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">Slug (auto-generated)</label>
<input class="form-input" type="text" value="<?= h($video['slug']) ?>" disabled style="opacity:.5;">
</div>
<div class="form-group">
<label class="form-label" for="duration">Duration (seconds)</label>
<input class="form-input" type="number" id="duration" name="duration"
value="<?= (int)$video['duration'] ?>" min="0">
</div>
<div class="form-group">
<label class="form-label" for="sort_order">Sort Order</label>
<input class="form-input" type="number" id="sort_order" name="sort_order"
value="<?= (int)$video['sort_order'] ?>" min="0">
</div>
<div class="form-group">
<label class="form-label">Published</label>
<label style="display:flex;align-items:center;gap:.6rem;cursor:pointer;margin-top:.35rem;">
<input type="checkbox" name="published" value="1" <?= $video['published'] ? 'checked' : '' ?>>
<span style="font-size:.82rem;color:var(--text-2);">Published</span>
</label>
</div>
<div class="form-group span2">
<label class="form-label">Thumbnail</label>
<?php if ($video['thumbnail']): ?>
<img class="thumb-preview" src="<?= h('../' . MEDIA_URL . $video['thumbnail']) ?>" alt="Current thumbnail">
<span class="hint" style="display:block;margin-bottom:.5rem;">Current: <?= h($video['thumbnail']) ?></span>
<?php endif; ?>
<input class="form-input" type="file" id="thumbnail" name="thumbnail"
accept="image/jpeg,image/png,image/webp,image/gif" style="padding:.45rem .6rem;">
<span class="hint">Upload new image to replace current thumbnail.</span>
</div>
</div>
</div>
<!-- ── Existing sources ── -->
<?php if (!empty($video['sources'])): ?>
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Existing Sources</p>
<div class="source-list">
<?php foreach ($video['sources'] as $i => $s): ?>
<div class="source-item">
<div class="source-item-header">
<div>
<div class="source-item-file"><?= h($s['file_path']) ?></div>
<div class="source-item-mime"><?= h($s['mime_type']) ?></div>
</div>
<a href="edit.php?id=<?= $id ?>&del_src=<?= $s['id'] ?>"
class="btn btn-danger btn-sm"
onclick="return confirm('Remove this source file from disk?')">
<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="M9 6V4h6v2"/></svg>
Remove
</a>
</div>
<div class="source-item-fields">
<input type="hidden" name="src_id[]" value="<?= $s['id'] ?>">
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="src_quality[]">
<?php foreach (['1080p','720p','480p','360p','4K',''] as $q): ?>
<option value="<?= $q ?>" <?= $s['quality']===$q ? 'selected' : '' ?>><?= $q ?: '—' ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Label</label>
<input class="form-input" type="text" name="src_label[]" value="<?= h($s['label']) ?>" placeholder="e.g. HD">
</div>
<div class="form-group">
<label class="form-label">Sort</label>
<input class="form-input" type="number" name="src_sort[]" value="<?= (int)$s['sort_order'] ?>" min="0">
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- ── Add new sources ── -->
<div class="card" style="margin-bottom:1rem;">
<p class="section-heading">Add New Source Files</p>
<p class="hint" style="margin-bottom:.85rem;">Upload additional formats (MP4, WebM, OGV, MOV, MKV, AVI).</p>
<div class="source-list" id="new-sources-container">
<div class="add-source-row" data-new-row="0">
<div class="form-group">
<label class="form-label">Video File</label>
<input class="form-input" type="file" name="new_source_file[]"
accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.mov,.mkv,.avi"
style="padding:.45rem .6rem;">
</div>
<div class="form-group">
<label class="form-label">Quality</label>
<select class="form-select" name="new_source_quality[]">
<option value="1080p">1080p</option>
<option value="720p" selected>720p</option>
<option value="480p">480p</option>
<option value="360p">360p</option>
<option value="4K">4K</option>
<option value="">—</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Label</label>
<input class="form-input" type="text" name="new_source_label[]" placeholder="e.g. HD">
</div>
<div class="form-group">
<label class="form-label">Sort</label>
<input class="form-input" type="number" name="new_source_sort[]" value="0" min="0" style="width:70px;">
</div>
</div>
</div>
<button type="button" id="add-new-source" class="btn btn-secondary btn-sm" style="margin-top:.75rem;align-self:flex-start;">
<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 Another Format
</button>
</div>
<div style="display:flex;gap:.75rem;flex-wrap:wrap;">
<button class="btn btn-primary" type="submit">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Changes
</button>
<a href="index.php" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
</div>
<?php render_footer(); ?>
</main>
<script>
document.getElementById('add-new-source').addEventListener('click', function() {
var container = document.getElementById('new-sources-container');
var rows = container.querySelectorAll('[data-new-row]');
var idx = rows.length;
var clone = rows[0].cloneNode(true);
clone.setAttribute('data-new-row', idx);
clone.querySelectorAll('input[type="file"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="text"]').forEach(function(el){ el.value=''; });
clone.querySelectorAll('input[type="number"]').forEach(function(el){ el.value=idx; });
if (!clone.querySelector('.remove-row')) {
var btn = document.createElement('button');
btn.type='button'; btn.className='btn btn-danger btn-sm remove-row'; btn.style='align-self:flex-end'; btn.innerHTML='✕';
btn.onclick=function(){ this.closest('[data-new-row]').remove(); };
clone.appendChild(btn);
}
container.appendChild(clone);
});
</script>
</body>
</html>
+295
View File
@@ -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>
+81
View File
@@ -0,0 +1,81 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/layout.php';
if (admin_is_logged_in()) {
header('Location: index.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pw = $_POST['password'] ?? '';
if (admin_login($pw)) {
header('Location: index.php');
exit;
}
$error = 'Invalid password.';
}
render_head('Admin Login — TyClifford.com', '
.login-wrap {
display:flex; align-items:center; justify-content:center;
min-height:70vh;
}
.login-card {
background:var(--surface); border:1px solid var(--border);
border-radius:calc(var(--r)+4px); padding:2.5rem 2rem;
width:100%; max-width:380px;
display:flex; flex-direction:column; gap:1.5rem;
position:relative; overflow:hidden;
box-shadow:0 4px 60px rgba(0,0,0,.5);
}
.login-card::before {
content:""; position:absolute; top:0; left:0; right:0; height:2px;
background:linear-gradient(90deg,var(--pat),var(--purple),var(--blue),var(--green),var(--blue),var(--purple),var(--pat));
background-size:300% 100%; animation:shimmer 5s linear infinite;
}
.login-title { font-size:1.1rem; font-weight:700; color:var(--text); }
.login-sub { font-family:"Share Tech Mono",monospace; font-size:.63rem; color:var(--text-3); letter-spacing:.12em; text-transform:uppercase; margin-top:.15rem; }
');
?>
<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>
<a href="../index.php" class="btn btn-secondary btn-sm">← Back to site</a>
</header>
<div class="login-wrap">
<div class="login-card">
<div>
<div class="login-title">Admin Login</div>
<div class="login-sub">tyclifford.com / media / admin</div>
</div>
<?php if ($error): ?>
<div class="notice notice-error"><?= h($error) ?></div>
<?php endif; ?>
<form method="post" action="login.php">
<div class="form-group" style="margin-bottom:1rem">
<label class="form-label" for="password">Password</label>
<input class="form-input" type="password" id="password" name="password"
autofocus autocomplete="current-password" placeholder="••••••••">
</div>
<button class="btn btn-primary" type="submit" style="width:100%">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
Sign In
</button>
</form>
</div>
</div>
<?php render_footer(); ?>
</main>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/auth.php';
admin_logout();
header('Location: login.php');
exit;
+219
View File
@@ -0,0 +1,219 @@
<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/../includes/layout.php';
admin_require_auth();
$msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'settings';
if ($action === 'settings') {
$per_page = max(1, min(50, (int)($_POST['catalogue_per_page'] ?? 10)));
$site_title = trim($_POST['site_title'] ?? 'Ty Clifford');
$site_sub = trim($_POST['site_sub'] ?? 'tyclifford.com / media');
set_setting('catalogue_per_page', (string)$per_page);
set_setting('site_title', $site_title);
set_setting('site_sub', $site_sub);
$msg = 'Settings saved.';
}
if ($action === 'change_password') {
$current = $_POST['current_password'] ?? '';
$new1 = $_POST['new_password'] ?? '';
$new2 = $_POST['new_password2'] ?? '';
if (!password_verify($current, setting('admin_password'))) {
$msg = 'ERROR: Current password is incorrect.';
} elseif (strlen($new1) < 6) {
$msg = 'ERROR: New password must be at least 6 characters.';
} elseif ($new1 !== $new2) {
$msg = 'ERROR: New passwords do not match.';
} else {
set_setting('admin_password', password_hash($new1, PASSWORD_DEFAULT));
$msg = 'Password changed successfully.';
}
}
}
$per_page = setting('catalogue_per_page', '10');
$site_title = setting('site_title', 'Ty Clifford');
$site_sub = setting('site_sub', 'tyclifford.com / media');
// DB stats
$db = get_db();
$total_videos = $db->query("SELECT COUNT(*) FROM videos")->fetchColumn();
$total_sources = $db->query("SELECT COUNT(*) FROM video_sources")->fetchColumn();
$db_size = file_exists(DB_PATH) ? round(filesize(DB_PATH)/1024, 1) : 0;
$media_size = 0;
if (is_dir(MEDIA_DIR)) {
$iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(MEDIA_DIR, FilesystemIterator::SKIP_DOTS));
foreach ($iter as $f) $media_size += $f->getSize();
$media_size = round($media_size / 1048576, 1);
}
$msg_type = str_starts_with($msg, 'ERROR') ? 'error' : 'success';
render_head('Settings — Admin', '
.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); }
.nav-divider { border:none; border-top:1px solid var(--border); margin:.4rem 0; }
.form-grid { display:grid; grid-template-columns:1fr; gap:1rem; }
@media(min-width:600px) { .form-grid { grid-template-columns:1fr 1fr; } }
.section-heading { font-family:"Share Tech Mono",monospace; font-size:.65rem; letter-spacing:.15em; text-transform:uppercase; color:var(--text-3); margin-bottom:.75rem; }
.hint { font-size:.72rem; color:var(--text-3); margin-top:.2rem; }
.stat-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(130px,1fr)); gap:.75rem; }
.stat-tile { background:var(--surface-2); border:1px solid var(--border); border-radius:var(--r); padding:1rem; display:flex; flex-direction:column; gap:.3rem; }
.stat-val { font-family:"Share Tech Mono",monospace; font-size:1.4rem; color:var(--text); font-weight:700; }
.stat-label{ font-family:"Share Tech Mono",monospace; font-size:.6rem; letter-spacing:.1em; text-transform:uppercase; color:var(--text-3); }
');
?>
<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 / settings</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">Logout</a>
</nav>
</header>
<?php if ($msg): ?>
<div class="notice notice-<?= $msg_type ?>"><?= h($msg) ?></div>
<?php endif; ?>
<div class="admin-layout">
<aside class="admin-nav">
<div class="admin-nav-label">Navigation</div>
<a href="index.php">
<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" class="active">
<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>
<div style="display:flex;flex-direction:column;gap:1.25rem;">
<!-- Stats -->
<div class="card">
<p class="section-heading">Overview</p>
<div class="stat-grid">
<div class="stat-tile">
<span class="stat-val"><?= $total_videos ?></span>
<span class="stat-label">Videos</span>
</div>
<div class="stat-tile">
<span class="stat-val"><?= $total_sources ?></span>
<span class="stat-label">Source Files</span>
</div>
<div class="stat-tile">
<span class="stat-val"><?= $db_size ?> KB</span>
<span class="stat-label">Database</span>
</div>
<div class="stat-tile">
<span class="stat-val"><?= $media_size ?> MB</span>
<span class="stat-label">Media Storage</span>
</div>
</div>
</div>
<!-- General settings -->
<div class="card">
<p class="section-heading">General Settings</p>
<form method="post" action="settings.php">
<input type="hidden" name="action" value="settings">
<div class="form-grid" style="margin-bottom:1rem;">
<div class="form-group">
<label class="form-label" for="site_title">Site Title</label>
<input class="form-input" type="text" id="site_title" name="site_title"
value="<?= h($site_title) ?>">
</div>
<div class="form-group">
<label class="form-label" for="site_sub">Site Subtitle</label>
<input class="form-input" type="text" id="site_sub" name="site_sub"
value="<?= h($site_sub) ?>">
</div>
<div class="form-group">
<label class="form-label" for="catalogue_per_page">Videos per page</label>
<input class="form-input" type="number" id="catalogue_per_page"
name="catalogue_per_page" value="<?= h($per_page) ?>" min="1" max="50">
<span class="hint">How many thumbnails to show per page (150).</span>
</div>
</div>
<button class="btn btn-primary btn-sm" type="submit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Settings
</button>
</form>
</div>
<!-- Change password -->
<div class="card">
<p class="section-heading">Change Password</p>
<form method="post" action="settings.php">
<input type="hidden" name="action" value="change_password">
<div class="form-grid" style="margin-bottom:1rem;">
<div class="form-group">
<label class="form-label" for="current_password">Current Password</label>
<input class="form-input" type="password" id="current_password" name="current_password"
autocomplete="current-password">
</div>
<div></div>
<div class="form-group">
<label class="form-label" for="new_password">New Password</label>
<input class="form-input" type="password" id="new_password" name="new_password"
autocomplete="new-password" minlength="6">
<span class="hint">Minimum 6 characters.</span>
</div>
<div class="form-group">
<label class="form-label" for="new_password2">Confirm New Password</label>
<input class="form-input" type="password" id="new_password2" name="new_password2"
autocomplete="new-password">
</div>
</div>
<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"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg>
Change Password
</button>
</form>
</div>
<!-- File locations reference -->
<div class="card">
<p class="section-heading">File Locations</p>
<div style="display:flex;flex-direction:column;gap:.5rem;font-family:'Share Tech Mono',monospace;font-size:.7rem;color:var(--text-3);">
<div><span style="color:var(--text-2);">Database:</span> data/media.db</div>
<div><span style="color:var(--text-2);">Videos:</span> media/videos/</div>
<div><span style="color:var(--text-2);">Thumbnails:</span> media/thumbs/</div>
<div><span style="color:var(--text-2);">Default password:</span> <span style="color:var(--red);">admin</span> — change immediately!</div>
</div>
</div>
</div>
</div>
<?php render_footer(); ?>
</main>
</body>
</html>