feat(*): refactor dynamic vs. static structure and distinctions; make additional elements dynamic

This commit is contained in:
Cory Dransfeldt 2025-06-13 16:31:36 -07:00
parent 7a0b808f24
commit be0fd1451c
No known key found for this signature in database
136 changed files with 987 additions and 960 deletions

View file

@ -59,6 +59,10 @@ CMD bash -c "\
echo \"${SSH_PRIVATE_KEY}\" > ~/.ssh/id_rsa && \ echo \"${SSH_PRIVATE_KEY}\" > ~/.ssh/id_rsa && \
chmod 600 ~/.ssh/id_rsa && \ chmod 600 ~/.ssh/id_rsa && \
ssh-keyscan -H \"${SERVER_IP}\" >> ~/.ssh/known_hosts && \ ssh-keyscan -H \"${SERVER_IP}\" >> ~/.ssh/known_hosts && \
rsync -avz --delete dist/ root@\"${SERVER_IP}\":/var/www/coryd.dev/ && \ rsync -avz --delete dist/ root@\"${SERVER_IP}\":/var/www/coryd.dev/public/ && \
rsync -avz --delete vendor/ root@\"${SERVER_IP}\":/var/www/coryd.dev/vendor/ && \
rsync -avz --delete app/ root@\"${SERVER_IP}\":/var/www/coryd.dev/app/ && \
rsync -avz --delete composer.json root@\"${SERVER_IP}\":/var/www/coryd.dev/composer.json && \
rsync -avz --delete bootstrap.php root@\"${SERVER_IP}\":/var/www/coryd.dev/bootstrap.php && \
echo \"✅ Deployed successfully\" && \ echo \"✅ Deployed successfully\" && \
tail -f /dev/null" tail -f /dev/null"

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
require __DIR__ . "/Utils/init.php"; require __DIR__ . "/Utils/init.php";
use App\Classes\ApiHandler; use App\Classes\ApiHandler;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
use App\Classes\ApiHandler; use App\Classes\ApiHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
use App\Classes\BaseHandler; use App\Classes\BaseHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
use App\Classes\ApiHandler; use App\Classes\ApiHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
require __DIR__ . '/../server/utils/init.php';
use App\Classes\BaseHandler; use App\Classes\BaseHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . '/../server/utils/init.php'; require_once __DIR__ . '/../bootstrap.php';
$id = $_GET['id'] ?? null; $id = $_GET['id'] ?? null;
$class = $_GET['class'] ?? null; $class = $_GET['class'] ?? null;

View file

@ -1,78 +0,0 @@
<?php
require __DIR__ . "/../vendor/autoload.php";
use App\Classes\BaseHandler;
class LatestListenHandler extends BaseHandler
{
protected int $cacheTTL = 60;
public function __construct()
{
parent::__construct();
$this->initializeCache();
}
public function handleRequest(): void
{
try {
$cachedData = $this->cache ? $this->cache->get("latest_listen") : null;
if ($cachedData) {
$this->sendResponse(json_decode($cachedData, true));
return;
}
$data = $this->makeRequest("GET", "optimized_latest_listen?select=*");
if (!is_array($data) || empty($data[0])) {
$this->sendResponse(["message" => "No recent tracks found"], 404);
return;
}
$latestListen = $this->formatLatestListen($data[0]);
if ($this->cache) $this->cache->set(
"latest_listen",
json_encode($latestListen),
$this->cacheTTL
);
$this->sendResponse($latestListen);
} catch (\Exception $e) {
error_log("LatestListenHandler Error: " . $e->getMessage());
$this->sendErrorResponse("Internal Server Error: " . $e->getMessage(), 500);
}
}
private function formatLatestListen(array $latestListen): array
{
$emoji = $latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧");
$trackName = htmlspecialchars(
$latestListen["track_name"] ?? "Unknown Track",
ENT_QUOTES,
"UTF-8"
);
$artistName = htmlspecialchars(
$latestListen["artist_name"] ?? "Unknown Artist",
ENT_QUOTES,
"UTF-8"
);
$url = htmlspecialchars($latestListen["url"] ?? "/", ENT_QUOTES, "UTF-8");
return [
"content" => sprintf(
'%s %s by <a href="https://www.coryd.dev%s">%s</a>',
$emoji,
$trackName,
$url,
$artistName
),
];
}
}
$handler = new LatestListenHandler();
$handler->handleRequest();

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
require __DIR__ . '/../server/utils/init.php';
use App\Classes\BaseHandler; use App\Classes\BaseHandler;
@ -20,7 +19,7 @@ class QueryHandler extends BaseHandler
$referer = $_SERVER['HTTP_REFERER'] ?? ''; $referer = $_SERVER['HTTP_REFERER'] ?? '';
$hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true); $hostAllowed = fn($url) => in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true);
if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden invalid origin", 403); if (!$hostAllowed($origin) && !$hostAllowed($referer)) $this->sendErrorResponse("Forbidden: invalid origin", 403);
$allowedSource = $origin ?: $referer; $allowedSource = $origin ?: $referer;
$scheme = parse_url($allowedSource, PHP_URL_SCHEME) ?? 'https'; $scheme = parse_url($allowedSource, PHP_URL_SCHEME) ?? 'https';

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
require __DIR__ . "/Utils/init.php";
use App\Classes\ApiHandler; use App\Classes\ApiHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
require __DIR__ . '/../server/utils/init.php';
use App\Classes\BaseHandler; use App\Classes\BaseHandler;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
use App\Classes\ApiHandler; use App\Classes\ApiHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -1,6 +1,6 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require_once __DIR__ . '/../bootstrap.php';
use App\Classes\ApiHandler; use App\Classes\ApiHandler;
use GuzzleHttp\Client; use GuzzleHttp\Client;

View file

@ -2,9 +2,6 @@
namespace App\Classes; namespace App\Classes;
require __DIR__ . "/BaseHandler.php";
require __DIR__ . '/../../server/utils/init.php';
abstract class ApiHandler extends BaseHandler abstract class ApiHandler extends BaseHandler
{ {
protected function ensureCliAccess(): void protected function ensureCliAccess(): void

View file

@ -2,8 +2,6 @@
namespace App\Classes; namespace App\Classes;
require __DIR__ . "/../../vendor/autoload.php";
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;

View file

@ -0,0 +1,69 @@
<?php
namespace App\Classes;
use App\Classes\BaseHandler;
class LatestListenHandler extends BaseHandler
{
protected int $cacheTTL = 60;
public function __construct()
{
parent::__construct();
$this->initializeCache();
}
public function handleRequest(): void
{
$data = $this->getLatestListen();
if (!$data) {
$this->sendResponse(["message" => "No recent tracks found"], 404);
return;
}
$this->sendResponse($data);
}
public function getLatestListen(): ?array
{
try {
$cachedData = $this->cache ? $this->cache->get("latest_listen") : null;
if ($cachedData) return json_decode($cachedData, true);
$data = $this->makeRequest("GET", "optimized_latest_listen?select=*");
if (!is_array($data) || empty($data[0])) return null;
$latestListen = $this->formatLatestListen($data[0]);
if ($this->cache) $this->cache->set("latest_listen", json_encode($latestListen), $this->cacheTTL);
return $latestListen;
} catch (\Exception $e) {
error_log("LatestListenHandler::getLatestListen error: " . $e->getMessage());
return null;
}
}
private function formatLatestListen(array $latestListen): array
{
$emoji = $latestListen["artist_emoji"] ?? ($latestListen["genre_emoji"] ?? "🎧");
$trackName = htmlspecialchars($latestListen["track_name"] ?? "Unknown Track", ENT_QUOTES, "UTF-8");
$artistName = htmlspecialchars($latestListen["artist_name"] ?? "Unknown Artist", ENT_QUOTES, "UTF-8");
$url = htmlspecialchars($latestListen["url"] ?? "/", ENT_QUOTES, "UTF-8");
return [
"content" => sprintf(
'%s %s by <a href="%s">%s</a>',
$emoji,
$trackName,
$url,
$artistName
),
];
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Classes;
class MusicDataHandler extends BaseHandler
{
protected int $cacheTTL = 300;
public function getThisWeekData(): array
{
$cacheKey = 'music_week_data';
$cached = $this->cache ? $this->cache->get($cacheKey) : null;
if ($cached) return json_decode($cached, true);
$response = $this->makeRequest('GET', 'optimized_week_music?select=*');
$music = $response[0]['week_music'] ?? [];
$music['total_tracks'] = $music['week_summary']['total_tracks'] ?? 0;
$music['total_artists'] = $music['week_summary']['total_artists'] ?? 0;
$music['total_albums'] = $music['week_summary']['total_albums'] ?? 0;
if ($this->cache) $this->cache->set($cacheKey, json_encode($music), $this->cacheTTL);
return $music;
}
}

View file

@ -11,21 +11,18 @@ abstract class PageFetcher extends BaseHandler
protected function cacheGet(string $key): mixed protected function cacheGet(string $key): mixed
{ {
return $this->cache && $this->cache->exists($key) return $this->cache && $this->cache->exists($key) ? json_decode($this->cache->get($key), true) : null;
? json_decode($this->cache->get($key), true)
: null;
} }
protected function cacheSet(string $key, mixed $value, int $ttl = 3600): void protected function cacheSet(string $key, mixed $value, int $ttl = 300): void
{ {
if ($this->cache) { if ($this->cache) $this->cache->setex($key, $ttl, json_encode($value));
$this->cache->setex($key, $ttl, json_encode($value));
}
} }
protected function fetchSingleFromApi(string $endpoint, string $url): ?array protected function fetchSingleFromApi(string $endpoint, string $url): ?array
{ {
$data = $this->fetchFromApi($endpoint, "url=eq./{$url}"); $data = $this->fetchFromApi($endpoint, "url=eq./{$url}");
return $data[0] ?? null; return $data[0] ?? null;
} }
@ -39,6 +36,7 @@ abstract class PageFetcher extends BaseHandler
if ($this->globals !== null) return $this->globals; if ($this->globals !== null) return $this->globals;
$fetcher = new GlobalsFetcher(); $fetcher = new GlobalsFetcher();
$this->globals = $fetcher->fetch(); $this->globals = $fetcher->fetch();
return $this->globals; return $this->globals;

View file

@ -0,0 +1,36 @@
<?php
namespace App\Classes;
class RecentMediaHandler extends BaseHandler
{
protected int $cacheTTL = 300;
public function getRecentMedia(): array
{
try {
$cacheKey = 'recent_media';
if ($this->cache) {
$cached = $this->cache->get($cacheKey);
if ($cached) return json_decode($cached, true);
}
$response = $this->makeRequest("GET", "optimized_recent_media?select=*");
$activity = $response[0]['recent_activity'] ?? [];
$data = [
'recentMusic' => $activity['recentMusic'] ?? [],
'recentWatchedRead' => $activity['recentWatchedRead'] ?? [],
];
if ($this->cache) $this->cache->set($cacheKey, json_encode($data), $this->cacheTTL);
return $data;
} catch (\Exception $e) {
error_log("RecentMediaHandler error: " . $e->getMessage());
return ['recentMusic' => [], 'recentWatchedRead' => []];
}
}
}

22
app/utils/icons.php Normal file
View file

@ -0,0 +1,22 @@
<?php
if (!function_exists('getTablerIcon')) {
function getTablerIcon($iconName, $class = '', $size = 24)
{
$icons = [
'arrow-left' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /></svg>',
'arrow-right' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" /></svg>',
'article' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>',
'books' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-books"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041z" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></svg>',
'device-tv-old' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv-old"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M16 3l-4 4l-4 -4" /><path d="M15 7v13" /><path d="M18 15v.01" /><path d="M18 12v.01" /></svg>',
'headphones' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M15 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M4 15v-3a8 8 0 0 1 16 0v3" /></svg>',
'movie' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M8 4l0 16" /><path d="M16 4l0 16" /><path d="M4 8l4 0" /><path d="M4 16l4 0" /><path d="M4 12l16 0" /><path d="M16 8l4 0" /><path d="M16 16l4 0" /></svg>',
'star' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-star"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg>',
'vinyl' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-vinyl"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 3.937a9 9 0 1 0 5 8.063" /><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M20 4m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M20 4l-3.5 10l-2.5 2" /></svg>'
];
return $icons[$iconName] ?? '<span class="icon-placeholder">[Missing: ' . htmlspecialchars($iconName) . ']</span>';
}
}
?>

147
app/utils/media.php Normal file
View file

@ -0,0 +1,147 @@
<?php
if (!function_exists('renderMediaGrid')) {
function renderMediaGrid(array $items, int $count = 0, string $loading = 'lazy')
{
$limit = $count > 0 ? $count : count($items);
$firstType = $items[0]['type'] ?? ($items[0]['grid']['type'] ?? '');
$shapeClass = in_array($firstType, ['books', 'movies', 'tv']) ? 'vertical' : 'square';
echo '<div class="media-grid ' . $shapeClass . '">';
foreach (array_slice($items, 0, $limit) as $item) {
$grid = $item['grid'] ?? $item;
$alt = htmlspecialchars($grid['alt'] ?? '');
$image = htmlspecialchars($grid['image'] ?? '');
$title = htmlspecialchars($grid['title'] ?? '');
$subtext = htmlspecialchars($grid['subtext'] ?? '');
$url = $grid['url'] ?? null;
$type = $item['type'] ?? '';
$isVertical = in_array($type, ['books', 'movies', 'tv']);
$imageClass = $isVertical ? 'vertical' : 'square';
$width = $isVertical ? 120 : 150;
$height = $isVertical ? 184 : 150;
$openLink = $url ? '<a class="' . htmlspecialchars($type) . '" href="' . htmlspecialchars($url) . '" title="' . $alt . '">' : '';
$closeLink = $url ? '</a>' : '';
echo $openLink;
echo '<div class="media-grid-item">';
if ($title || $subtext) {
echo '<div class="meta-text media-highlight">';
if ($title) echo '<div class="header">' . $title . '</div>';
if ($subtext) echo '<div class="subheader">' . $subtext . '</div>';
echo '</div>';
}
echo '<img
srcset="' . $image . '?class=' . $imageClass . 'sm&type=webp ' . $width . 'w, ' .
$image . '?class=' . $imageClass . 'md&type=webp ' . ($width * 2) . 'w"
sizes="(max-width: 450px) ' . $width . 'px, ' . ($width * 2) . 'px"
src="' . $image . '?class=' . $imageClass . 'sm&type=webp"
alt="' . $alt . '"
loading="' . $loading . '"
decoding="async"
width="' . $width . '"
height="' . $height . '"
>';
echo '</div>';
echo $closeLink;
}
echo '</div>';
}
}
if (!function_exists('renderAssociatedMedia')) {
function renderAssociatedMedia(
array $artists = [],
array $books = [],
array $genres = [],
array $movies = [],
array $posts = [],
array $shows = [],
)
{
$sections = [
"artists" => ["icon" => "headphones", "css_class" => "music", "label" => "Related artist(s)", "hasGrid" => true],
"books" => ["icon" => "books", "css_class" => "books", "label" => "Related book(s)", "hasGrid" => true],
"genres" => ["icon" => "headphones", "css_class" => "music", "label" => "Related genre(s)", "hasGrid" => false],
"movies" => ["icon" => "movie", "css_class" => "movies", "label" => "Related movie(s)", "hasGrid" => true],
"posts" => ["icon" => "article", "css_class" => "article", "label" => "Related post(s)", "hasGrid" => false],
"shows" => ["icon" => "device-tv-old", "css_class" => "tv", "label" => "Related show(s)", "hasGrid" => true]
];
$allMedia = compact('artists', 'books', 'genres', 'movies', 'posts', 'shows');
echo '<div class="associated-media">';
foreach ($sections as $key => $section) {
$items = $allMedia[$key];
if (empty($items)) continue;
echo '<h3 id="' . htmlspecialchars($key) . '" class="' . htmlspecialchars($section['css_class']) . '">';
echo getTablerIcon($section['icon']);
echo htmlspecialchars($section['label']);
echo '</h3>';
if ($section['hasGrid']) {
renderMediaGrid($items);
} else {
echo '<ul>';
foreach ($items as $item) {
echo '<li class="' . htmlspecialchars($section['css_class']) . '">';
echo '<a href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title'] ?? $item['name'] ?? '') . '</a>';
if ($key === "artists" && isset($item['total_plays']) && $item['total_plays'] > 0) {
echo ' (' . htmlspecialchars($item['total_plays']) . ' play' . ($item['total_plays'] > 1 ? 's' : '') . ')';
} elseif ($key === "books" && isset($item['author'])) {
echo ' by ' . htmlspecialchars($item['author']);
} elseif (($key === "movies" || $key === "shows") && isset($item['year'])) {
echo ' (' . htmlspecialchars($item['year']) . ')';
} elseif ($key === "posts" && isset($item['date'])) {
echo ' (' . date("F j, Y", strtotime($item['date'])) . ')';
}
echo '</li>';
}
echo '</ul>';
}
}
echo '</div>';
}
}
if (!function_exists('renderMediaLinks')) {
function renderMediaLinks(array $data, string $type, int $count = 10): string {
if (empty($data) || empty($type)) return "";
$slice = array_slice($data, 0, $count);
if (count($slice) === 0) return "";
$buildLink = function ($item) use ($type) {
switch ($type) {
case "genre":
return '<a href="' . htmlspecialchars($item['genre_url']) . '">' . htmlspecialchars($item['genre_name']) . '</a>';
case "artist":
return '<a href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['name']) . '</a>';
case "book":
return '<a href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
default:
return '';
}
};
if (count($slice) === 1) return $buildLink($slice[0]);
$links = array_map($buildLink, $slice);
$last = array_pop($links);
return implode(', ', $links) . ' and ' . $last;
}
}
?>

39
app/utils/metadata.php Normal file
View file

@ -0,0 +1,39 @@
<?php
if (!function_exists('setupPageMetadata')) {
function setupPageMetadata(array $page, string $requestUri): array
{
$globals = $page['globals'] ?? [];
$title = htmlspecialchars($page["metadata"]["title"] ?? "", ENT_QUOTES, "UTF-8");
$description = htmlspecialchars($page["metadata"]["description"] ?? "", ENT_QUOTES, "UTF-8");
$image = htmlspecialchars(
$page["metadata"]["open_graph_image"] ?? $globals["metadata"]["open_graph_image"] ?? "",
ENT_QUOTES,
"UTF-8"
);
$fullUrl = $globals["url"] . $requestUri;
$oembedUrl = $globals["url"] . "/oembed" . $requestUri;
return [
'pageTitle' => $title,
'pageDescription' => $description,
'ogImage' => $image,
'fullUrl' => $fullUrl,
'oembedUrl' => $oembedUrl,
'globals' => $globals
];
}
}
if (!function_exists('cleanMeta')) {
function cleanMeta($value)
{
$value = trim($value ?? '');
$value = str_replace(["\r", "\n"], ' ', $value);
$value = preg_replace('/\s+/', ' ', $value);
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
}
?>

41
app/utils/paginator.php Normal file
View file

@ -0,0 +1,41 @@
<?php
if (!function_exists('renderPaginator')) {
function renderPaginator(array $pagination, int $totalPages): void
{
if (!$pagination || $totalPages <= 1) return;
?>
<script type="module" src="/assets/scripts/components/select-pagination.js" defer></script>
<nav aria-label="Pagination" class="pagination">
<?php if (!empty($pagination['href']['previous'])): ?>
<a href="<?= $pagination['href']['previous'] ?>" aria-label="Previous page">
<?= getTablerIcon('arrow-left') ?>
</a>
<?php else: ?>
<span><?= getTablerIcon('arrow-left') ?></span>
<?php endif; ?>
<select-pagination data-base-index="1">
<select class="client-side" aria-label="Page selection">
<?php foreach ($pagination['pages'] as $i): ?>
<option value="<?= $i ?>" <?= ($pagination['pageNumber'] === $i) ? 'selected' : '' ?>>
<?= $i ?> of <?= $totalPages ?>
</option>
<?php endforeach; ?>
</select>
<noscript>
<p><span aria-current="page"><?= $pagination['pageNumber'] ?></span> of <?= $totalPages ?></p>
</noscript>
</select-pagination>
<?php if (!empty($pagination['href']['next'])): ?>
<a href="<?= $pagination['href']['next'] ?>" aria-label="Next page">
<?= getTablerIcon('arrow-right') ?>
</a>
<?php else: ?>
<span><?= getTablerIcon('arrow-right') ?></span>
<?php endif; ?>
</nav>
<?php
}
}

11
app/utils/routing.php Normal file
View file

@ -0,0 +1,11 @@
<?php
if (!function_exists('redirectTo404')) {
function redirectTo404(): void
{
header("Location: /404/", true, 302);
exit();
}
}
?>

84
app/utils/strings.php Normal file
View file

@ -0,0 +1,84 @@
<?php
use Kaoken\MarkdownIt\MarkdownIt;
use Kaoken\MarkdownIt\Plugins\MarkdownItFootnote;
if (!function_exists('truncateText')) {
function truncateText($text, $limit = 50, $ellipsis = "...")
{
if (mb_strwidth($text, "UTF-8") <= $limit) return $text;
$truncated = mb_substr($text, 0, $limit, "UTF-8");
$lastSpace = mb_strrpos($truncated, " ", 0, "UTF-8");
if ($lastSpace !== false) $truncated = mb_substr($truncated, 0, $lastSpace, "UTF-8");
return $truncated . $ellipsis;
}
}
if (!function_exists('parseMarkdown')) {
function parseMarkdown($markdown)
{
if (empty($markdown)) return '';
$md = new MarkdownIt([
"html" => true,
"linkify" => true,
]);
$md->plugin(new MarkdownItFootnote());
return $md->render($markdown);
}
}
if (!function_exists('parseCountryField')) {
function parseCountryField($countryField)
{
if (empty($countryField)) return null;
$delimiters = [',', '/', '&', ' and '];
$countries = [$countryField];
foreach ($delimiters as $delimiter) {
$tempCountries = [];
foreach ($countries as $country) {
$tempCountries = array_merge($tempCountries, explode($delimiter, $country));
}
$countries = $tempCountries;
}
$countries = array_map('trim', $countries);
$countries = array_map('getCountryName', $countries);
$countries = array_filter($countries);
return implode(', ', array_unique($countries));
}
}
if (!function_exists('getCountryName')) {
function getCountryName($countryName)
{
$isoCodes = new \Sokil\IsoCodes\IsoCodesFactory();
$countries = $isoCodes->getCountries();
$country = $countries->getByAlpha2($countryName);
if ($country) return $country->getName();
return ucfirst(strtolower($countryName));
}
}
if (!function_exists('pluralize')) {
function pluralize($count, $string, $trailing = '')
{
if ((int)$count === 1) return $string;
return $string . 's' . ($trailing ? $trailing : '');
}
}
?>

17
app/utils/tags.php Normal file
View file

@ -0,0 +1,17 @@
<?php
if (!function_exists('renderTags')) {
function renderTags(array $tags): void
{
if (empty($tags)) return;
echo '<div class="tags">';
foreach ($tags as $tag) {
$slug = strtolower(trim($tag));
echo '<a href="/tags/' . rawurlencode($slug) . '">#' . htmlspecialchars($slug) . '</a>';
}
echo '</div>';
}
}

6
bootstrap.php Normal file
View file

@ -0,0 +1,6 @@
<?php
define('PROJECT_ROOT', realpath(__DIR__ . '/..'));
require_once PROJECT_ROOT . '/vendor/autoload.php';
require_once PROJECT_ROOT . '/app/utils/init.php';

64
cli/package-lock.json generated
View file

@ -13,7 +13,7 @@
"commander": "^14.0.0", "commander": "^14.0.0",
"figlet": "^1.8.1", "figlet": "^1.8.1",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.2", "glob": "^11.0.3",
"inquirer": "^12.6.3", "inquirer": "^12.6.3",
"transliteration": "^2.3.5" "transliteration": "^2.3.5"
}, },
@ -337,6 +337,27 @@
} }
} }
}, },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -425,27 +446,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/balanced-match": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz",
"integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==",
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/brace-expansion": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz",
"integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^3.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
@ -673,14 +673,14 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "11.0.2", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.3.1",
"jackspeak": "^4.0.1", "jackspeak": "^4.1.1",
"minimatch": "^10.0.0", "minimatch": "^10.0.3",
"minipass": "^7.1.2", "minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0", "package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0" "path-scurry": "^2.0.0"
@ -791,12 +791,12 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "10.0.2", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-+9TJCIYXgZ2Dm5LxVCFsa8jOm+evMwXHFI0JM1XROmkfkpz8/iLLDh+TwSmyIBrs6C6Xu9294/fq8cBA+P6AqA==", "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^4.0.1" "@isaacs/brace-expansion": "^5.0.0"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "20 || >=22"

View file

@ -16,7 +16,7 @@
"commander": "^14.0.0", "commander": "^14.0.0",
"figlet": "^1.8.1", "figlet": "^1.8.1",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.2", "glob": "^11.0.3",
"inquirer": "^12.6.3", "inquirer": "^12.6.3",
"transliteration": "^2.3.5" "transliteration": "^2.3.5"
} }

View file

@ -16,7 +16,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"App\\Classes\\": "api/Classes/" "App\\Classes\\": "app/Classes/"
} }
}, },
"config": { "config": {

View file

@ -22,8 +22,8 @@ export default async function (eleventyConfig) {
eleventyConfig.addPassthroughCopy("src/assets"); eleventyConfig.addPassthroughCopy("src/assets");
eleventyConfig.addPassthroughCopy("api"); eleventyConfig.addPassthroughCopy("api");
eleventyConfig.addPassthroughCopy("vendor"); eleventyConfig.addPassthroughCopy("bootstrap.php");
eleventyConfig.addPassthroughCopy("server"); eleventyConfig.addPassthroughCopy("config/dynamic");
eleventyConfig.addPassthroughCopy({ eleventyConfig.addPassthroughCopy({
"node_modules/minisearch/dist/umd/index.js": "node_modules/minisearch/dist/umd/index.js":
"assets/scripts/components/minisearch.js", "assets/scripts/components/minisearch.js",

82
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "coryd.dev", "name": "coryd.dev",
"version": "9.2.9", "version": "10.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "coryd.dev", "name": "coryd.dev",
"version": "9.2.9", "version": "10.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"minisearch": "^7.1.2", "minisearch": "^7.1.2",
@ -251,6 +251,29 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -756,9 +779,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001722", "version": "1.0.30001723",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
"integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==", "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1385,9 +1408,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.166", "version": "1.5.167",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
"integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1720,15 +1743,15 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "11.0.2", "version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.3.1",
"jackspeak": "^4.0.1", "jackspeak": "^4.1.1",
"minimatch": "^10.0.0", "minimatch": "^10.0.3",
"minipass": "^7.1.2", "minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0", "package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0" "path-scurry": "^2.0.0"
@ -1756,37 +1779,14 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/glob/node_modules/balanced-match": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz",
"integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz",
"integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^3.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/glob/node_modules/minimatch": { "node_modules/glob/node_modules/minimatch": {
"version": "10.0.2", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-+9TJCIYXgZ2Dm5LxVCFsa8jOm+evMwXHFI0JM1XROmkfkpz8/iLLDh+TwSmyIBrs6C6Xu9294/fq8cBA+P6AqA==", "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^4.0.1" "@isaacs/brace-expansion": "^5.0.0"
}, },
"engines": { "engines": {
"node": "20 || >=22" "node": "20 || >=22"

View file

@ -1,6 +1,6 @@
{ {
"name": "coryd.dev", "name": "coryd.dev",
"version": "9.2.9", "version": "10.0.0",
"description": "The source for my personal site. Built using 11ty (and other tools).", "description": "The source for my personal site. Built using 11ty (and other tools).",
"type": "module", "type": "module",
"engines": { "engines": {

View file

@ -24,7 +24,6 @@ SELECT
'url', CONCAT(globals.url, p.permalink), 'url', CONCAT(globals.url, p.permalink),
'type', 'page' 'type', 'page'
) AS metadata, ) AS metadata,
p.updated,
( (
SELECT SELECT
json_agg( json_agg(

View file

@ -0,0 +1,34 @@
CREATE OR REPLACE VIEW optimized_week_music AS
SELECT json_build_object(
'week_artists', (
SELECT json_agg(a ORDER BY a.plays DESC)
FROM (
SELECT * FROM week_artists ORDER BY plays DESC LIMIT 8
) a
),
'week_albums', (
SELECT json_agg(al ORDER BY al.plays DESC)
FROM (
SELECT * FROM week_albums ORDER BY plays DESC LIMIT 8
) al
),
'week_genres', (
SELECT json_agg(g ORDER BY g.plays DESC)
FROM (
SELECT * FROM week_genres ORDER BY plays DESC LIMIT 5
) g
),
'recent_tracks', (
SELECT json_agg(r ORDER BY r.listened_at DESC)
FROM (
SELECT * FROM recent_tracks ORDER BY listened_at DESC LIMIT 10
) r
),
'week_summary', (
SELECT json_build_object(
'total_tracks', (SELECT COUNT(*) FROM week_tracks),
'total_artists', (SELECT COUNT(*) FROM week_artists),
'total_albums', (SELECT COUNT(*) FROM week_albums)
)
)
) AS week_music;

View file

@ -17,7 +17,7 @@
<VirtualHost *:443> <VirtualHost *:443>
ServerAdmin hi@coryd.dev ServerAdmin hi@coryd.dev
ServerName www.coryd.dev ServerName www.coryd.dev
DocumentRoot /var/www/coryd.dev DocumentRoot /var/www/coryd.dev/public
ErrorLog ${APACHE_LOG_DIR}/error.log ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined CustomLog ${APACHE_LOG_DIR}/access.log combined

View file

@ -1,20 +0,0 @@
<?php
function getTablerIcon($iconName, $class = '', $size = 24)
{
$icons = [
'arrow-left' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /></svg>',
'arrow-right' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 12l14 0" /><path d="M13 18l6 -6" /><path d="M13 6l6 6" /></svg>',
'article' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-article"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M7 8h10" /><path d="M7 12h10" /><path d="M7 16h10" /></svg>',
'books' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-books"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M9 4m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z" /><path d="M5 8h4" /><path d="M9 16h4" /><path d="M13.803 4.56l2.184 -.53c.562 -.135 1.133 .19 1.282 .732l3.695 13.418a1.02 1.02 0 0 1 -.634 1.219l-.133 .041l-2.184 .53c-.562 .135 -1.133 -.19 -1.282 -.732l-3.695 -13.418a1.02 1.02 0 0 1 .634 -1.219l.133 -.041z" /><path d="M14 9l4 -1" /><path d="M16 16l3.923 -.98" /></svg>',
'device-tv-old' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-tv-old"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 7m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" /><path d="M16 3l-4 4l-4 -4" /><path d="M15 7v13" /><path d="M18 15v.01" /><path d="M18 12v.01" /></svg>',
'headphones' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-headphones"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M15 13m0 2a2 2 0 0 1 2 -2h1a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-1a2 2 0 0 1 -2 -2z" /><path d="M4 15v-3a8 8 0 0 1 16 0v3" /></svg>',
'movie' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-movie"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" /><path d="M8 4l0 16" /><path d="M16 4l0 16" /><path d="M4 8l4 0" /><path d="M4 16l4 0" /><path d="M4 12l16 0" /><path d="M16 8l4 0" /><path d="M16 16l4 0" /></svg>',
'star' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-star"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg>',
'vinyl' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-vinyl"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 3.937a9 9 0 1 0 5 8.063" /><path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M20 4m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M20 4l-3.5 10l-2.5 2" /></svg>'
];
return $icons[$iconName] ?? '<span class="icon-placeholder">[Missing: ' . htmlspecialchars($iconName) . ']</span>';
}
?>

View file

@ -1,112 +0,0 @@
<?php
function renderMediaGrid(array $items, int $count = 0, string $loading = 'lazy') {
$limit = $count > 0 ? $count : count($items);
$firstType = $items[0]['type'] ?? ($items[0]['grid']['type'] ?? '');
$shapeClass = in_array($firstType, ['books', 'movies', 'tv']) ? 'vertical' : 'square';
echo '<div class="media-grid ' . $shapeClass . '">';
foreach (array_slice($items, 0, $limit) as $item) {
$grid = $item['grid'] ?? $item;
$alt = htmlspecialchars($grid['alt'] ?? '');
$image = htmlspecialchars($grid['image'] ?? '');
$title = htmlspecialchars($grid['title'] ?? '');
$subtext = htmlspecialchars($grid['subtext'] ?? '');
$url = $grid['url'] ?? null;
$type = $item['type'] ?? '';
$isVertical = in_array($type, ['books', 'movies', 'tv']);
$imageClass = $isVertical ? 'vertical' : 'square';
$width = $isVertical ? 120 : 150;
$height = $isVertical ? 184 : 150;
$openLink = $url ? '<a class="' . htmlspecialchars($type) . '" href="' . htmlspecialchars($url) . '" title="' . $alt . '">' : '';
$closeLink = $url ? '</a>' : '';
echo $openLink;
echo '<div class="media-grid-item">';
if ($title || $subtext) {
echo '<div class="meta-text media-highlight">';
if ($title) echo '<div class="header">' . $title . '</div>';
if ($subtext) echo '<div class="subheader">' . $subtext . '</div>';
echo '</div>';
}
echo '<img
srcset="' . $image . '?class=' . $imageClass . 'sm&type=webp ' . $width . 'w, ' .
$image . '?class=' . $imageClass . 'md&type=webp ' . ($width * 2) . 'w"
sizes="(max-width: 450px) ' . $width . 'px, ' . ($width * 2) . 'px"
src="' . $image . '?class=' . $imageClass . 'sm&type=webp"
alt="' . $alt . '"
loading="' . $loading . '"
decoding="async"
width="' . $width . '"
height="' . $height . '"
>';
echo '</div>';
echo $closeLink;
}
echo '</div>';
}
function renderAssociatedMedia(
array $artists = [],
array $books = [],
array $genres = [],
array $movies = [],
array $posts = [],
array $shows = [],
) {
$sections = [
"artists" => ["icon" => "headphones", "css_class" => "music", "label" => "Related artist(s)", "hasGrid" => true],
"books" => ["icon" => "books", "css_class" => "books", "label" => "Related book(s)", "hasGrid" => true],
"genres" => ["icon" => "headphones", "css_class" => "music", "label" => "Related genre(s)", "hasGrid" => false],
"movies" => ["icon" => "movie", "css_class" => "movies", "label" => "Related movie(s)", "hasGrid" => true],
"posts" => ["icon" => "article", "css_class" => "article", "label" => "Related post(s)", "hasGrid" => false],
"shows" => ["icon" => "device-tv-old", "css_class" => "tv", "label" => "Related show(s)", "hasGrid" => true]
];
$allMedia = compact('artists', 'books', 'genres', 'movies', 'posts', 'shows');
echo '<div class="associated-media">';
foreach ($sections as $key => $section) {
$items = $allMedia[$key];
if (empty($items)) continue;
echo '<h3 id="' . htmlspecialchars($key) . '" class="' . htmlspecialchars($section['css_class']) . '">';
echo getTablerIcon($section['icon']);
echo htmlspecialchars($section['label']);
echo '</h3>';
if ($section['hasGrid']) {
renderMediaGrid($items);
} else {
echo '<ul>';
foreach ($items as $item) {
echo '<li class="' . htmlspecialchars($section['css_class']) . '">';
echo '<a href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title'] ?? $item['name'] ?? '') . '</a>';
if ($key === "artists" && isset($item['total_plays']) && $item['total_plays'] > 0) {
echo ' (' . htmlspecialchars($item['total_plays']) . ' play' . ($item['total_plays'] > 1 ? 's' : '') . ')';
} elseif ($key === "books" && isset($item['author'])) {
echo ' by ' . htmlspecialchars($item['author']);
} elseif (($key === "movies" || $key === "shows") && isset($item['year'])) {
echo ' (' . htmlspecialchars($item['year']) . ')';
} elseif ($key === "posts" && isset($item['date'])) {
echo ' (' . date("F j, Y", strtotime($item['date'])) . ')';
}
echo '</li>';
}
echo '</ul>';
}
}
echo '</div>';
}
?>

View file

@ -1,33 +0,0 @@
<?php
function setupPageMetadata(array $page, string $requestUri): array {
$globals = $page['globals'] ?? [];
$title = htmlspecialchars($page["metadata"]["title"] ?? "", ENT_QUOTES, "UTF-8");
$description = htmlspecialchars($page["metadata"]["description"] ?? "", ENT_QUOTES, "UTF-8");
$image = htmlspecialchars(
$page["metadata"]["open_graph_image"] ?? $globals["metadata"]["open_graph_image"] ?? "",
ENT_QUOTES,
"UTF-8"
);
$fullUrl = $globals["url"] . $requestUri;
$oembedUrl = $globals["url"] . "/oembed" . $requestUri;
return [
'pageTitle' => $title,
'pageDescription' => $description,
'ogImage' => $image,
'fullUrl' => $fullUrl,
'oembedUrl' => $oembedUrl,
'globals' => $globals
];
}
function cleanMeta($value) {
$value = trim($value ?? '');
$value = str_replace(["\r", "\n"], ' ', $value);
$value = preg_replace('/\s+/', ' ', $value);
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
?>

View file

@ -1,40 +0,0 @@
<?php
require_once __DIR__ . '/icons.php';
function renderPaginator(array $pagination, int $totalPages): void {
if (!$pagination || $totalPages <= 1) return;
?>
<script type="module" src="/assets/scripts/components/select-pagination.js" defer></script>
<nav aria-label="Pagination" class="pagination">
<?php if (!empty($pagination['href']['previous'])): ?>
<a href="<?= $pagination['href']['previous'] ?>" aria-label="Previous page">
<?= getTablerIcon('arrow-left') ?>
</a>
<?php else: ?>
<span><?= getTablerIcon('arrow-left') ?></span>
<?php endif; ?>
<select-pagination data-base-index="1">
<select class="client-side" aria-label="Page selection">
<?php foreach ($pagination['pages'] as $i): ?>
<option value="<?= $i ?>" <?= ($pagination['pageNumber'] === $i) ? 'selected' : '' ?>>
<?= $i ?> of <?= $totalPages ?>
</option>
<?php endforeach; ?>
</select>
<noscript>
<p><span aria-current="page"><?= $pagination['pageNumber'] ?></span> of <?= $totalPages ?></p>
</noscript>
</select-pagination>
<?php if (!empty($pagination['href']['next'])): ?>
<a href="<?= $pagination['href']['next'] ?>" aria-label="Next page">
<?= getTablerIcon('arrow-right') ?>
</a>
<?php else: ?>
<span><?= getTablerIcon('arrow-right') ?></span>
<?php endif; ?>
</nav>
<?php
}

View file

@ -1,8 +0,0 @@
<?php
function redirectTo404(): void {
header("Location: /404/", true, 302);
exit();
}
?>

View file

@ -1,71 +0,0 @@
<?php
use Kaoken\MarkdownIt\MarkdownIt;
use Kaoken\MarkdownIt\Plugins\MarkdownItFootnote;
function truncateText($text, $limit = 50, $ellipsis = "...")
{
if (mb_strwidth($text, "UTF-8") <= $limit) return $text;
$truncated = mb_substr($text, 0, $limit, "UTF-8");
$lastSpace = mb_strrpos($truncated, " ", 0, "UTF-8");
if ($lastSpace !== false) $truncated = mb_substr($truncated, 0, $lastSpace, "UTF-8");
return $truncated . $ellipsis;
}
function parseMarkdown($markdown)
{
if (empty($markdown)) return '';
$md = new MarkdownIt([
"html" => true,
"linkify" => true,
]);
$md->plugin(new MarkdownItFootnote());
return $md->render($markdown);
}
function parseCountryField($countryField) {
if (empty($countryField)) return null;
$delimiters = [',', '/', '&', ' and '];
$countries = [$countryField];
foreach ($delimiters as $delimiter) {
$tempCountries = [];
foreach ($countries as $country) {
$tempCountries = array_merge($tempCountries, explode($delimiter, $country));
}
$countries = $tempCountries;
}
$countries = array_map('trim', $countries);
$countries = array_map('getCountryName', $countries);
$countries = array_filter($countries);
return implode(', ', array_unique($countries));
}
function getCountryName($countryName) {
$isoCodes = new \Sokil\IsoCodes\IsoCodesFactory();
$countries = $isoCodes->getCountries();
$country = $countries->getByAlpha2($countryName);
if ($country) return $country->getName();
return ucfirst(strtolower($countryName));
}
function pluralize($count, $string, $trailing = '') {
if ((int)$count === 1) return $string;
return $string . 's' . ($trailing ? $trailing : '');
}
?>

View file

@ -1,14 +0,0 @@
<?php
function renderTags(array $tags): void {
if (empty($tags)) return;
echo '<div class="tags">';
foreach ($tags as $tag) {
$slug = strtolower(trim($tag));
echo '<a href="/tags/' . rawurlencode($slug) . '">#' . htmlspecialchars($slug) . '</a>';
}
echo '</div>';
}

View file

@ -1,41 +0,0 @@
class NowPlaying extends HTMLElement {
static tagName = "now-playing";
static register(tagName = this.tagName, registry = globalThis.customElements) {
registry.define(tagName, this);
}
async connectedCallback() {
this.contentElement = this.querySelector(".content");
if (!this.contentElement) return;
const cache = localStorage.getItem("now-playing-cache");
if (cache) this.updateHTML(JSON.parse(cache));
await this.fetchAndUpdate();
}
async fetchAndUpdate() {
try {
const data = await this.fetchData();
const newHTML = data?.content;
if (newHTML && newHTML !== this.contentElement.innerHTML) {
this.updateHTML(newHTML);
localStorage.setItem("now-playing-cache", JSON.stringify(newHTML));
}
} catch {}
}
updateHTML(value) {
this.contentElement.innerHTML = value;
}
async fetchData() {
return fetch(`/api/playing.php?nocache=${Date.now()}`)
.then(response => response.json())
.catch(() => ({}));
}
}
NowPlaying.register();

View file

@ -7,7 +7,6 @@ const staticAssets = [
'/assets/fonts/dmi.woff2', '/assets/fonts/dmi.woff2',
'/assets/fonts/ml.woff2', '/assets/fonts/ml.woff2',
'/assets/scripts/index.js', '/assets/scripts/index.js',
'/assets/scripts/components/now-playing.js',
'/assets/scripts/components/select-pagination.js', '/assets/scripts/components/select-pagination.js',
]; ];

View file

@ -501,10 +501,4 @@ footer {
footer { footer {
margin: var(--sizing-3xl) auto 0; margin: var(--sizing-3xl) auto 0;
.updated {
font-size: var(--font-size-sm);
text-align: center;
margin-top: 0;
}
} }

View file

@ -1,42 +0,0 @@
import EleventyFetch from "@11ty/eleventy-fetch";
const { POSTGREST_URL, POSTGREST_API_KEY } = process.env;
const fetchLatestListen = async () => {
try {
const data = await EleventyFetch(
`${POSTGREST_URL}/optimized_latest_listen?select=*`,
{
duration: "1h",
type: "json",
fetchOptions: {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${POSTGREST_API_KEY}`,
},
},
},
);
const trackData = data[0];
if (!trackData) {
return { content: "🎧 No recent listens found" };
}
const emoji = trackData.artist_emoji || trackData.genre_emoji || "🎧";
return {
content: `${emoji} ${
trackData.track_name
} by <a href="https://www.coryd.dev${trackData.url}">${trackData.artist_name}</a>`,
};
} catch (error) {
console.error("Error fetching the latest listen:", error);
return {};
}
};
export default async function () {
return await fetchLatestListen();
}

View file

@ -1,33 +0,0 @@
import EleventyFetch from "@11ty/eleventy-fetch";
const { POSTGREST_URL, POSTGREST_API_KEY } = process.env;
const fetchRecentMedia = async () => {
try {
const data = await EleventyFetch(
`${POSTGREST_URL}/optimized_recent_media?select=*`,
{
duration: "1h",
type: "json",
fetchOptions: {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${POSTGREST_API_KEY}`,
},
},
},
);
const [{ recent_activity } = {}] = data;
return recent_activity || [];
} catch (error) {
console.error("Error fetching recent media data:", error);
return [];
}
};
export default async function () {
return await fetchRecentMedia();
}

View file

@ -1,7 +0,0 @@
<script type="module" src="/assets/scripts/components/now-playing.js?v={% appVersion %}"></script>
<p class="{{ section }}">
<mark>Now playing</mark>&puncsp;
<now-playing>
<span class="content">{{ nowPlaying }}</span>
</now-playing>
</p>

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../../vendor/autoload.php"; require_once __DIR__ . '/../../bootstrap.php';
require __DIR__ . "/../../server/utils/init.php";
use App\Classes\ArtistFetcher; use App\Classes\ArtistFetcher;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../../vendor/autoload.php"; require_once __DIR__ . '/../../bootstrap.php';
require __DIR__ . "/../../server/utils/init.php";
use App\Classes\BookFetcher; use App\Classes\BookFetcher;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../../vendor/autoload.php"; require_once __DIR__ . '/../../bootstrap.php';
require __DIR__ . "/../../server/utils/init.php";
use App\Classes\GenreFetcher; use App\Classes\GenreFetcher;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../../vendor/autoload.php"; require_once __DIR__ . '/../../bootstrap.php';
require __DIR__ . "/../../server/utils/init.php";
use App\Classes\MovieFetcher; use App\Classes\MovieFetcher;

View file

@ -1,7 +1,6 @@
<?php <?php
require __DIR__ . "/../../vendor/autoload.php"; require_once __DIR__ . '/../../bootstrap.php';
require __DIR__ . "/../../server/utils/init.php";
use App\Classes\ShowFetcher; use App\Classes\ShowFetcher;

View file

@ -1,7 +1,7 @@
<?php <?php
require __DIR__ . "/../vendor/autoload.php"; require __DIR__ . "/../../vendor/autoload.php";
require __DIR__ . "/../server/utils/init.php"; require __DIR__ . "/../../config/dynamic/init.php";
use App\Classes\TagFetcher; use App\Classes\TagFetcher;
use App\Classes\GlobalsFetcher; use App\Classes\GlobalsFetcher;

View file

@ -0,0 +1,15 @@
<?php
use App\Classes\LatestListenHandler;
$handler = new LatestListenHandler();
$data = $handler->getLatestListen();
if (!empty($data['content'])):
?>
<p class="now-playing">
<mark>Now playing</mark>&puncsp;
<span class="content"><?= $data['content'] ?></span>
</p>
<?php endif; ?>

View file

@ -0,0 +1,15 @@
<?php
use App\Classes\RecentMediaHandler;
$handler = new RecentMediaHandler();
$media = $handler->getRecentMedia();
if (!empty($media['recentMusic'])) echo renderMediaGrid($media['recentMusic'], 0, 'eager');
if (!empty($media['recentWatchedRead'])) echo renderMediaGrid($media['recentWatchedRead'], 0, 'eager');
?>
{% render "static/blocks/banners/rss.liquid",
url:"/feeds",
text:"Subscribe to my posts, movies, books, links or activity feed(s)"
%}

View file

@ -0,0 +1,41 @@
<?php if (!empty($music['recent_tracks'])): ?>
<div class="music-chart">
<?php foreach (array_slice($music['recent_tracks'], 0, 10) as $item): ?>
<?php $chart = $item['chart'] ?? []; ?>
<div class="chart-item">
<div class="meta">
<a href="<?= htmlspecialchars($chart['url'] ?? '#') ?>">
<img
srcset="
<?= htmlspecialchars($chart['image']) ?>?class=w50&type=webp 50w,
<?= htmlspecialchars($chart['image']) ?>?class=w100&type=webp 100w
"
sizes="(max-width: 450px) 50px, 100px"
src="<?= htmlspecialchars($chart['image']) ?>?class=w50&type=webp"
alt="<?= htmlspecialchars($chart['alt'] ?? '') ?>"
loading="lazy"
decoding="async"
width="64"
height="64"
>
</a>
<div class="meta-text">
<a class="title" href="<?= htmlspecialchars($chart['url'] ?? '#') ?>">
<?= htmlspecialchars($chart['title'] ?? '') ?>
</a>
<span class="subheader"><?= htmlspecialchars($chart['subtext'] ?? '') ?></span>
</div>
</div>
<?php if (!empty($chart['played_at'])): ?>
<?php
$timestamp = (int) $chart['played_at'];
$playedAt = (new DateTime("@$timestamp"))->setTimezone(new DateTimeZone('America/Los_Angeles'));
?>
<time datetime="<?= $playedAt->format('c') ?>">
<?= $playedAt->format('F j, g:ia') ?>
</time>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>

View file

@ -1,15 +0,0 @@
{% render "media/grid.liquid",
globals:globals,
data:media.recentMusic,
loading:"eager"
%}
{% render "media/grid.liquid",
globals:globals,
data:media.recentWatchedRead,
shape:"vertical",
loading:"eager"
%}
{% render "blocks/banners/rss.liquid",
url:"/feeds",
text:"Subscribe to my posts, movies, books, links or activity feed(s)"
%}

View file

@ -1,22 +0,0 @@
{%- assign updateTime = "" -%}
{%- if updated == "now" -%}
{%- assign updateTime = "now" | date:"%B %-d, %l:%M %P", "America/Los_Angeles" -%}
{%- elsif pageUpdated -%}
{%- assign updateTime = page.updated | date:"%B %-d, %l:%M %P", "America/Los_Angeles" -%}
{%- endif -%}
<footer>
{%- if updateTime -%}
<p class="updated"><em>This page was last updated on {{ updateTime | strip }}.</em></p>
{%- endif -%}
{% render "nav/menu.liquid",
page:page,
nav:nav.footer_icons
class:"social"
%}
{% render "nav/menu.liquid",
page:page,
nav:nav.footer_text
class:"sub-pages"
separator:true
%}
</footer>

View file

@ -1,30 +0,0 @@
<div class="music-chart">
{%- for item in data limit:10 -%}
<div class="chart-item">
<div class="meta">
<a href="{{ item.chart.url }}">
<img
srcset="
{{ item.chart.image }}?class=w50&type=webp 50w,
{{ item.chart.image }}?class=w100&type=webp 100w
"
sizes="(max-width: 450px) 50px, 100px"
src="{{ item.chart.image }}?class=w50&type=webp"
alt="{{ item.chart.alt | replaceQuotes }}"
loading="lazy"
decoding="async"
width="64"
height="64"
>
</a>
<div class="meta-text">
<a class="title" href="{{ item.chart.url }}">{{ item.chart.title }}</a>
<span class="subheader">{{ item.chart.subtext }}</span>
</div>
</div>
<time datetime="{{ item.chart.played_at | date: "%Y-%m-%dT%H:%M:%S%:z", "America/Los_Angeles" }}">
{{ item.chart.played_at | date:"%B %-d, %-I:%M%p", "America/Los_Angeles" }}
</time>
</div>
{%- endfor -%}
</div>

View file

@ -35,7 +35,7 @@
{% if key == "books" or key == "movies" or key == "shows" %} {% if key == "books" or key == "movies" or key == "shows" %}
{% assign shape = "vertical" %} {% assign shape = "vertical" %}
{% endif %} {% endif %}
{% render "media/grid.liquid", {% render "static/media/grid.liquid",
data:items, data:items,
shape:shape shape:shape
%} %}

View file

@ -1,22 +1,22 @@
{%- for block in blocks -%} {%- for block in blocks -%}
{%- case block.type -%} {%- case block.type -%}
{%- when "calendar_banner" -%} {%- when "calendar_banner" -%}
{% render "blocks/banners/calendar.liquid", {% render "static/blocks/banners/calendar.liquid",
url:block.url, url:block.url,
text:block.text text:block.text
%} %}
{%- when "divider" -%} {%- when "divider" -%}
{{ block.markup | markdown }} {{ block.markup | markdown }}
{%- when "forgejo_banner" -%} {%- when "forgejo_banner" -%}
{% render "blocks/banners/forgejo.liquid", {% render "static/blocks/banners/forgejo.liquid",
url:block.url url:block.url
%} %}
{%- when "github_banner" -%} {%- when "github_banner" -%}
{% render "blocks/banners/github.liquid", {% render "static/blocks/banners/github.liquid",
url:block.url url:block.url
%} %}
{%- when "hero" -%} {%- when "hero" -%}
{% render "blocks/hero.liquid", {% render "static/blocks/hero.liquid",
globals:globals, globals:globals,
image:block.image, image:block.image,
alt:block.alt alt:block.alt
@ -24,17 +24,17 @@
{%- when "markdown" -%} {%- when "markdown" -%}
{{ block.text | markdown }} {{ block.text | markdown }}
{%- when "npm_banner" -%} {%- when "npm_banner" -%}
{% render "blocks/banners/npm.liquid", {% render "static/blocks/banners/npm.liquid",
url:block.url, url:block.url,
command:block.command command:block.command
%} %}
{%- when "rss_banner" -%} {%- when "rss_banner" -%}
{% render "blocks/banners/rss.liquid", {% render "static/blocks/banners/rss.liquid",
url:block.url, url:block.url,
text:block.text text:block.text
%} %}
{%- when "youtube_player" -%} {%- when "youtube_player" -%}
{% render "blocks/youtube-player.liquid", {% render "static/blocks/youtube-player.liquid",
url:block.url url:block.url
%} %}
{%- endcase -%} {%- endcase -%}

View file

@ -1,7 +1,6 @@
<article class="intro"> <article class="intro">
{{ intro }} {{ intro }}
{% render "blocks/now-playing.liquid", {% render "dynamic/media/now-playing.php.liquid",
nowPlaying:nowPlaying
section:"music" section:"music"
%} %}
<hr /> <hr />

View file

@ -10,7 +10,7 @@
• {{ item.label }} • {{ item.label }}
{%- if item.notes -%} {%- if item.notes -%}
{% assign notes = item.notes | markdown %} {% assign notes = item.notes | markdown %}
{% render "blocks/dialog.liquid", {% render "static/blocks/dialog.liquid",
icon:"info-circle", icon:"info-circle",
label:"View info about this concert" label:"View info about this concert"
dynamic:"optimized_concerts", dynamic:"optimized_concerts",
@ -49,7 +49,7 @@
{%- endif -%} {%- endif -%}
{%- endif -%} {%- endif -%}
</h3> </h3>
{% render "blocks/tags.liquid", {% render "static/blocks/tags.liquid",
tags:item.tags tags:item.tags
%} %}
</article> </article>

View file

@ -0,0 +1,13 @@
<footer>
{% render "static/nav/menu.liquid",
page:page,
nav:nav.footer_icons
class:"social"
%}
{% render "static/nav/menu.liquid",
page:page,
nav:nav.footer_text
class:"sub-pages"
separator:true
%}
</footer>

View file

@ -20,13 +20,13 @@
<a href="/" tabindex="0">{{ headerContent }}</a> <a href="/" tabindex="0">{{ headerContent }}</a>
{%- endif -%} {%- endif -%}
</h1> </h1>
{% render "nav/menu.liquid", {% render "static/nav/menu.liquid",
page:page, page:page,
nav:nav.primary_icons nav:nav.primary_icons
class:"icons" class:"icons"
%} %}
</section> </section>
{% render "nav/menu.liquid", {% render "static/nav/menu.liquid",
page:page, page:page,
nav:nav.primary nav:nav.primary
class:"primary" class:"primary"

View file

@ -42,6 +42,6 @@
</a> </a>
{%- endfor -%} {%- endfor -%}
</div> </div>
{% render "nav/paginator.liquid", {% render "static/nav/paginator.liquid",
pagination:pagination pagination:pagination
%} %}

View file

@ -6,7 +6,7 @@
<span class="subheader">{{ item.chart.plays }} {{ playsLabel }}</span> <span class="subheader">{{ item.chart.plays }} {{ playsLabel }}</span>
</div> </div>
<div class="chart-item-progress"> <div class="chart-item-progress">
{% render "media/progress-bar.liquid", {% render "static/media/progress-bar.liquid",
percentage:item.chart.percentage percentage:item.chart.percentage
%} %}
</div> </div>

View file

@ -3,7 +3,7 @@
{%- if count -%} {%- if count -%}
{%- for item in data limit:count -%} {%- for item in data limit:count -%}
<li value="{{ item.chart.rank }}"> <li value="{{ item.chart.rank }}">
{% render "media/music/charts/item.liquid", {% render "static/media/music/charts/item.liquid",
item:item item:item
%} %}
</li> </li>
@ -11,7 +11,7 @@
{%- else -%} {%- else -%}
{%- for item in pagination.items -%} {%- for item in pagination.items -%}
<li value="{{ item.chart.rank }}"> <li value="{{ item.chart.rank }}">
{% render "media/music/charts/item.liquid", {% render "static/media/music/charts/item.liquid",
item:item item:item
%} %}
</li> </li>
@ -19,6 +19,6 @@
{%- endif -%} {%- endif -%}
</ol> </ol>
</div> </div>
{% render "nav/paginator.liquid", {% render "static/nav/paginator.liquid",
pagination:pagination pagination:pagination
%} %}

View file

@ -9,7 +9,7 @@
({{ movie.year }}) ({{ movie.year }})
</div> </div>
</div> </div>
{% render "blocks/hero.liquid", {% render "static/blocks/hero.liquid",
globals:globals, globals:globals,
image:movie.backdrop, image:movie.backdrop,
alt:movie.title alt:movie.title

View file

@ -2,7 +2,7 @@
{%- assign source = page -%} {%- assign source = page -%}
{%- case schema -%} {%- case schema -%}
{%- when 'artist', 'genre', 'book', 'movie', 'show', 'tags' -%} {%- when 'artist', 'genre', 'book', 'movie', 'show', 'tags' -%}
{% render "fetchers/{{ schema }}.php.liquid" %} {% render "dynamic/fetchers/{{ schema }}.php.liquid" %}
{%- when 'blog' -%} {%- when 'blog' -%}
{%- assign source = post -%} {%- assign source = post -%}
{%- when 'music-index', 'music-week-artists' -%} {%- when 'music-index', 'music-week-artists' -%}
@ -37,7 +37,7 @@
{%- assign fullUrl = meta.url -%} {%- assign fullUrl = meta.url -%}
{%- assign oembedUrl = globals.url | append: "/oembed" | append: page.url -%} {%- assign oembedUrl = globals.url | append: "/oembed" | append: page.url -%}
{%- if type == 'dynamic' -%} {%- if type == 'dynamic' -%}
{% render "metadata/dynamic.php.liquid" {% render "dynamic/metadata/index.php.liquid"
fullUrl: fullUrl, fullUrl: fullUrl,
oembedUrl: oembedUrl, oembedUrl: oembedUrl,
pageTitle: meta.title, pageTitle: meta.title,
@ -46,7 +46,7 @@
globals: globals, globals: globals,
%} %}
{%- else -%} {%- else -%}
{% render "metadata/static.liquid" {% render "static/metadata/static.liquid"
fullUrl: fullUrl, fullUrl: fullUrl,
oembedUrl: oembedUrl, oembedUrl: oembedUrl,
pageTitle: meta.title, pageTitle: meta.title,
@ -55,7 +55,7 @@
globals: globals, globals: globals,
%} %}
{%- endif %} {%- endif %}
{% render "metadata/base.liquid" {% render "static/metadata/base.liquid"
pageTitle: meta.title, pageTitle: meta.title,
globals: globals, globals: globals,
eleventy: eleventy, eleventy: eleventy,

View file

@ -1,6 +1,7 @@
{%- assign categoryUrl = link.permalink | downcase -%} {%- assign categoryUrl = link.permalink | downcase -%}
{%- assign isHttp = categoryUrl contains "http" -%} {%- assign isHttp = categoryUrl contains "http" -%}
{%- if categoryUrl | isLinkActive:page.url -%} {%- assign url = page.activeUrl | default: page.url -%}
{%- if categoryUrl | isLinkActive:url -%}
{%- capture linkClass -%} {%- capture linkClass -%}
{%- if link.section -%}button{%- endif -%} {%- if link.section -%}button{%- endif -%}
{%- if link.icon -%}icon{%- endif -%} {%- if link.icon -%}icon{%- endif -%}

View file

@ -1,6 +1,6 @@
<nav class="{{ class }}"> <nav class="{{ class }}">
{%- for link in nav -%} {%- for link in nav -%}
{% render "nav/link.liquid", {% render "static/nav/link.liquid",
page:page, page:page,
link:link link:link
%} %}

Some files were not shown because too many files have changed in this diff Show more