171 lines
7.8 KiB
PHP
171 lines
7.8 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/lib/tetris_store.php';
|
||
|
||
$projectName = trim((string) ($_SERVER['PROJECT_NAME'] ?? 'RetroStack'));
|
||
$projectDescription = trim((string) ($_SERVER['PROJECT_DESCRIPTION'] ?? 'Classic Tetris-style puzzle gameplay with an online leaderboard.'));
|
||
$projectImageUrl = trim((string) ($_SERVER['PROJECT_IMAGE_URL'] ?? ''));
|
||
$scoreId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||
$score = null;
|
||
$rank = null;
|
||
$errorMessage = null;
|
||
$topScores = [];
|
||
|
||
try {
|
||
if ($scoreId > 0) {
|
||
$score = tetrisFetchScore($scoreId);
|
||
$rank = $score ? tetrisFetchScoreRank($scoreId) : null;
|
||
}
|
||
$topScores = tetrisFetchTopScores(10);
|
||
} catch (Throwable $exception) {
|
||
$errorMessage = 'Leaderboard data is unavailable right now.';
|
||
error_log('Tetris score page error: ' . $exception->getMessage());
|
||
}
|
||
|
||
if (!$score) {
|
||
http_response_code(404);
|
||
}
|
||
|
||
$pageTitle = $score ? sprintf('%s — %s score %d', $projectName !== '' ? $projectName : 'RetroStack', $score['player_name'], (int) $score['score']) : (($projectName !== '' ? $projectName : 'RetroStack') . ' — Score not found');
|
||
$pageDescription = $score
|
||
? sprintf('%s reached %d points, %d lines, and level %d in this RetroStack run.', $score['player_name'], (int) $score['score'], (int) $score['lines_cleared'], (int) $score['level_reached'])
|
||
: $projectDescription;
|
||
|
||
function esc(?string $value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= esc($pageTitle) ?></title>
|
||
<meta name="description" content="<?= esc($pageDescription) ?>">
|
||
<?php if ($projectDescription !== ''): ?>
|
||
<meta property="og:description" content="<?= esc($projectDescription) ?>">
|
||
<meta property="twitter:description" content="<?= esc($projectDescription) ?>">
|
||
<?php endif; ?>
|
||
<?php if ($projectImageUrl !== ''): ?>
|
||
<meta property="og:image" content="<?= esc($projectImageUrl) ?>">
|
||
<meta property="twitter:image" content="<?= esc($projectImageUrl) ?>">
|
||
<?php endif; ?>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||
<link rel="stylesheet" href="assets/css/custom.css?v=<?= urlencode((string) filemtime(__DIR__ . '/assets/css/custom.css')) ?>">
|
||
</head>
|
||
<body>
|
||
<header class="border-bottom border-secondary-subtle sticky-top app-header">
|
||
<nav class="navbar navbar-expand-lg navbar-dark py-3">
|
||
<div class="container">
|
||
<a class="navbar-brand d-flex align-items-center gap-2" href="index.php#top">
|
||
<span class="brand-mark">RS</span>
|
||
<span>
|
||
<span class="d-block brand-title">RetroStack</span>
|
||
<span class="brand-subtitle">Online score detail</span>
|
||
</span>
|
||
</a>
|
||
<div class="ms-auto d-flex gap-2">
|
||
<a class="btn btn-outline-light btn-sm" href="index.php#leaderboard">Leaderboard</a>
|
||
<a class="btn btn-light btn-sm" href="index.php#play">Play again</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
</header>
|
||
|
||
<main class="py-5">
|
||
<div class="container">
|
||
<?php if ($errorMessage): ?>
|
||
<div class="alert alert-warning border-0 surface-panel mb-4"><?= esc($errorMessage) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$score): ?>
|
||
<section class="surface-panel p-4 p-lg-5 text-center mx-auto" style="max-width: 720px;">
|
||
<span class="section-label">Score detail</span>
|
||
<h1 class="h2 mt-3">That run could not be found.</h1>
|
||
<p class="text-secondary mb-4">Try a fresh round and submit a new score to populate the online leaderboard.</p>
|
||
<a class="btn btn-light" href="index.php#play">Return to the game</a>
|
||
</section>
|
||
<?php else: ?>
|
||
<div class="row g-4 align-items-start">
|
||
<div class="col-lg-8">
|
||
<section class="surface-panel p-4 p-lg-5">
|
||
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-start mb-4">
|
||
<div>
|
||
<span class="section-label">Verified leaderboard run</span>
|
||
<h1 class="display-title mt-3 mb-2"><?= esc($score['player_name']) ?></h1>
|
||
<p class="text-secondary mb-0">Submitted <?= esc(date('M j, Y H:i', strtotime((string) $score['created_at']))) ?> UTC</p>
|
||
</div>
|
||
<div class="score-rank-pill">Rank #<?= (int) $rank ?></div>
|
||
</div>
|
||
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="metric-card h-100">
|
||
<div class="metric-label">Score</div>
|
||
<div class="metric-value"><?= number_format((int) $score['score']) ?></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="metric-card h-100">
|
||
<div class="metric-label">Lines</div>
|
||
<div class="metric-value"><?= number_format((int) $score['lines_cleared']) ?></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="metric-card h-100">
|
||
<div class="metric-label">Level</div>
|
||
<div class="metric-value"><?= number_format((int) $score['level_reached']) ?></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-sm-6 col-xl-3">
|
||
<div class="metric-card h-100">
|
||
<div class="metric-label">Duration</div>
|
||
<div class="metric-value"><?= number_format((int) $score['duration_seconds']) ?>s</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="surface-subpanel p-4">
|
||
<h2 class="h5 mb-3">Run notes</h2>
|
||
<ul class="detail-list mb-0">
|
||
<li>Online leaderboard entries are ordered by score, then lines, then level, then shorter survival time.</li>
|
||
<li>This detail page gives each score a shareable destination for friendly competition.</li>
|
||
<li>To improve your rank, return to the main board and chase a higher line clear count.</li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<aside class="surface-panel p-4">
|
||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||
<div>
|
||
<span class="section-label">Top runs</span>
|
||
<h2 class="h5 mt-2 mb-0">Leaderboard snapshot</h2>
|
||
</div>
|
||
<a class="text-decoration-none small-link" href="index.php#leaderboard">View live board</a>
|
||
</div>
|
||
<?php if (!$topScores): ?>
|
||
<p class="text-secondary mb-0">No scores yet. Be the first to submit a run.</p>
|
||
<?php else: ?>
|
||
<div class="leaderboard-list compact-list">
|
||
<?php foreach ($topScores as $index => $entry): ?>
|
||
<a class="leaderboard-item <?= (int) $entry['id'] === (int) $score['id'] ? 'is-active' : '' ?>" href="score.php?id=<?= (int) $entry['id'] ?>">
|
||
<span class="leaderboard-rank">#<?= $index + 1 ?></span>
|
||
<span class="leaderboard-player"><?= esc($entry['player_name']) ?></span>
|
||
<span class="leaderboard-meta"><?= number_format((int) $entry['score']) ?> pts</span>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</main>
|
||
</body>
|
||
</html>
|