desloppify: code health overhaul plus abuse hardening (140 green, strict 87.2)
Honest baseline after removing 45 gaming suppressions. Structural: lib/game plus root hooks extraction, CORS origin echo, DEV_ORIGINS layering. Fixes: error-cause binding, discord 5xx split, null-cache guard, co-located tests, rejecting-fetch coverage, naming and typing cleanup, socket controller, cache bound, cascade helpers, latch resets. Hardening: per-IP rate limit on API routes, deterministic-miss UX, retry cooldown plus cap. Suite 140 green, build plus lint clean.
This commit is contained in:
78 files changed
+4908
-1430
No files matched your search
@@ -0,0 +1,211 @@
|
||||
import type { GamePricePoint, GameProfile, ItadReview } from "@/lib/game/game-profile-types";
|
||||
import type { Dictionary } from "@/lib/i18n/get-dictionary";
|
||||
import type { Locale } from "@/lib/i18n/locales";
|
||||
|
||||
export type ModalDict = Dictionary["discord"]["gameModal"];
|
||||
|
||||
export const STEAM_SCORE_KEYS: Partial<Record<string, keyof ModalDict["reviewScores"]>> = {
|
||||
"Overwhelmingly Positive": "overwhelminglyPositive",
|
||||
"Very Positive": "veryPositive",
|
||||
Positive: "positive",
|
||||
"Mostly Positive": "mostlyPositive",
|
||||
Mixed: "mixed",
|
||||
"Mostly Negative": "mostlyNegative",
|
||||
Negative: "negative",
|
||||
"Very Negative": "veryNegative",
|
||||
"Overwhelmingly Negative": "overwhelminglyNegative",
|
||||
};
|
||||
|
||||
export const LOCALE_TAGS: Record<Locale, string> = {
|
||||
tr: "tr-TR",
|
||||
en: "en-US",
|
||||
de: "de-DE",
|
||||
};
|
||||
|
||||
export const COUNTRIES: Record<Locale, string> = {
|
||||
tr: "TR",
|
||||
en: "US",
|
||||
de: "DE",
|
||||
};
|
||||
|
||||
export function localizeScore(label: string, dict: ModalDict): string {
|
||||
const key = STEAM_SCORE_KEYS[label];
|
||||
return key ? dict.reviewScores[key] : label;
|
||||
}
|
||||
|
||||
export function formatRatingPercent(value: number, localeTag: string): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(localeTag, {
|
||||
style: "percent",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
} catch {
|
||||
return `%${(value * 100).toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPrice(point: GamePricePoint, localeTag: string): string {
|
||||
try {
|
||||
return new Intl.NumberFormat(localeTag, {
|
||||
style: "currency",
|
||||
currency: point.currency,
|
||||
}).format(point.price);
|
||||
} catch {
|
||||
return `${point.price} ${point.currency}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatReleaseDate(value: string, localeTag: string): string {
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
const parsed = new Date(`${value}T00:00:00`);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return parsed.toLocaleDateString(localeTag, {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function formatCount(value: number | null | undefined, localeTag: string, unit: string): string {
|
||||
if (value == null) {
|
||||
return "—";
|
||||
}
|
||||
return `${value.toLocaleString(localeTag)} ${unit}`;
|
||||
}
|
||||
|
||||
export function discountPercent(point: GamePricePoint): number | null {
|
||||
if (point.regular > point.price && point.regular > 0) {
|
||||
return Math.round((1 - point.price / point.regular) * 100);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function steamdbRating(positive: number, total: number): number | null {
|
||||
if (
|
||||
!Number.isFinite(positive) ||
|
||||
!Number.isFinite(total) ||
|
||||
total <= 0 ||
|
||||
positive < 0 ||
|
||||
positive > total
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const score = positive / total;
|
||||
return score - (score - 0.5) * 2 ** -Math.log10(total + 1);
|
||||
}
|
||||
|
||||
export function toIsoDate(unixSeconds: number | undefined): string | null {
|
||||
if (!unixSeconds) {
|
||||
return null;
|
||||
}
|
||||
return new Date(unixSeconds * 1000).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export interface GameView {
|
||||
gameUrl: string;
|
||||
name: string;
|
||||
bestPrice: GamePricePoint | null;
|
||||
historyLow: GamePricePoint | null;
|
||||
isFree: boolean;
|
||||
}
|
||||
|
||||
export function selectGameView(
|
||||
profile: GameProfile | null,
|
||||
fallbackName: string,
|
||||
fallbackGameUrl: string,
|
||||
): GameView {
|
||||
const bestPrice = profile?.itad?.prices ?? null;
|
||||
const historyLow = profile?.itad?.historyLow ?? null;
|
||||
return {
|
||||
gameUrl: profile?.gameUrl ?? fallbackGameUrl,
|
||||
name: profile?.name || fallbackName,
|
||||
bestPrice,
|
||||
historyLow,
|
||||
isFree: !!bestPrice && bestPrice.price === 0,
|
||||
};
|
||||
}
|
||||
export interface ProfileSummary {
|
||||
dbRating: number | null;
|
||||
itadReviews: ItadReview[];
|
||||
hasDetails: boolean;
|
||||
hasReviews: boolean;
|
||||
metascore: ItadReview | null;
|
||||
opencritic: ItadReview | null;
|
||||
}
|
||||
|
||||
export function describeProfile(profile: GameProfile | null): ProfileSummary {
|
||||
const dbRating =
|
||||
profile?.steamReviews?.positive != null && profile?.steamReviews?.total != null
|
||||
? steamdbRating(profile.steamReviews.positive, profile.steamReviews.total)
|
||||
: null;
|
||||
const itadReviews = profile?.itad?.reviews ?? [];
|
||||
const hasDetails = !!(
|
||||
profile &&
|
||||
(profile.developers.length > 0 ||
|
||||
profile.publishers.length > 0 ||
|
||||
profile.releaseDate ||
|
||||
profile.platforms.length > 0 ||
|
||||
profile.links.length > 0)
|
||||
);
|
||||
const metascore = itadReviews.find((r) => r.source === "Metascore") ?? null;
|
||||
const opencritic = itadReviews.find((r) => r.source === "OpenCritic") ?? null;
|
||||
const hasReviews = !!(profile?.steamReviews || metascore || opencritic);
|
||||
return { dbRating, itadReviews, hasDetails, hasReviews, metascore, opencritic };
|
||||
}
|
||||
|
||||
const DEFAULT_ELAPSED_UNITS = { hour: "h", minute: "m", lessThanMinute: "<1m" };
|
||||
|
||||
const ELAPSED_UNITS: Partial<
|
||||
Record<string, { hour: string; minute: string; lessThanMinute: string }>
|
||||
> = {
|
||||
tr: { hour: "s", minute: "dk", lessThanMinute: "<1dk" },
|
||||
de: { hour: " Std.", minute: " Min.", lessThanMinute: "<1 Min." },
|
||||
en: DEFAULT_ELAPSED_UNITS,
|
||||
};
|
||||
|
||||
export function formatElapsed(start: number, lang: string): string {
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - start) / 1000));
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const units = ELAPSED_UNITS[lang] ?? ELAPSED_UNITS.en ?? DEFAULT_ELAPSED_UNITS;
|
||||
if (hours > 0) {
|
||||
return `${hours}${units.hour} ${minutes}${units.minute}`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}${units.minute}`;
|
||||
}
|
||||
return units.lessThanMinute;
|
||||
}
|
||||
export function normalizeGameTitle(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[®™©]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export interface GameLocale {
|
||||
lang: Locale;
|
||||
country: string;
|
||||
localeTag: string;
|
||||
steamLang: string;
|
||||
}
|
||||
|
||||
const STEAM_LANG: Record<Locale, string> = {
|
||||
tr: "turkish",
|
||||
en: "english",
|
||||
de: "german",
|
||||
};
|
||||
|
||||
export function resolveGameLocale(lang: string): GameLocale {
|
||||
const resolved = lang === "tr" || lang === "en" || lang === "de" ? lang : "en";
|
||||
return {
|
||||
lang: resolved,
|
||||
country: COUNTRIES[resolved],
|
||||
localeTag: LOCALE_TAGS[resolved],
|
||||
steamLang: STEAM_LANG[resolved],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user