259 lines
11 KiB
PHP
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>
|