Files
drop/server/utils/ageRatings.ts
T
AgentScrubblesandRobert Clabough 26abc584a9 Set Age Restrictions on library with a slider (#468)
* Adding groups logic and restrictions

* Cleaning up some redundant code

* hastily made translations

* Linter issues

* More linting

* PR comments

* Covering other APIs with age filters per comment

* Add ages to age ratings

* Making the linter happy

---------

Co-authored-by: Robert Clabough <robert@clabough.tech>
2026-08-06 16:13:43 +10:00

142 lines
4.2 KiB
TypeScript

import { AgeRatingOrganization } from "~/prisma/client/enums";
/**
* This file will live as the known ratings that come from various sources. Each source reports a bit differently, the goal
* will be to normalize to these ratings. Admins of course can override and set a separate rating if needed.
*
* These will change, but historically very infrequently.
*
* Each rating has a `value` (the string stored in the database) and an `age` (the minimum age for that rating).
*/
interface RatingDef {
readonly value: string;
readonly age: number;
}
export const ESRBRating = {
EC: { value: "EC", age: 3 },
E: { value: "E", age: 6 },
E10: { value: "E10", age: 10 },
T: { value: "T", age: 13 },
M: { value: "M", age: 17 },
AO: { value: "AO", age: 18 },
} as const;
export const PEGIRating = {
"3": { value: "3", age: 3 },
"7": { value: "7", age: 7 },
"12": { value: "12", age: 12 },
"16": { value: "16", age: 16 },
"18": { value: "18", age: 18 },
} as const;
export const CEROrating = {
A: { value: "A", age: 0 },
B: { value: "B", age: 12 },
C: { value: "C", age: 15 },
D: { value: "D", age: 17 },
Z: { value: "Z", age: 18 },
} as const;
export const USKRating = {
"0": { value: "0", age: 0 },
"6": { value: "6", age: 6 },
"12": { value: "12", age: 12 },
"16": { value: "16", age: 16 },
"18": { value: "18", age: 18 },
} as const;
export const GRACRating = {
ALL: { value: "ALL", age: 0 },
"12": { value: "12", age: 12 },
"15": { value: "15", age: 15 },
"18": { value: "18", age: 18 },
} as const;
export const ClassIndRating = {
L: { value: "L", age: 0 },
"10": { value: "10", age: 10 },
"12": { value: "12", age: 12 },
"14": { value: "14", age: 14 },
"16": { value: "16", age: 16 },
"18": { value: "18", age: 18 },
} as const;
export const ACBRating = {
G: { value: "G", age: 0 },
PG: { value: "PG", age: 8 },
M: { value: "M", age: 15 },
MA15: { value: "MA15", age: 15 },
R18: { value: "R18", age: 18 },
RC: { value: "RC", age: 18 },
} as const;
export const RATINGS_FOR_ORGANIZATION = {
[AgeRatingOrganization.ESRB]: ESRBRating,
[AgeRatingOrganization.PEGI]: PEGIRating,
[AgeRatingOrganization.CERO]: CEROrating,
[AgeRatingOrganization.USK]: USKRating,
[AgeRatingOrganization.GRAC]: GRACRating,
[AgeRatingOrganization.ClassInd]: ClassIndRating,
[AgeRatingOrganization.ACB]: ACBRating,
} as const satisfies Record<AgeRatingOrganization, Record<string, RatingDef>>;
export function getAvailableRatings(org: AgeRatingOrganization): string[] {
return Object.values(RATINGS_FOR_ORGANIZATION[org]).map((r) => r.value);
}
/**
* Given a maximum allowed age, returns all org:rating pairs that exceed it (should be banned).
*/
export function getBannedRatingsForMaxAge(
maxAge: number,
): Array<{ organization: AgeRatingOrganization; rating: string }> {
const banned: Array<{ organization: AgeRatingOrganization; rating: string }> =
[];
for (const org of Object.values(AgeRatingOrganization)) {
const ratings = RATINGS_FOR_ORGANIZATION[org];
for (const def of Object.values(ratings)) {
if (def.age > maxAge) {
banned.push({ organization: org, rating: def.value });
}
}
}
return banned;
}
/**
* Given a set of banned ratings, infer what max age slider position would produce them.
* Returns null if the set doesn't match any clean slider threshold (manual overrides).
*/
export function inferMaxAgeFromBannedRatings(
banned: Array<{ organization: string; rating: string }>,
): number | null {
if (banned.length === 0) return 18;
// Try each possible age and check if the banned set matches exactly
const possibleAges = new Set<number>();
for (const orgRatings of Object.values(RATINGS_FOR_ORGANIZATION)) {
for (const def of Object.values(orgRatings)) {
possibleAges.add(def.age);
}
}
const bannedSet = new Set(banned.map((b) => `${b.organization}:${b.rating}`));
for (const age of Array.from(possibleAges).sort((a, b) => b - a)) {
const expected = getBannedRatingsForMaxAge(age);
const expectedSet = new Set(
expected.map((e) => `${e.organization}:${e.rating}`),
);
if (
expectedSet.size === bannedSet.size &&
[...expectedSet].every((e) => bannedSet.has(e))
) {
return age;
}
}
return null;
}