873 lines
21 KiB
PHP
873 lines
21 KiB
PHP
<?php
|
|
/*
|
|
Minimal DB schema for Mediakor posts:
|
|
|
|
CREATE TABLE posts (
|
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
title VARCHAR(255) NOT NULL,
|
|
meta VARCHAR(255) DEFAULT NULL,
|
|
body TEXT NOT NULL,
|
|
is_published TINYINT(1) NOT NULL DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
|
|
Insert example:
|
|
|
|
INSERT INTO posts (title, meta, body) VALUES
|
|
('Welcome to Mediakor', 'System Log • Today',
|
|
'Mediakor is your central hub for projects, servers, and experiments. Use the nav on the left to explore each sector of the network.');
|
|
*/
|
|
|
|
// --- DB CONFIG ---
|
|
// You can also set these as environment variables in your Docker stack
|
|
$dbHost = $_ENV['DB_HOST'] ?? 'mariadb';
|
|
$dbName = $_ENV['DB_NAME'] ?? 'appdb';
|
|
$dbUser = $_ENV['DB_USER'] ?? 'appuser';
|
|
$dbPass = $_ENV['DB_PASS'] ?? 'apppass';
|
|
|
|
$posts = [];
|
|
$dbError = null;
|
|
|
|
try {
|
|
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
|
|
$pdo = new PDO($dsn, $dbUser, $dbPass, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]);
|
|
|
|
$stmt = $pdo->query("
|
|
SELECT id, title, meta, body, created_at
|
|
FROM posts
|
|
WHERE is_published = 1
|
|
ORDER BY created_at DESC, id DESC
|
|
");
|
|
$posts = $stmt->fetchAll();
|
|
} catch (PDOException $e) {
|
|
// Fallback: static posts if DB connection fails
|
|
$dbError = 'DB_connect_failed';
|
|
$posts = [
|
|
[
|
|
'id' => 0,
|
|
'title' => 'Welcome to Mediakor',
|
|
'meta' => 'System Log • Fallback',
|
|
'body' => 'Database connection failed, so this is the static fallback feed. Once your DB is configured, posts will be served from the posts table.',
|
|
'created_at' => null,
|
|
],
|
|
[
|
|
'id' => 0,
|
|
'title' => 'Configure Database',
|
|
'meta' => 'Setup • Guide',
|
|
'body' => 'Edit the DB settings at the top of index.php and create the posts table using the SQL snippet in the comment.',
|
|
'created_at' => null,
|
|
],
|
|
];
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Mediakor — Command Hub</title>
|
|
|
|
<style>
|
|
:root{
|
|
--bg:#050608;
|
|
--bg-alt:#0b0c10;
|
|
--panel:#101218;
|
|
--panel-soft:#161923;
|
|
--accent:#ff8c32;
|
|
--accent-soft:rgba(255,140,50,0.18);
|
|
--accent-border:rgba(255,140,50,0.5);
|
|
--text:#f3f4f6;
|
|
--muted:#a1a1aa;
|
|
--divider:#23232e;
|
|
--shadow:0 0 40px rgba(0,0,0,0.65);
|
|
}
|
|
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
|
|
body{
|
|
min-height:100vh;
|
|
background:
|
|
radial-gradient(800px 600px at 75% 0%, #17141f 0, transparent 60%),
|
|
radial-gradient(900px 700px at 0% 100%, #141019 0, transparent 60%),
|
|
linear-gradient(160deg,#050608,#050608 40%,#080910 100%);
|
|
color:var(--text);
|
|
font:500 15px/1.6 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
display:flex;
|
|
flex-direction:column;
|
|
overflow-x:hidden;
|
|
position:relative;
|
|
}
|
|
|
|
/* Twinkling starfields (two layers, desynced animation) */
|
|
body::before,
|
|
body::after{
|
|
content:"";
|
|
position:fixed;
|
|
inset:-200px;
|
|
pointer-events:none;
|
|
z-index:-1;
|
|
background-repeat:no-repeat;
|
|
}
|
|
|
|
body::before{
|
|
background-image:
|
|
radial-gradient(1px 1px at 10% 20%,rgba(255,255,255,.40) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 40% 80%,rgba(255,255,255,.25) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 80% 30%,rgba(255,255,255,.30) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 20% 60%,rgba(255,255,255,.22) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 65% 50%,rgba(255,255,255,.20) 0,transparent 60%);
|
|
animation:twinkleLayer1 14s linear infinite alternate;
|
|
}
|
|
|
|
body::after{
|
|
background-image:
|
|
radial-gradient(1px 1px at 15% 75%,rgba(255,255,255,.25) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 70% 15%,rgba(255,255,255,.30) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 90% 60%,rgba(255,255,255,.22) 0,transparent 60%),
|
|
radial-gradient(1px 1px at 35% 35%,rgba(255,255,255,.27) 0,transparent 60%);
|
|
animation:twinkleLayer2 18s linear infinite alternate;
|
|
}
|
|
|
|
@keyframes twinkleLayer1{
|
|
0% {opacity:0.25; transform:translate3d(0,0,0);}
|
|
50% {opacity:0.55; transform:translate3d(10px,-8px,0);}
|
|
100%{opacity:0.20; transform:translate3d(-8px,6px,0);}
|
|
}
|
|
|
|
@keyframes twinkleLayer2{
|
|
0% {opacity:0.18; transform:translate3d(0,0,0);}
|
|
50% {opacity:0.50; transform:translate3d(-12px,10px,0);}
|
|
100%{opacity:0.22; transform:translate3d(8px,-6px,0);}
|
|
}
|
|
|
|
/* Header */
|
|
.mk-header{
|
|
padding:14px 22px 10px;
|
|
border-bottom:1px solid var(--divider);
|
|
background:linear-gradient(90deg,rgba(0,0,0,.75),rgba(5,5,10,.4));
|
|
backdrop-filter:blur(18px);
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:space-between;
|
|
position:sticky;
|
|
top:0;
|
|
z-index:20;
|
|
}
|
|
|
|
.mk-logo{
|
|
display:flex;
|
|
align-items:baseline;
|
|
gap:8px;
|
|
}
|
|
|
|
.mk-logo-main{
|
|
font-size:26px;
|
|
letter-spacing:0.16em;
|
|
text-transform:uppercase;
|
|
font-weight:700;
|
|
color:var(--accent);
|
|
}
|
|
|
|
.mk-logo-sub{
|
|
font-size:12px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.24em;
|
|
color:var(--muted);
|
|
}
|
|
|
|
.mk-header-pill{
|
|
padding:4px 10px;
|
|
border-radius:999px;
|
|
border:1px solid var(--accent-border);
|
|
background:radial-gradient(circle at 0 0,rgba(255,140,50,.25),transparent 55%);
|
|
font-size:11px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.18em;
|
|
color:var(--muted);
|
|
display:flex;
|
|
align-items:center;
|
|
gap:8px;
|
|
}
|
|
|
|
.mk-pulse{
|
|
width:7px;
|
|
height:7px;
|
|
border-radius:50%;
|
|
background:var(--accent);
|
|
box-shadow:0 0 8px rgba(255,140,50,.9);
|
|
animation:mk-pulse 1.4s infinite;
|
|
}
|
|
|
|
@keyframes mk-pulse{
|
|
0%{transform:scale(1);opacity:1}
|
|
50%{transform:scale(1.6);opacity:.25}
|
|
100%{transform:scale(1);opacity:1}
|
|
}
|
|
|
|
/* Layout */
|
|
.mk-shell{
|
|
flex:1;
|
|
display:flex;
|
|
max-width:1400px;
|
|
width:100%;
|
|
margin:16px auto 24px;
|
|
padding:0 16px;
|
|
gap:18px;
|
|
}
|
|
|
|
/* Sidebar */
|
|
.mk-sidebar{
|
|
width:240px;
|
|
min-width:220px;
|
|
max-height:calc(100vh - 96px);
|
|
background:linear-gradient(150deg,var(--panel),#080910);
|
|
border-radius:18px;
|
|
border:1px solid var(--divider);
|
|
box-shadow:var(--shadow);
|
|
padding:14px 12px 12px;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:10px;
|
|
position:sticky;
|
|
top:80px;
|
|
}
|
|
|
|
.mk-sidebar-title{
|
|
font-size:11px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.14em;
|
|
color:var(--muted);
|
|
padding:0 8px;
|
|
}
|
|
|
|
.mk-nav{
|
|
list-style:none;
|
|
margin-top:4px;
|
|
}
|
|
|
|
.mk-nav-item{
|
|
margin-bottom:4px;
|
|
}
|
|
|
|
.mk-nav-link{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:10px;
|
|
padding:7px 9px;
|
|
border-radius:12px;
|
|
color:var(--muted);
|
|
text-decoration:none;
|
|
font-size:13px;
|
|
border:1px solid transparent;
|
|
background:transparent;
|
|
transition:.16s ease;
|
|
cursor:pointer;
|
|
}
|
|
|
|
.mk-nav-link span.mk-dot{
|
|
width:7px;
|
|
height:7px;
|
|
border-radius:50%;
|
|
background:var(--accent-soft);
|
|
border:1px solid var(--accent-border);
|
|
}
|
|
|
|
.mk-nav-link small{
|
|
text-transform:uppercase;
|
|
letter-spacing:0.16em;
|
|
font-size:10px;
|
|
color:#71717a;
|
|
}
|
|
|
|
.mk-nav-link strong{
|
|
font-weight:600;
|
|
color:var(--text);
|
|
}
|
|
|
|
.mk-nav-link:hover{
|
|
background:radial-gradient(circle at 0 0,var(--accent-soft),transparent 55%);
|
|
border-color:var(--accent-border);
|
|
color:var(--text);
|
|
transform:translateX(2px);
|
|
}
|
|
|
|
.mk-nav-link.active{
|
|
background:linear-gradient(120deg,rgba(255,140,50,.25),rgba(0,0,0,.1));
|
|
border-color:var(--accent-border);
|
|
color:var(--text);
|
|
}
|
|
|
|
.mk-sidebar-footer{
|
|
margin-top:auto;
|
|
padding-top:8px;
|
|
border-top:1px dashed var(--divider);
|
|
font-size:11px;
|
|
color:var(--muted);
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:4px;
|
|
padding-inline:8px;
|
|
}
|
|
|
|
.mk-tag{
|
|
display:inline-flex;
|
|
align-items:center;
|
|
gap:6px;
|
|
padding:3px 8px;
|
|
border-radius:999px;
|
|
border:1px solid var(--accent-border);
|
|
background:rgba(0,0,0,.6);
|
|
font-size:10px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.16em;
|
|
}
|
|
|
|
/* Main content */
|
|
.mk-main{
|
|
flex:1;
|
|
min-width:0;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:12px;
|
|
}
|
|
|
|
.mk-main-header{
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:flex-end;
|
|
padding:6px 4px 4px;
|
|
}
|
|
|
|
.mk-main-title{
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:2px;
|
|
}
|
|
|
|
.mk-main-title h2{
|
|
font-size:18px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.18em;
|
|
color:var(--text);
|
|
}
|
|
|
|
.mk-main-title span{
|
|
font-size:12px;
|
|
color:var(--muted);
|
|
}
|
|
|
|
.mk-main-meta{
|
|
font-size:11px;
|
|
color:var(--muted);
|
|
text-transform:uppercase;
|
|
letter-spacing:0.14em;
|
|
}
|
|
|
|
/* Posts */
|
|
.mk-post-list{
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:12px;
|
|
}
|
|
|
|
.mk-post{
|
|
background:linear-gradient(145deg,var(--panel),var(--panel-soft));
|
|
border-radius:18px;
|
|
border:1px solid var(--divider);
|
|
box-shadow:var(--shadow);
|
|
padding:14px 16px 13px;
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
|
|
.mk-post::before{
|
|
content:"";
|
|
position:absolute;
|
|
inset:0;
|
|
background:radial-gradient(600px 220px at 105% -10%,rgba(255,140,50,.18),transparent 60%);
|
|
opacity:0.85;
|
|
pointer-events:none;
|
|
}
|
|
|
|
.mk-post-inner{
|
|
position:relative;
|
|
z-index:1;
|
|
}
|
|
|
|
.mk-post-header{
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:flex-start;
|
|
gap:10px;
|
|
margin-bottom:6px;
|
|
}
|
|
|
|
.mk-post-title{
|
|
font-size:17px;
|
|
font-weight:600;
|
|
letter-spacing:0.04em;
|
|
text-transform:uppercase;
|
|
}
|
|
|
|
.mk-post-meta{
|
|
font-size:11px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.18em;
|
|
color:var(--muted);
|
|
white-space:nowrap;
|
|
}
|
|
|
|
.mk-post-body{
|
|
font-size:14px;
|
|
color:#e4e4ea;
|
|
margin-top:2px;
|
|
}
|
|
|
|
.mk-post-footer{
|
|
margin-top:10px;
|
|
font-size:11px;
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:center;
|
|
color:var(--muted);
|
|
}
|
|
|
|
.mk-pill{
|
|
padding:3px 8px;
|
|
border-radius:999px;
|
|
border:1px solid var(--accent-border);
|
|
background:rgba(0,0,0,.5);
|
|
font-size:10px;
|
|
text-transform:uppercase;
|
|
letter-spacing:0.16em;
|
|
}
|
|
|
|
.mk-post-actions{
|
|
display:flex;
|
|
gap:8px;
|
|
font-size:11px;
|
|
}
|
|
|
|
.mk-post-actions button{
|
|
border:none;
|
|
outline:none;
|
|
background:transparent;
|
|
color:var(--muted);
|
|
cursor:pointer;
|
|
padding:0;
|
|
font:inherit;
|
|
opacity:.8;
|
|
}
|
|
|
|
.mk-post-actions button:hover{
|
|
opacity:1;
|
|
color:var(--accent);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width:960px){
|
|
.mk-shell{
|
|
flex-direction:column;
|
|
max-width:100%;
|
|
}
|
|
.mk-sidebar{
|
|
position:static;
|
|
width:auto;
|
|
flex-direction:row;
|
|
align-items:flex-start;
|
|
max-height:none;
|
|
overflow-x:auto;
|
|
padding:10px;
|
|
}
|
|
.mk-sidebar-title,
|
|
.mk-sidebar-footer{
|
|
display:none;
|
|
}
|
|
.mk-nav{
|
|
display:flex;
|
|
gap:4px;
|
|
margin-top:0;
|
|
}
|
|
.mk-nav-item{
|
|
margin:0;
|
|
}
|
|
}
|
|
|
|
@media (max-width:640px){
|
|
.mk-header{
|
|
flex-direction:column;
|
|
align-items:flex-start;
|
|
gap:8px;
|
|
}
|
|
.mk-main-header{
|
|
flex-direction:column;
|
|
align-items:flex-start;
|
|
gap:6px;
|
|
}
|
|
}
|
|
|
|
/* ===== Keyboard player + selection styles ===== */
|
|
|
|
#kb-player{
|
|
position:fixed;
|
|
width:26px;
|
|
height:26px;
|
|
border-radius:40%;
|
|
background:radial-gradient(circle at 30% 20%, #ffffff, #7cf5ff);
|
|
box-shadow:
|
|
0 0 10px rgba(124,245,255,.9),
|
|
0 0 26px rgba(124,245,255,.5);
|
|
transform:translate(-50%, -50%);
|
|
z-index:999;
|
|
pointer-events:none;
|
|
}
|
|
|
|
#kb-player::after{
|
|
content:"";
|
|
position:absolute;
|
|
left:50%;
|
|
top:50%;
|
|
width:4px;
|
|
height:9px;
|
|
border-radius:10px;
|
|
background:rgba(0,0,0,.35);
|
|
transform:translate(-50%,-80%);
|
|
}
|
|
|
|
#kb-hint{
|
|
position:fixed;
|
|
bottom:10px;
|
|
left:50%;
|
|
transform:translateX(-50%);
|
|
padding:6px 12px;
|
|
font-size:11px;
|
|
border-radius:999px;
|
|
background:rgba(0,0,0,.65);
|
|
border:1px solid rgba(255,255,255,.10);
|
|
z-index:998;
|
|
color:var(--muted);
|
|
}
|
|
#kb-hint kbd{
|
|
display:inline-block;
|
|
min-width:18px;
|
|
padding:1px 5px;
|
|
border-radius:4px;
|
|
border:1px solid rgba(255,255,255,.3);
|
|
font-size:10px;
|
|
font-weight:600;
|
|
text-align:center;
|
|
background:rgba(255,255,255,.06);
|
|
margin:0 1px;
|
|
}
|
|
|
|
/* Highlight when orb is overlapping a nav link */
|
|
.mk-nav-link.kb-highlight{
|
|
background:radial-gradient(circle at 0 0,var(--accent-soft),transparent 55%);
|
|
border-color:var(--accent-border);
|
|
color:var(--text);
|
|
transform:translateX(2px);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="mk-header">
|
|
<div class="mk-logo">
|
|
<div class="mk-logo-main">Mediakor</div>
|
|
<div class="mk-logo-sub">Central Hub</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mk-shell">
|
|
<!-- LEFT SIDEBAR NAV -->
|
|
<aside class="mk-sidebar">
|
|
<div class="mk-sidebar-title">Navigation</div>
|
|
<ul class="mk-nav">
|
|
<li class="mk-nav-item">
|
|
<a href="https://med.mediakor.com/index.php/" class="mk-nav-link active">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Medievalkor</strong><br>
|
|
<small>Web RPG</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://plex.mediakor.com/" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Media Server</strong><br>
|
|
<small>Plex for enjoyment needs</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://moni.mediakor.com/" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Status Panel</strong><br>
|
|
<small>Monitoring</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://wiki.mediakor.com/" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Documentation</strong><br>
|
|
<small>Wiki & Guides</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://emu.mediakor.com/" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Emulator</strong><br>
|
|
<small>Emulator for all the way through PS1</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://www.mediakor.com/admin_posts.php" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Admin Management</strong><br>
|
|
<small>Site Management</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
<li class="mk-nav-item">
|
|
<a href="https://www.mediakor.com/codetest.html" class="mk-nav-link">
|
|
<span class="mk-dot"></span>
|
|
<div>
|
|
<strong>Web Dev Playground</strong><br>
|
|
<small>Make some pages!</small>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="mk-sidebar-footer">
|
|
<div class="mk-tag">
|
|
<span class="mk-pulse"></span>
|
|
<span>Database online.</span>
|
|
</div>
|
|
<span>Feel free to reach out with any inquiries. atlaskor@mediakor.com</span>
|
|
<?php if ($dbError): ?>
|
|
<span style="color:#f97373;margin-top:4px;">DB status: offline (showing fallback posts)</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- MAIN CONTENT -->
|
|
<section class="mk-main">
|
|
<div class="mk-main-header">
|
|
<div class="mk-main-title">
|
|
<h2>Posts</h2>
|
|
<span>Latest entries from the Mediakor network.</span>
|
|
</div>
|
|
<div class="mk-main-meta">
|
|
Feed Mode: <strong style="color:var(--accent); margin-left:4px;">Chronological</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mk-post-list">
|
|
<?php if (empty($posts)): ?>
|
|
<article class="mk-post">
|
|
<div class="mk-post-inner">
|
|
<header class="mk-post-header">
|
|
<h3 class="mk-post-title">No Posts Yet</h3>
|
|
<div class="mk-post-meta">Setup • Info</div>
|
|
</header>
|
|
<div class="mk-post-body">
|
|
Use phpMyAdmin or your SQL client to insert rows into the <code>posts</code> table and they will appear here automatically.
|
|
</div>
|
|
<footer class="mk-post-footer">
|
|
<span class="mk-pill">System</span>
|
|
</footer>
|
|
</div>
|
|
</article>
|
|
<?php else: ?>
|
|
<?php foreach ($posts as $post): ?>
|
|
<article class="mk-post">
|
|
<div class="mk-post-inner">
|
|
<header class="mk-post-header">
|
|
<h3 class="mk-post-title">
|
|
<?php echo htmlspecialchars($post['title']); ?>
|
|
</h3>
|
|
<div class="mk-post-meta">
|
|
<?php echo htmlspecialchars($post['meta'] ?? 'Log Entry'); ?>
|
|
</div>
|
|
</header>
|
|
<div class="mk-post-body">
|
|
<?php echo nl2br(htmlspecialchars($post['body'])); ?>
|
|
</div>
|
|
<footer class="mk-post-footer">
|
|
<span class="mk-pill">Entry</span>
|
|
<div class="mk-post-actions">
|
|
<!-- These could later link to a dedicated post page or admin tools -->
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</article>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<!-- Keyboard-player orb + hint -->
|
|
<div id="kb-player"></div>
|
|
<div id="kb-hint">
|
|
Move with <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd>, stand on a nav item, press <kbd>E</kbd> to select.
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
const player = document.getElementById('kb-player');
|
|
const hint = document.getElementById('kb-hint');
|
|
const interactables = Array.from(document.querySelectorAll('.mk-nav-link'));
|
|
|
|
// Initial position (near left side / sidebar)
|
|
let x = 160;
|
|
let y = 220;
|
|
const speed = 230; // pixels per second
|
|
const keys = { w:false, a:false, s:false, d:false };
|
|
|
|
function updatePlayer(){
|
|
player.style.left = x + 'px';
|
|
player.style.top = y + 'px';
|
|
}
|
|
updatePlayer();
|
|
|
|
// Key handling
|
|
window.addEventListener('keydown', (e)=>{
|
|
switch(e.code){
|
|
case 'KeyW':
|
|
case 'KeyA':
|
|
case 'KeyS':
|
|
case 'KeyD':
|
|
case 'KeyE':
|
|
e.preventDefault(); // stop page scrolling on these keys
|
|
break;
|
|
}
|
|
|
|
if(e.code === 'KeyW') keys.w = true;
|
|
if(e.code === 'KeyA') keys.a = true;
|
|
if(e.code === 'KeyS') keys.s = true;
|
|
if(e.code === 'KeyD') keys.d = true;
|
|
|
|
if(e.code === 'KeyE'){
|
|
tryInteract();
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keyup', (e)=>{
|
|
if(e.code === 'KeyW') keys.w = false;
|
|
if(e.code === 'KeyA') keys.a = false;
|
|
if(e.code === 'KeyS') keys.s = false;
|
|
if(e.code === 'KeyD') keys.d = false;
|
|
});
|
|
|
|
// Movement loop
|
|
let lastTime = null;
|
|
function loop(timestamp){
|
|
if(lastTime === null) lastTime = timestamp;
|
|
const dt = (timestamp - lastTime) / 1000;
|
|
lastTime = timestamp;
|
|
|
|
let vx = 0, vy = 0;
|
|
if(keys.w) vy -= 1;
|
|
if(keys.s) vy += 1;
|
|
if(keys.a) vx -= 1;
|
|
if(keys.d) vx += 1;
|
|
|
|
if(vx !== 0 || vy !== 0){
|
|
const len = Math.hypot(vx, vy) || 1;
|
|
vx /= len;
|
|
vy /= len;
|
|
|
|
x += vx * speed * dt;
|
|
y += vy * speed * dt;
|
|
|
|
const half = 13;
|
|
const maxW = window.innerWidth;
|
|
const maxH = window.innerHeight;
|
|
|
|
if(x < half) x = half;
|
|
if(y < half) y = half;
|
|
if(x > maxW - half) x = maxW - half;
|
|
if(y > maxH - half) y = maxH - half;
|
|
|
|
updatePlayer();
|
|
}
|
|
|
|
updateHighlights();
|
|
requestAnimationFrame(loop);
|
|
}
|
|
requestAnimationFrame(loop);
|
|
|
|
function updateHighlights(){
|
|
const pRect = player.getBoundingClientRect();
|
|
let best = null;
|
|
let bestArea = 0;
|
|
|
|
interactables.forEach(el=>{
|
|
const r = el.getBoundingClientRect();
|
|
const overlapX = Math.max(0, Math.min(pRect.right, r.right) - Math.max(pRect.left, r.left));
|
|
const overlapY = Math.max(0, Math.min(pRect.bottom, r.bottom) - Math.max(pRect.top, r.top));
|
|
const area = overlapX * overlapY;
|
|
|
|
if(area > 1 && area > bestArea){
|
|
bestArea = area;
|
|
best = el;
|
|
}
|
|
});
|
|
|
|
interactables.forEach(el => el.classList.remove('kb-highlight'));
|
|
if(best){
|
|
best.classList.add('kb-highlight');
|
|
}
|
|
}
|
|
|
|
function tryInteract(){
|
|
const highlighted = document.querySelector('.mk-nav-link.kb-highlight');
|
|
if(!highlighted) return;
|
|
|
|
const href = highlighted.getAttribute('href');
|
|
|
|
if(href && href.startsWith('#') && href.length > 1){
|
|
const target = document.querySelector(href);
|
|
if(target){
|
|
target.scrollIntoView({ behavior:'smooth', block:'start' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(href && href !== '#'){
|
|
window.location.href = href;
|
|
} else {
|
|
highlighted.click();
|
|
}
|
|
}
|
|
|
|
// Adjust player if window size changes (keeps it on-screen)
|
|
window.addEventListener('resize', ()=>{
|
|
const half = 13;
|
|
const maxW = window.innerWidth;
|
|
const maxH = window.innerHeight;
|
|
if(x > maxW - half) x = maxW - half;
|
|
if(y > maxH - half) y = maxH - half;
|
|
updatePlayer();
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|