feat(*): refactor dynamic vs. static structure and distinctions; make additional elements dynamic
This commit is contained in:
parent
7a0b808f24
commit
eaab389cb9
136 changed files with 984 additions and 960 deletions
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
69
app/Classes/LatestListenHandler.php
Normal file
69
app/Classes/LatestListenHandler.php
Normal 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
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
26
app/Classes/MusicDataHandler.php
Normal file
26
app/Classes/MusicDataHandler.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
36
app/Classes/RecentMediaHandler.php
Normal file
36
app/Classes/RecentMediaHandler.php
Normal 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' => []];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
bootstrap.php
Normal file
6
bootstrap.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
define('PROJECT_ROOT', realpath(__DIR__ . '/..'));
|
||||||
|
|
||||||
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
|
require_once PROJECT_ROOT . '/config/dynamic/init.php';
|
64
cli/package-lock.json
generated
64
cli/package-lock.json
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\Classes\\": "api/Classes/"
|
"App\\Classes\\": "app/Classes/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|
22
config/dynamic/icons.php
Normal file
22
config/dynamic/icons.php
Normal 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
config/dynamic/media.php
Normal file
147
config/dynamic/media.php
Normal 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
config/dynamic/metadata.php
Normal file
39
config/dynamic/metadata.php
Normal 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
config/dynamic/paginator.php
Normal file
41
config/dynamic/paginator.php
Normal 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
config/dynamic/routing.php
Normal file
11
config/dynamic/routing.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
if (!function_exists('redirectTo404')) {
|
||||||
|
function redirectTo404(): void
|
||||||
|
{
|
||||||
|
header("Location: /404/", true, 302);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
84
config/dynamic/strings.php
Normal file
84
config/dynamic/strings.php
Normal 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
config/dynamic/tags.php
Normal file
17
config/dynamic/tags.php
Normal 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>';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
82
package-lock.json
generated
|
@ -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"
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
34
queries/views/media/music/week/summary.sql
Normal file
34
queries/views/media/music/week/summary.sql
Normal 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;
|
|
@ -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
|
||||||
|
|
|
@ -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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
function redirectTo404(): void {
|
|
||||||
header("Location: /404/", true, 302);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -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 : '');
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
|
|
@ -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>';
|
|
||||||
}
|
|
|
@ -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();
|
|
|
@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script type="module" src="/assets/scripts/components/now-playing.js?v={% appVersion %}"></script>
|
|
||||||
<p class="{{ section }}">
|
|
||||||
<mark>Now playing</mark> 
|
|
||||||
<now-playing>
|
|
||||||
<span class="content">{{ nowPlaying }}</span>
|
|
||||||
</now-playing>
|
|
||||||
</p>
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
15
src/includes/dynamic/media/now-playing.php.liquid
Normal file
15
src/includes/dynamic/media/now-playing.php.liquid
Normal 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> 
|
||||||
|
<span class="content"><?= $data['content'] ?></span>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
15
src/includes/dynamic/media/recent-media.php.liquid
Normal file
15
src/includes/dynamic/media/recent-media.php.liquid
Normal 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)"
|
||||||
|
%}
|
41
src/includes/dynamic/media/recent-tracks.php.liquid
Normal file
41
src/includes/dynamic/media/recent-tracks.php.liquid
Normal 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; ?>
|
|
@ -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)"
|
|
||||||
%}
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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
|
||||||
%}
|
%}
|
|
@ -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 -%}
|
|
@ -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 />
|
|
@ -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>
|
13
src/includes/static/layout/footer.liquid
Normal file
13
src/includes/static/layout/footer.liquid
Normal 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>
|
|
@ -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"
|
|
@ -42,6 +42,6 @@
|
||||||
</a>
|
</a>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</div>
|
</div>
|
||||||
{% render "nav/paginator.liquid",
|
{% render "static/nav/paginator.liquid",
|
||||||
pagination:pagination
|
pagination:pagination
|
||||||
%}
|
%}
|
|
@ -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>
|
|
@ -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
|
||||||
%}
|
%}
|
|
@ -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
|
|
@ -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,
|
|
@ -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 -%}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue