Files
ggdeals/app/Views/partials/deal_row.php
T
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

91 lines
5.9 KiB
PHP

<?php
/**
* $row : aggregation row (_id, best_price, best_store, best_kind, best_cut, best_hl, best_expiry, game, deal_rating, ends_within)
* $stores: code => store map
*/
$g = $row['game'];
$store = $stores[$row['best_store']] ?? null;
$hl = $g['all_time_low'];
$hlStore = $stores[$hl['store']] ?? null;
$rating = deal_rating((float) $hl['price'], (float) $row['best_price']);
$year = bson_date($g['release_date']);
$endsWithin = (int) ($row['ends_within'] ?? 0);
$endingSoon = $endsWithin > 0 && $endsWithin < 7 * 86400000;
$endsDays = $endingSoon ? max(1, (int) ceil($endsWithin / 86400000)) : 0;
?>
<div class="group bg-card border border-line rounded-xl hover:border-accent/40 transition-colors">
<div class="flex items-stretch gap-4 p-3">
<a href="/game/<?= $g['slug'] ?>" class="shrink-0 rounded-lg overflow-hidden ring-1 ring-line relative">
<img src="<?= esc($g['cover']['file']) ?>" alt="<?= esc($g['title']) ?>" class="w-[132px] h-[62px] object-cover group-hover:scale-[1.03] transition-transform">
<?php if ($endingSoon): ?>
<span class="absolute top-0.5 right-0.5 text-[9px] font-bold text-amber-300 bg-black/70 rounded px-1 py-0.5">⏳ <?= $endsDays ?>d</span>
<?php endif; ?>
</a>
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex items-center gap-2">
<a href="/game/<?= $g['slug'] ?>" class="block text-[15px] font-semibold text-zinc-100 hover:text-accent truncate"><?= esc($g['title']) ?></a>
<?php if ($row['best_hl']): ?><span class="hl-flag shrink-0">★ New HL</span><?php endif; ?>
<button @click="$store.gg.toggleWishlist('<?= $g['slug'] ?>', $event)"
:class="$store.gg.inWishlist('<?= $g['slug'] ?>') ? 'text-accent' : 'text-muted hover:text-accent'"
class="shrink-0 w-6 h-6 rounded-md bg-elev/60 border border-line/60 flex items-center justify-center transition-colors"
:title="$store.gg.inWishlist('<?= $g['slug'] ?>') ? 'Remove from wishlist' : 'Add to wishlist'">
<svg class="w-3.5 h-3.5" :fill="$store.gg.inWishlist('<?= $g['slug'] ?>') ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
</button>
</div>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
<span class="score-chip" title="Critic score">MC <?= (int) $g['score'] ?></span>
<span><?= esc(substr((string) $year, -4)) ?></span>
<span class="hidden sm:inline"><?= esc(implode(' · ', array_slice($g['genres'], 0, 2))) ?></span>
<span class="hidden lg:inline">
<span class="text-zinc-400"><?= (int) $g['active_deals'] ?></span> offers
</span>
</div>
</div>
<div class="text-right shrink-0">
<div class="flex items-center gap-2 justify-end">
<?php if ((int) $row['best_cut'] > 0): ?>
<span class="cut <?= cut_class((int) $row['best_cut']) ?> cut-badge">-<?= (int) $row['best_cut'] ?>%</span>
<?php endif; ?>
<span class="text-2xl font-extrabold text-accent leading-none"><?= money($row['best_price']) ?></span>
</div>
<div class="mt-1 text-xs text-muted line-through"><?= money($g['base_price']) ?></div>
</div>
</div>
<div class="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs">
<span class="text-muted">
Historical low
<span class="font-semibold text-accent"><?= money($hl['price']) ?></span>
at <span class="text-zinc-300"><?= esc($hlStore['name'] ?? '?') ?></span>
<span class="text-muted/70"><?= time_ago($hl['date']) ?></span>
</span>
<span class="rating <?= $rating['cls'] ?>"><?= $rating['label'] ?></span>
<?php if ($row['best_hl']): ?>
<span class="hl-flag">★ All-time low right now</span>
<?php endif; ?>
</div>
</div>
<div class="hidden xl:flex flex-col justify-center gap-1.5 pl-4 border-l border-line/70 w-[172px]">
<div class="flex items-center justify-between gap-2 text-xs">
<span class="flex items-center gap-1.5 min-w-0">
<?= view('partials/store_badge', ['store' => $stores[$g['best_official']['store']] ?? ['name' => '?', 'color' => '#333', 'textcolor' => '#fff']]) ?>
<span class="text-muted truncate">Official</span>
</span>
<span class="font-bold <?= $row['best_kind'] === 'official' ? 'text-accent' : 'text-zinc-300' ?>"><?= money($g['best_official']['price']) ?></span>
</div>
<div class="flex items-center justify-between gap-2 text-xs">
<span class="flex items-center gap-1.5 min-w-0">
<?= view('partials/store_badge', ['store' => $stores[$g['best_keyshop']['store']] ?? ['name' => '?', 'color' => '#333', 'textcolor' => '#fff']]) ?>
<span class="text-muted truncate">Keyshop</span>
</span>
<span class="font-bold <?= $row['best_kind'] === 'keyshop' ? 'text-accent' : 'text-zinc-300' ?>"><?= money($g['best_keyshop']['price']) ?></span>
</div>
</div>
</div>
</div>