Game profile: granular IGDB platforms and free-game badge
- List IGDB platform names verbatim (Windows, MacOS) instead of coarse buckets; fall back to Steam OS flags and hardware distributors only when IGDB has none - Show a Free badge instead of prices when best price is zero
This commit is contained in:
5 files changed
+74
-54
No files matched your search
@@ -161,6 +161,7 @@ export function GameProfileModal({
|
|||||||
const name = profile?.name ?? fallbackName;
|
const name = profile?.name ?? fallbackName;
|
||||||
const best = profile?.itad?.prices ?? null;
|
const best = profile?.itad?.prices ?? null;
|
||||||
const low = profile?.itad?.historyLow ?? null;
|
const low = profile?.itad?.historyLow ?? null;
|
||||||
|
const isFree = !!best && best.price === 0;
|
||||||
const hero = useMemo(() => {
|
const hero = useMemo(() => {
|
||||||
if (bannerPick) {
|
if (bannerPick) {
|
||||||
return bannerPick;
|
return bannerPick;
|
||||||
@@ -539,38 +540,44 @@ export function GameProfileModal({
|
|||||||
<p className="font-mono text-xs uppercase tracking-wider text-zinc-500">
|
<p className="font-mono text-xs uppercase tracking-wider text-zinc-500">
|
||||||
{dict.prices}
|
{dict.prices}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-1.5 grid gap-2 sm:grid-cols-2">
|
{isFree ? (
|
||||||
{best ? (
|
<p className="mt-1.5 inline-block rounded-full border border-mint/20 bg-mint/5 px-4 py-1.5 text-xs font-semibold text-mint">
|
||||||
<a
|
{dict.free}
|
||||||
href={best.url || undefined}
|
</p>
|
||||||
target="_blank"
|
) : (
|
||||||
rel="noopener noreferrer"
|
<div className="mt-1.5 grid gap-2 sm:grid-cols-2">
|
||||||
className="rounded-xl border border-mint/20 bg-mint/5 p-3 transition-colors hover:border-mint/40"
|
{best ? (
|
||||||
>
|
<a
|
||||||
<p className="font-mono text-xs text-mint">
|
href={best.url || undefined}
|
||||||
{dict.bestPrice} · {best.shop}
|
target="_blank"
|
||||||
</p>
|
rel="noopener noreferrer"
|
||||||
<p className="mt-1 text-lg font-bold">
|
className="rounded-xl border border-mint/20 bg-mint/5 p-3 transition-colors hover:border-mint/40"
|
||||||
{formatPrice(best, tag)}{" "}
|
>
|
||||||
{discountPercent(best) ? (
|
<p className="font-mono text-xs text-mint">
|
||||||
<span className="ml-1 rounded bg-mint/15 px-1.5 py-0.5 align-middle font-mono text-xs text-mint">
|
{dict.bestPrice} · {best.shop}
|
||||||
-%{discountPercent(best)}
|
</p>
|
||||||
</span>
|
<p className="mt-1 text-lg font-bold">
|
||||||
) : null}
|
{formatPrice(best, tag)}{" "}
|
||||||
</p>
|
{discountPercent(best) ? (
|
||||||
</a>
|
<span className="ml-1 rounded bg-mint/15 px-1.5 py-0.5 align-middle font-mono text-xs text-mint">
|
||||||
) : null}
|
-%{discountPercent(best)}
|
||||||
{low ? (
|
</span>
|
||||||
<div className="rounded-xl border border-white/5 bg-white/[0.03] p-3">
|
) : null}
|
||||||
<p className="font-mono text-xs text-zinc-500">
|
</p>
|
||||||
{dict.historyLow} · {low.shop}
|
</a>
|
||||||
</p>
|
) : null}
|
||||||
<p className="mt-1 text-lg font-bold text-zinc-200">
|
{low ? (
|
||||||
{formatPrice(low, tag)}
|
<div className="rounded-xl border border-white/5 bg-white/[0.03] p-3">
|
||||||
</p>
|
<p className="font-mono text-xs text-zinc-500">
|
||||||
</div>
|
{dict.historyLow} · {low.shop}
|
||||||
) : null}
|
</p>
|
||||||
</div>
|
<p className="mt-1 text-lg font-bold text-zinc-200">
|
||||||
|
{formatPrice(low, tag)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
+32
-22
@@ -88,9 +88,7 @@ async function getDetectables(): Promise<DetectableEntry[]> {
|
|||||||
return detectablesCache.data;
|
return detectablesCache.data;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://cdn.discordapp.com/detectables/games-v1.json", {
|
const response = await fetch("https://cdn.discordapp.com/detectables/games-v1.json");
|
||||||
next: { revalidate: 86_400 },
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error(`detectables failed: ${response.status}`);
|
console.error(`detectables failed: ${response.status}`);
|
||||||
return detectablesCache?.data ?? [];
|
return detectablesCache?.data ?? [];
|
||||||
@@ -104,10 +102,6 @@ async function getDetectables(): Promise<DetectableEntry[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DISTRIBUTOR_PLATFORM: Record<string, string> = {
|
const DISTRIBUTOR_PLATFORM: Record<string, string> = {
|
||||||
steam: "PC",
|
|
||||||
battlenet: "PC",
|
|
||||||
gog: "PC",
|
|
||||||
epic: "PC",
|
|
||||||
xbox: "Xbox",
|
xbox: "Xbox",
|
||||||
xbox_title: "Xbox",
|
xbox_title: "Xbox",
|
||||||
xbox_game_pass: "Xbox",
|
xbox_game_pass: "Xbox",
|
||||||
@@ -220,10 +214,16 @@ interface IgdbGame {
|
|||||||
|
|
||||||
let igdbTokenCache: { token: string; expires: number } | null = null;
|
let igdbTokenCache: { token: string; expires: number } | null = null;
|
||||||
|
|
||||||
|
let igdbKeyWarned = false;
|
||||||
|
|
||||||
async function getIgdbToken(): Promise<string | null> {
|
async function getIgdbToken(): Promise<string | null> {
|
||||||
const clientId = process.env.TWITCH_CLIENT_ID;
|
const clientId = process.env.TWITCH_CLIENT_ID;
|
||||||
const clientSecret = process.env.TWITCH_CLIENT_SECRET;
|
const clientSecret = process.env.TWITCH_CLIENT_SECRET;
|
||||||
if (!clientId || !clientSecret) {
|
if (!clientId || !clientSecret) {
|
||||||
|
if (!igdbKeyWarned) {
|
||||||
|
igdbKeyWarned = true;
|
||||||
|
console.error("igdb keys missing: set TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET");
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (igdbTokenCache && Date.now() < igdbTokenCache.expires) {
|
if (igdbTokenCache && Date.now() < igdbTokenCache.expires) {
|
||||||
@@ -386,9 +386,15 @@ interface ItadOverviewPrice {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let itadKeyWarned = false;
|
||||||
|
|
||||||
async function itadFetch<T>(path: string, init?: RequestInit): Promise<T | null> {
|
async function itadFetch<T>(path: string, init?: RequestInit): Promise<T | null> {
|
||||||
const key = process.env.ITAD_API_KEY;
|
const key = process.env.ITAD_API_KEY;
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
if (!itadKeyWarned) {
|
||||||
|
itadKeyWarned = true;
|
||||||
|
console.error("itad key missing: set ITAD_API_KEY");
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -499,24 +505,28 @@ export async function getGameProfile(
|
|||||||
|
|
||||||
const igdbPlatforms = new Set<string>();
|
const igdbPlatforms = new Set<string>();
|
||||||
for (const p of igdb?.platforms ?? []) {
|
for (const p of igdb?.platforms ?? []) {
|
||||||
const label = `${p.abbreviation ?? ""} ${p.name ?? ""}`.toUpperCase();
|
const raw = (p.name ?? "").trim();
|
||||||
if (/PS|PLAYSTATION/.test(label)) {
|
if (!raw) {
|
||||||
igdbPlatforms.add("PlayStation");
|
continue;
|
||||||
} else if (/XBOX|XONE|XSX/.test(label)) {
|
}
|
||||||
igdbPlatforms.add("Xbox");
|
if (raw === "PC (Microsoft Windows)") {
|
||||||
} else if (/PC|WIN|MAC|LIN/.test(label)) {
|
igdbPlatforms.add("Windows");
|
||||||
igdbPlatforms.add("PC");
|
} else if (raw === "Mac") {
|
||||||
} else if (/SWITCH/.test(label)) {
|
igdbPlatforms.add("MacOS");
|
||||||
igdbPlatforms.add("Switch");
|
} else {
|
||||||
|
igdbPlatforms.add(raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const platforms = new Set<string>(igdbPlatforms);
|
const platforms = new Set<string>(igdbPlatforms);
|
||||||
if (platforms.size === 0) {
|
if (platforms.size === 0 && steam?.platforms) {
|
||||||
if (
|
if (steam.platforms.windows) {
|
||||||
steam?.platforms &&
|
platforms.add("Windows");
|
||||||
(steam.platforms.windows || steam.platforms.mac || steam.platforms.linux)
|
}
|
||||||
) {
|
if (steam.platforms.mac) {
|
||||||
platforms.add("PC");
|
platforms.add("MacOS");
|
||||||
|
}
|
||||||
|
if (steam.platforms.linux) {
|
||||||
|
platforms.add("Linux");
|
||||||
}
|
}
|
||||||
for (const sku of entry?.third_party_skus ?? []) {
|
for (const sku of entry?.third_party_skus ?? []) {
|
||||||
const mapped = DISTRIBUTOR_PLATFORM[sku.distributor];
|
const mapped = DISTRIBUTOR_PLATFORM[sku.distributor];
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"links": "Links",
|
"links": "Links",
|
||||||
"reviews": "Bewertungen",
|
"reviews": "Bewertungen",
|
||||||
"prices": "Preise",
|
"prices": "Preise",
|
||||||
|
"free": "Kostenlos",
|
||||||
"bestPrice": "Bester Preis",
|
"bestPrice": "Bester Preis",
|
||||||
"historyLow": "Historischer Tiefstpreis",
|
"historyLow": "Historischer Tiefstpreis",
|
||||||
"allPrices": "ITAD",
|
"allPrices": "ITAD",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"links": "Links",
|
"links": "Links",
|
||||||
"reviews": "Reviews",
|
"reviews": "Reviews",
|
||||||
"prices": "Prices",
|
"prices": "Prices",
|
||||||
|
"free": "Free",
|
||||||
"bestPrice": "Best Price",
|
"bestPrice": "Best Price",
|
||||||
"historyLow": "All-Time Low",
|
"historyLow": "All-Time Low",
|
||||||
"allPrices": "ITAD",
|
"allPrices": "ITAD",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"links": "Bağlantılar",
|
"links": "Bağlantılar",
|
||||||
"reviews": "İncelemeler",
|
"reviews": "İncelemeler",
|
||||||
"prices": "Fiyatlar",
|
"prices": "Fiyatlar",
|
||||||
|
"free": "Ücretsiz",
|
||||||
"bestPrice": "En İyi Fiyat",
|
"bestPrice": "En İyi Fiyat",
|
||||||
"historyLow": "Tarihi En Düşük",
|
"historyLow": "Tarihi En Düşük",
|
||||||
"allPrices": "ITAD",
|
"allPrices": "ITAD",
|
||||||
|
|||||||
Reference in new issue
Block a user