20f4fd7095
The light lavender theme carried its color in decoration -- four colored background blobs, a conic-gradient brand mark, gradient-clipped wordmark, a four-color animated button, a spinning conic empty mark, and a five-color spectrum rail. All of that competed with the only color on screen that carries meaning: which source a story came from. - One indigo-slate ink ramp (bg / surface / line / ink) carries every neutral surface; the only saturated hues are the three sources, used strictly as signals -- filter pill, card spine, source chip, timestamp, feed health. - Rule for every filled state: bright fill, deep ink type. Source tokens kept in sync between :root and View::sourceColor(). - Signature "signal bars": a CSS brand mark of three ascending bars in the source hues, and the header rail re-cut into three wire segments that double as the palette legend. The empty state reuses the mark, unlit and static. - Bootstrap switched to data-bs-theme="dark" with matching form controls, alerts, and focus rings; --ink-faint recalibrated so small text clears WCAG 4.5:1 on every surface. Reduced-motion behavior unchanged. - README documents the two rules next to View::sourceColor().
104 lines
2.4 KiB
PHP
104 lines
2.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
final class View
|
|
{
|
|
public static function e(?string $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
public static function sourceLabel(string $source): string
|
|
{
|
|
return match ($source) {
|
|
'youtube' => 'YouTube',
|
|
'reddit' => 'Reddit',
|
|
'rt' => 'RT',
|
|
default => ucfirst($source),
|
|
};
|
|
}
|
|
|
|
public static function sourceBadge(string $source): string
|
|
{
|
|
return match ($source) {
|
|
'youtube' => 'text-bg-danger',
|
|
'reddit' => 'text-bg-warning',
|
|
'rt' => 'text-bg-dark',
|
|
default => 'text-bg-secondary',
|
|
};
|
|
}
|
|
|
|
public static function sourceColor(string $source): string
|
|
{
|
|
return match ($source) {
|
|
'youtube' => '#ff3b5c',
|
|
'reddit' => '#ff7a2f',
|
|
'rt' => '#1fcb93',
|
|
default => '#f1f3f7',
|
|
};
|
|
}
|
|
|
|
public static function rangeLabel(string $range): string
|
|
{
|
|
return match ($range) {
|
|
'1h' => '1 hour',
|
|
'2h' => '2 hours',
|
|
'6h' => '6 hours',
|
|
'24h' => '24 hours',
|
|
'7d' => '7 days',
|
|
default => 'All time',
|
|
};
|
|
}
|
|
|
|
public static function render(string $template, array $data = []): string
|
|
{
|
|
$path = dirname(__DIR__) . '/templates/' . $template . '.php';
|
|
if (!is_file($path)) {
|
|
throw new \RuntimeException(sprintf('Template "%s" not found.', $template));
|
|
}
|
|
|
|
extract($data, EXTR_SKIP);
|
|
ob_start();
|
|
include $path;
|
|
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
public static function timeAgo(?string $iso): string
|
|
{
|
|
if ($iso === null || $iso === '') {
|
|
return '';
|
|
}
|
|
|
|
$timestamp = strtotime($iso);
|
|
if ($timestamp === false) {
|
|
return '';
|
|
}
|
|
|
|
$diff = time() - $timestamp;
|
|
if ($diff < 60) {
|
|
return 'just now';
|
|
}
|
|
|
|
$units = [
|
|
31536000 => 'y',
|
|
2592000 => 'mo',
|
|
604800 => 'w',
|
|
86400 => 'd',
|
|
3600 => 'h',
|
|
60 => 'm',
|
|
];
|
|
|
|
foreach ($units as $seconds => $label) {
|
|
if ($diff >= $seconds) {
|
|
return floor($diff / $seconds) . $label . ' ago';
|
|
}
|
|
}
|
|
|
|
return 'just now';
|
|
}
|
|
}
|