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
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
<?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());
|
||||
}
|
||||
Reference in New Issue
Block a user