Files
aj-portfolio/public/admin/dashboard.php
2025-12-23 13:18:58 +02:00

259 lines
11 KiB
PHP

<?php
declare(strict_types=1);
require __DIR__ . '/../../includes/bootstrap.php';
require_admin_login();
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32));
$f = flash_get();
$projectsCount = 0;
$skillsCount = 0;
$contactCounts = ['total' => 0, 'new' => 0];
$recentContacts = [];
try { $projectsCount = (int)pdo()->query("SELECT COUNT(*) FROM projects")->fetchColumn(); } catch (Throwable $e) {}
try { $skillsCount = (int)pdo()->query("SELECT COUNT(*) FROM skills")->fetchColumn(); } catch (Throwable $e) {}
try {
pdo()->exec("
CREATE TABLE IF NOT EXISTS contact_requests (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
email VARCHAR(120) NOT NULL,
message TEXT NOT NULL,
status ENUM('new','read','archived') NOT NULL DEFAULT 'new',
created_at DATETIME NOT NULL,
ip VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
$contactCounts['total'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests")->fetchColumn();
$contactCounts['new'] = (int)pdo()->query("SELECT COUNT(*) FROM contact_requests WHERE status='new'")->fetchColumn();
$recentContacts = pdo()->query("SELECT id, name, email, message, status, created_at FROM contact_requests ORDER BY created_at DESC, id DESC LIMIT 5")->fetchAll() ?: [];
} catch (Throwable $e) {}
$hasRefresh = false;
$hasAccess = false;
try {
$row = pdo()->query("SELECT refresh_token, access_token, access_expires FROM spotify_tokens WHERE id=1")->fetch();
if ($row) {
$hasRefresh = !empty($row['refresh_token']);
$hasAccess = !empty($row['access_token']) && (int)($row['access_expires'] ?? 0) > (time() + 30);
}
} catch (Throwable $e) {}
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet">
<link rel="stylesheet" href="/public/css/admin.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: { extend: { colors: { ink:"#07060a", neon:"#a78bfa" } } }
}
</script>
<style>
.eq span{height:6px}
.sp-playing .eq span{animation:eq .8s infinite ease-in-out alternate}
.sp-playing .eq span:nth-child(2){animation-delay:.15s}
.sp-playing .eq span:nth-child(3){animation-delay:.3s}
.sp-playing .eq span:nth-child(4){animation-delay:.45s}
@keyframes eq { from{height:4px} to{height:14px} }
.sp-playing #spImg{animation:spin 4s linear infinite}
@keyframes spin { from{transform:rotate(0)} to{transform:rotate(360deg)} }
</style>
</head>
<body class="min-vh-100 bg-ink text-white">
<div class="max-w-6xl mx-auto px-4 py-6">
<div class="flex items-center justify-between gap-3 mb-4">
<div>
<h1 class="text-2xl font-extrabold tracking-tight">Dashboard</h1>
</div>
<div class="flex items-center gap-2">
<a class="btn btn-outline-light rounded-xl" href="<?= htmlspecialchars(url_path('/')) ?>">Home</a>
<a class="btn btn-outline-light rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/logout.php')) ?>">Logout</a>
</div>
</div>
<?php if ($f): ?>
<div class="alert alert-<?= htmlspecialchars($f['type']) ?> mb-4"><?= htmlspecialchars($f['msg']) ?></div>
<?php endif; ?>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
<!-- Projects -->
<div class="rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-4">
<div class="flex items-center justify-between">
<div class="w-11 h-11 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<i class="ri-layout-4-line"></i>
</div>
<div class="text-right">
<div class="text-white/50 text-sm">Projects</div>
<div class="text-3xl font-extrabold tracking-tight"><?= $projectsCount ?></div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-white/10 flex gap-2">
<a class="btn btn-primary w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/projects.php?new=1')) ?>">Add</a>
<a class="btn btn-outline-light w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/projects.php')) ?>">Manage</a>
</div>
</div>
<!-- Skills -->
<div class="rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-4">
<div class="flex items-center justify-between">
<div class="w-11 h-11 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<i class="ri-stack-fill"></i>
</div>
<div class="text-right">
<div class="text-white/50 text-sm">Tech Stack</div>
<div class="text-3xl font-extrabold tracking-tight"><?= $skillsCount ?></div>
</div>
</div>
<div class="text-white/50 text-sm mt-2">Edit languages and frameworks for your homepage.</div>
<div class="mt-4 pt-4 border-t border-white/10 flex gap-2">
<a class="btn btn-primary w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/skills.php?new=1')) ?>">Add</a>
<a class="btn btn-outline-light w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/skills.php')) ?>">Adjust</a>
</div>
</div>
<!-- Contacts -->
<div class="rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-4">
<div class="flex items-center justify-between">
<div class="w-11 h-11 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<i class="ri-mail-send-line"></i>
</div>
<div class="text-right">
<div class="text-white/50 text-sm">Contacts</div>
<div class="text-3xl font-extrabold tracking-tight"><?= (int)$contactCounts['total'] ?></div>
<div class="text-xs text-white/60"><?= (int)$contactCounts['new'] ?> new</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-white/10 flex gap-2">
<a class="btn btn-primary w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/contacts.php')) ?>">Inbox</a>
<a class="btn btn-outline-light w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/contacts.php?status=new')) ?>">New</a>
</div>
</div>
<!-- Spotify -->
<div id="spAdminCard" class="rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-4">
<div class="flex items-center justify-between">
<div class="w-11 h-11 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center">
<i class="ri-spotify-line"></i>
</div>
<div class="text-right">
<div class="text-white/50 text-sm">Spotify</div>
<div class="font-semibold">
<?= $hasRefresh ? 'Refresh token set' : ($hasAccess ? 'Access token set' : 'Not configured') ?>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-white/10">
<div class="flex items-center justify-between">
<div class="text-white/50 text-sm" id="spMode">Offline</div>
<div class="eq flex items-end gap-[3px]" aria-hidden="true">
<span class="w-[3px] rounded bg-green-500 opacity-90"></span>
<span class="w-[3px] rounded bg-green-500 opacity-90"></span>
<span class="w-[3px] rounded bg-green-500 opacity-90"></span>
<span class="w-[3px] rounded bg-green-500 opacity-90"></span>
</div>
</div>
<a id="spA" href="#" target="_blank" class="mt-3 flex items-center gap-3 no-underline text-white">
<img id="spImg" class="w-14 h-14 rounded-2xl object-cover bg-white/5 border border-white/10" alt="Album art">
<div class="min-w-0">
<div id="spT" class="font-semibold truncate">-</div>
<div id="spR" class="text-white/60 text-sm truncate">-</div>
</div>
</a>
<div class="text-white/50 text-sm mt-2">
Shows <b>Now Playing</b> or <b>Last Played</b>. Temporary token expires; refresh token is best.
</div>
<div class="flex gap-2 mt-3">
<a class="btn btn-outline-light w-100 rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/spotify_token.php')) ?>">Set Token</a>
<button class="btn btn-primary w-100 rounded-xl" id="spRefreshBtn" type="button">Refresh</button>
</div>
</div>
</div>
</div>
<?php if ($recentContacts): ?>
<div class="mt-6 card-glass rounded-2xl p-4">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-2">
<div>
<div class="text-white-50 text-sm">Inbox preview</div>
<div class="font-bold">Latest contact requests</div>
</div>
<a class="btn btn-outline-light btn-sm rounded-xl" href="<?= htmlspecialchars(url_path('/public/admin/contacts.php')) ?>">View all</a>
</div>
<div class="list-group list-group-flush">
<?php foreach ($recentContacts as $c): ?>
<div class="list-group-item bg-transparent text-white px-0 d-flex justify-content-between align-items-start">
<div>
<div class="fw-semibold"><?= htmlspecialchars((string)$c['name']) ?></div>
<div class="text-white-50 small"><?= htmlspecialchars(mb_strimwidth((string)$c['message'], 0, 120, '...')) ?></div>
</div>
<div class="text-end">
<span class="badge text-bg-light"><?= htmlspecialchars(ucfirst((string)$c['status'])) ?></span>
<div class="text-white-50 small"><?= htmlspecialchars((string)$c['created_at']) ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function($){
function render(res){
const $card = $("#spAdminCard");
$card.removeClass("sp-playing");
if(!res || !res.ok){
$("#spMode").text("Offline");
$("#spT").text("Not configured");
$("#spR").text("Set token to enable");
$("#spA").attr("href","#");
$("#spImg").removeAttr("src");
return;
}
const mode = res.mode || "offline";
const t = res.track || {};
$("#spMode").text(mode === "playing" ? "Now Playing" : (mode === "recent" ? "Last Played" : "Offline"));
$("#spT").text(t.title || "-");
$("#spR").text(t.artist || "-");
$("#spA").attr("href", t.url || "#");
if(t.art) $("#spImg").attr("src", t.art);
if(mode === "playing") $card.addClass("sp-playing");
}
function load(){
$.ajax({ url:"/api/spotify.php", method:"GET", dataType:"json", cache:false, timeout:8000 })
.done(render)
.fail(()=>render(null));
}
$(function(){
$("#spRefreshBtn").on("click", load);
load();
});
})(jQuery);
</script>
</body>
</html>