Files
deepseek fff7a4b9b9 GG.deals clone: CodeIgniter 4 + MongoDB game price tracker
- Full gg.deals-style UI: deal feed with filters, game pages with price
  history charts, rankings, vouchers, news, prepaids, subscriptions,
  bundles, franchises, wishlist, API docs
- Real data crawlers:
  * gg:crawl (Steam Store API + GOG Catalog + Epic free games)
  * gg:catalog (252K game slugs from gg.deals sitemaps)
  * gg:ggdeals (per-game prices via FlareSolverr session, JSON-LD parsing)
  * gg:seed / gg:crawl-covers (demo seed + Steam CDN cover art)
- 24/7 self-updating scheduler (scripts/scheduler_loop.php, lockfile-protected)
- Tailwind v4 dark/light theme, Alpine.js, Chart.js
2026-08-23 07:06:02 +00:00

111 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* Price formatter.
*/
function money(float|int|null $v): string
{
if ($v === null) {
return '—';
}
return '$' . number_format((float) $v, 2);
}
/**
* Discount badge class: bigger cut => hotter color.
*/
function cut_class(int $cut): string
{
return $cut >= 75 ? 'cut-hi' : ($cut >= 50 ? 'cut-mid' : 'cut-lo');
}
/**
* Deal rating label comparing current price to the all-time low.
*/
function deal_rating(float $hl, float $price): array
{
$ratio = $price > 0 ? $hl / $price : 0;
return match (true) {
$ratio >= 0.99 => ['label' => '🔥 Hot', 'cls' => 'rating-best', 'ratio' => $ratio],
$ratio >= 0.85 => ['label' => '🔥 Hot', 'cls' => 'rating-great', 'ratio' => $ratio],
$ratio >= 0.6 => ['label' => 'Good deal', 'cls' => 'rating-good', 'ratio' => $ratio],
$ratio >= 0.35 => ['label' => 'Fair deal', 'cls' => 'rating-fair', 'ratio' => $ratio],
default => ['label' => 'High price', 'cls' => 'rating-high', 'ratio' => $ratio],
};
}
/**
* Build a URL like the current one with query params overridden.
*/
function qs_url(array $overrides = [], array $remove = []): string
{
$get = service('request')->getGet();
$get = array_map(static fn ($v) => is_array($v) ? array_values(array_filter($v, 'strlen')) : $v, $get);
foreach ($remove as $key) {
unset($get[$key]);
}
$get = array_merge($get, $overrides);
// drop empty / zero-default params for clean URLs
foreach ($get as $key => $val) {
if (is_array($val)) {
if ($val === []) unset($get[$key]);
continue;
}
if ($key === 'min_cut' && (int) $val === 0) { unset($get[$key]); continue; }
if ($key === 'kind' && $val === '') { unset($get[$key]); continue; }
if ($key === 'page' && ($val === null || (int) $val === 1)) { unset($get[$key]); continue; }
if (($key === 'min_price' || $key === 'max_price' || $key === 'q') && $val === '') { unset($get[$key]); continue; }
}
$qs = http_build_query($get);
// keep the current path (e.g. /games/rpg) so filters stay within it
$path = '/' . ltrim(uri_string(), '/');
return $path . ($qs !== '' ? '?' . $qs : '');
}
function store_initials(string $name): string
{
$words = preg_split('/\s+/', $name) ?: [];
$ini = '';
foreach ($words as $w) {
if ($w === '' || ! ctype_alpha($w[0])) {
continue;
}
$ini .= strtoupper($w[0]);
if (strlen($ini) >= 2) {
break;
}
}
return $ini !== '' ? $ini : strtoupper(substr($name, 0, 2));
}
function time_ago(int|string|MongoDB\BSON\UTCDateTime $ts): string
{
if ($ts instanceof MongoDB\BSON\UTCDateTime) {
$ts = $ts->toDateTime()->getTimestamp();
}
$diff = max(1, time() - (int) $ts);
if ($diff < 3600) return floor($diff / 60) . ' min ago';
if ($diff < 86400) return floor($diff / 3600) . ' h ago';
if ($diff < 86400*30) return floor($diff / 86400) . ' days ago';
if ($diff < 86400*365) return floor($diff / (86400*30)) . ' months ago';
return floor($diff / (86400*365)) . ' y ago';
}
function bson_date($ts): ?string
{
if (! $ts instanceof MongoDB\BSON\UTCDateTime) {
return null;
}
return date('M j, Y', $ts->toDateTime()->getTimestamp());
}