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>
This commit is contained in:
AgentScrubbles
2026-08-06 16:13:43 +10:00
committed by GitHub
co-authored by Robert Clabough
parent b6b28afc2e
commit 26abc584a9
10 changed files with 357 additions and 153 deletions
+100 -39
View File
@@ -5,64 +5,71 @@ import { AgeRatingOrganization } from "~/prisma/client/enums";
* 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: "EC",
E: "E",
E10: "E10",
T: "T",
M: "M",
AO: "AO",
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": "3",
"7": "7",
"12": "12",
"16": "16",
"18": "18",
"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: "A",
B: "B",
C: "C",
D: "D",
Z: "Z",
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": "0",
"6": "6",
"12": "12",
"16": "16",
"18": "18",
"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: "ALL",
"12": "12",
"15": "15",
"18": "18",
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: "L",
"10": "10",
"12": "12",
"14": "14",
"16": "16",
"18": "18",
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: "G",
PG: "PG",
M: "M",
MA15: "MA15",
R18: "R18",
RC: "RC",
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 = {
@@ -73,8 +80,62 @@ export const RATINGS_FOR_ORGANIZATION = {
[AgeRatingOrganization.GRAC]: GRACRating,
[AgeRatingOrganization.ClassInd]: ClassIndRating,
[AgeRatingOrganization.ACB]: ACBRating,
} as const satisfies Record<AgeRatingOrganization, Record<string, string>>;
} as const satisfies Record<AgeRatingOrganization, Record<string, RatingDef>>;
export function getAvailableRatings(org: AgeRatingOrganization): string[] {
return Object.values(RATINGS_FOR_ORGANIZATION[org]);
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;
}