mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-08-24 23:32:04 +10:00
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:
co-authored by
Robert Clabough
parent
b6b28afc2e
commit
26abc584a9
@@ -702,6 +702,8 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"addBannedRating": "Gesperrte Einstufung hinzufügen",
|
"addBannedRating": "Gesperrte Einstufung hinzufügen",
|
||||||
"addMember": "Benutzer auswählen...",
|
"addMember": "Benutzer auswählen...",
|
||||||
|
"advancedOverrides": "Erweitert: Manuelle Überschreibungen",
|
||||||
|
"applyAgeRestriction": "Altersbeschränkung anwenden",
|
||||||
"bannedRatings": "Gesperrte Alterseinstufungen",
|
"bannedRatings": "Gesperrte Alterseinstufungen",
|
||||||
"createGroup": "Gruppe erstellen",
|
"createGroup": "Gruppe erstellen",
|
||||||
"deleteConfirm": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
"deleteConfirm": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
@@ -709,6 +711,9 @@
|
|||||||
"description": "Benutzergruppen und Altersbeschränkungen verwalten.",
|
"description": "Benutzergruppen und Altersbeschränkungen verwalten.",
|
||||||
"descriptionField": "Beschreibung",
|
"descriptionField": "Beschreibung",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
|
"manualOverrideNote": "Die aktuellen gesperrten Bewertungen entsprechen keiner Standard-Altersschwelle. Sie wurden möglicherweise manuell konfiguriert.",
|
||||||
|
"maximumContentAge": "Altersbeschränkung",
|
||||||
|
"maximumContentAgeDescription": "Legen Sie das maximale Inhaltsalter für Mitglieder dieser Gruppe fest. Bewertungen über diesem Alter werden gesperrt.",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"noBannedRatings": "Keine gesperrten Einstufungen konfiguriert.",
|
"noBannedRatings": "Keine gesperrten Einstufungen konfiguriert.",
|
||||||
@@ -716,6 +721,7 @@
|
|||||||
"oidcManagedNote": "Die Gruppenmitgliedschaft wird von Ihrem OIDC-Anbieter verwaltet. Mitglieder werden bei der Anmeldung automatisch synchronisiert. Um die Gruppenmitgliedschaft zu ändern, aktualisieren Sie die Gruppenzuweisungen Ihres Identitätsanbieters.",
|
"oidcManagedNote": "Die Gruppenmitgliedschaft wird von Ihrem OIDC-Anbieter verwaltet. Mitglieder werden bei der Anmeldung automatisch synchronisiert. Um die Gruppenmitgliedschaft zu ändern, aktualisieren Sie die Gruppenzuweisungen Ihres Identitätsanbieters.",
|
||||||
"removeBannedRating": "Entfernen",
|
"removeBannedRating": "Entfernen",
|
||||||
"removeMember": "Entfernen",
|
"removeMember": "Entfernen",
|
||||||
|
"sliderOverrideWarning": "Das manuelle Hinzufügen oder Entfernen von Bewertungen überschreibt den Schieberegler. Verwenden Sie den obigen Schieberegler, um auf eine Standard-Altersschwelle zurückzusetzen.",
|
||||||
"title": "Benutzergruppen",
|
"title": "Benutzergruppen",
|
||||||
"unratedNote": "Spiele ohne Alterseinstufung werden für Benutzer in dieser Gruppe ebenfalls ausgeblendet."
|
"unratedNote": "Spiele ohne Alterseinstufung werden für Benutzer in dieser Gruppe ebenfalls ausgeblendet."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -501,6 +501,8 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"addBannedRating": "Add forbidden rating",
|
"addBannedRating": "Add forbidden rating",
|
||||||
"addMember": "Pick a scallywag...",
|
"addMember": "Pick a scallywag...",
|
||||||
|
"advancedOverrides": "Advanced: Manual Overrides",
|
||||||
|
"applyAgeRestriction": "Apply age restriction",
|
||||||
"bannedRatings": "Forbidden Ratings",
|
"bannedRatings": "Forbidden Ratings",
|
||||||
"createGroup": "Form a Crew",
|
"createGroup": "Form a Crew",
|
||||||
"deleteConfirm": "Be ye sure ye want to disband this crew? There be no turnin' back!",
|
"deleteConfirm": "Be ye sure ye want to disband this crew? There be no turnin' back!",
|
||||||
@@ -508,6 +510,9 @@
|
|||||||
"description": "Manage yer crew groups and age restrictions on the plunder.",
|
"description": "Manage yer crew groups and age restrictions on the plunder.",
|
||||||
"descriptionField": "Description",
|
"descriptionField": "Description",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
|
"manualOverrideNote": "The current banned ratings don't match a standard age threshold. They may've been manually set by the captain.",
|
||||||
|
"maximumContentAge": "Age Restriction",
|
||||||
|
"maximumContentAgeDescription": "Set the maximum content age fer members of this crew. Ratings above this age be banned, arr!",
|
||||||
"members": "Crew Members",
|
"members": "Crew Members",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"noBannedRatings": "No forbidden ratings set, captain.",
|
"noBannedRatings": "No forbidden ratings set, captain.",
|
||||||
@@ -515,6 +520,7 @@
|
|||||||
"oidcManagedNote": "Crew membership be managed by yer OIDC harbourmaster. Members be synced when they come aboard. To change crew membership, update yer identity provider's assignments.",
|
"oidcManagedNote": "Crew membership be managed by yer OIDC harbourmaster. Members be synced when they come aboard. To change crew membership, update yer identity provider's assignments.",
|
||||||
"removeBannedRating": "Remove",
|
"removeBannedRating": "Remove",
|
||||||
"removeMember": "Cast overboard",
|
"removeMember": "Cast overboard",
|
||||||
|
"sliderOverrideWarning": "Manually addin' or removin' ratings here will override the slider. Use the slider above to reset to a standard age threshold.",
|
||||||
"title": "Crew Groups",
|
"title": "Crew Groups",
|
||||||
"unratedNote": "Unrated plunder will also be hidden from scallywags in this crew."
|
"unratedNote": "Unrated plunder will also be hidden from scallywags in this crew."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -820,6 +820,8 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"addBannedRating": "Add banned rating",
|
"addBannedRating": "Add banned rating",
|
||||||
"addMember": "Select a user...",
|
"addMember": "Select a user...",
|
||||||
|
"advancedOverrides": "Advanced: Manual Overrides",
|
||||||
|
"applyAgeRestriction": "Apply age restriction",
|
||||||
"bannedRatings": "Banned Ratings",
|
"bannedRatings": "Banned Ratings",
|
||||||
"createGroup": "Create Group",
|
"createGroup": "Create Group",
|
||||||
"deleteConfirm": "Are you sure you want to delete this group? This action cannot be undone.",
|
"deleteConfirm": "Are you sure you want to delete this group? This action cannot be undone.",
|
||||||
@@ -827,6 +829,9 @@
|
|||||||
"description": "Manage user groups and age rating restrictions.",
|
"description": "Manage user groups and age rating restrictions.",
|
||||||
"descriptionField": "Description",
|
"descriptionField": "Description",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
|
"manualOverrideNote": "The current banned ratings do not match a standard age threshold. They may have been manually configured.",
|
||||||
|
"maximumContentAge": "Age Restriction",
|
||||||
|
"maximumContentAgeDescription": "Set the maximum content age allowed for members of this group. Ratings above this age will be banned.",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"noBannedRatings": "No banned ratings configured.",
|
"noBannedRatings": "No banned ratings configured.",
|
||||||
@@ -834,6 +839,7 @@
|
|||||||
"oidcManagedNote": "Group membership is managed by your OIDC provider. Members are automatically synced when users log in. To change group membership, update your identity provider's group assignments.",
|
"oidcManagedNote": "Group membership is managed by your OIDC provider. Members are automatically synced when users log in. To change group membership, update your identity provider's group assignments.",
|
||||||
"removeBannedRating": "Remove",
|
"removeBannedRating": "Remove",
|
||||||
"removeMember": "Remove",
|
"removeMember": "Remove",
|
||||||
|
"sliderOverrideWarning": "Manually adding or removing ratings here will override the slider. Use the slider above to reset to a standard age threshold.",
|
||||||
"title": "User Groups",
|
"title": "User Groups",
|
||||||
"unratedNote": "Games without age ratings will also be hidden from users in this group."
|
"unratedNote": "Games without age ratings will also be hidden from users in this group."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -702,6 +702,8 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"addBannedRating": "Ajouter une classification interdite",
|
"addBannedRating": "Ajouter une classification interdite",
|
||||||
"addMember": "Sélectionner un utilisateur...",
|
"addMember": "Sélectionner un utilisateur...",
|
||||||
|
"advancedOverrides": "Avancé : Remplacements manuels",
|
||||||
|
"applyAgeRestriction": "Appliquer la restriction d'âge",
|
||||||
"bannedRatings": "Classifications interdites",
|
"bannedRatings": "Classifications interdites",
|
||||||
"createGroup": "Créer un groupe",
|
"createGroup": "Créer un groupe",
|
||||||
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer ce groupe ? Cette action est irréversible.",
|
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer ce groupe ? Cette action est irréversible.",
|
||||||
@@ -709,6 +711,9 @@
|
|||||||
"description": "Gérer les groupes d'utilisateurs et les restrictions d'âge.",
|
"description": "Gérer les groupes d'utilisateurs et les restrictions d'âge.",
|
||||||
"descriptionField": "Description",
|
"descriptionField": "Description",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
|
"manualOverrideNote": "Les classifications interdites actuelles ne correspondent pas à un seuil d'âge standard. Elles ont peut-être été configurées manuellement.",
|
||||||
|
"maximumContentAge": "Restriction d'âge",
|
||||||
|
"maximumContentAgeDescription": "Définissez l'âge maximum du contenu autorisé pour les membres de ce groupe. Les classifications au-dessus de cet âge seront interdites.",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
"noBannedRatings": "Aucune classification interdite configurée.",
|
"noBannedRatings": "Aucune classification interdite configurée.",
|
||||||
@@ -716,6 +721,7 @@
|
|||||||
"oidcManagedNote": "L'appartenance aux groupes est gérée par votre fournisseur OIDC. Les membres sont automatiquement synchronisés lors de la connexion. Pour modifier l'appartenance aux groupes, mettez à jour les affectations de groupes de votre fournisseur d'identité.",
|
"oidcManagedNote": "L'appartenance aux groupes est gérée par votre fournisseur OIDC. Les membres sont automatiquement synchronisés lors de la connexion. Pour modifier l'appartenance aux groupes, mettez à jour les affectations de groupes de votre fournisseur d'identité.",
|
||||||
"removeBannedRating": "Retirer",
|
"removeBannedRating": "Retirer",
|
||||||
"removeMember": "Retirer",
|
"removeMember": "Retirer",
|
||||||
|
"sliderOverrideWarning": "L'ajout ou la suppression manuelle de classifications remplacera le curseur. Utilisez le curseur ci-dessus pour revenir à un seuil d'âge standard.",
|
||||||
"title": "Groupes d'utilisateurs",
|
"title": "Groupes d'utilisateurs",
|
||||||
"unratedNote": "Les jeux sans classification d'âge seront également masqués pour les utilisateurs de ce groupe."
|
"unratedNote": "Les jeux sans classification d'âge seront également masqués pour les utilisateurs de ce groupe."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -678,6 +678,8 @@
|
|||||||
"groups": {
|
"groups": {
|
||||||
"addBannedRating": "Dodaj zablokowaną kategorię",
|
"addBannedRating": "Dodaj zablokowaną kategorię",
|
||||||
"addMember": "Wybierz użytkownika...",
|
"addMember": "Wybierz użytkownika...",
|
||||||
|
"advancedOverrides": "Zaawansowane: Ręczne zastępowanie",
|
||||||
|
"applyAgeRestriction": "Zastosuj ograniczenie wiekowe",
|
||||||
"bannedRatings": "Zablokowane kategorie wiekowe",
|
"bannedRatings": "Zablokowane kategorie wiekowe",
|
||||||
"createGroup": "Utwórz grupę",
|
"createGroup": "Utwórz grupę",
|
||||||
"deleteConfirm": "Czy na pewno chcesz usunąć tę grupę? Tej akcji nie można cofnąć.",
|
"deleteConfirm": "Czy na pewno chcesz usunąć tę grupę? Tej akcji nie można cofnąć.",
|
||||||
@@ -685,6 +687,9 @@
|
|||||||
"description": "Zarządzaj grupami użytkowników i ograniczeniami wiekowymi.",
|
"description": "Zarządzaj grupami użytkowników i ograniczeniami wiekowymi.",
|
||||||
"descriptionField": "Opis",
|
"descriptionField": "Opis",
|
||||||
"details": "Szczegóły",
|
"details": "Szczegóły",
|
||||||
|
"manualOverrideNote": "Obecne zablokowane oceny nie odpowiadają standardowemu progowi wiekowemu. Mogły zostać skonfigurowane ręcznie.",
|
||||||
|
"maximumContentAge": "Ograniczenie wiekowe",
|
||||||
|
"maximumContentAgeDescription": "Ustaw maksymalny wiek treści dozwolony dla członków tej grupy. Oceny powyżej tego wieku zostaną zablokowane.",
|
||||||
"members": "Członkowie",
|
"members": "Członkowie",
|
||||||
"name": "Nazwa",
|
"name": "Nazwa",
|
||||||
"noBannedRatings": "Brak skonfigurowanych zablokowanych kategorii.",
|
"noBannedRatings": "Brak skonfigurowanych zablokowanych kategorii.",
|
||||||
@@ -692,6 +697,7 @@
|
|||||||
"oidcManagedNote": "Członkostwo w grupach jest zarządzane przez dostawcę OIDC. Członkowie są automatycznie synchronizowani podczas logowania. Aby zmienić członkostwo w grupach, zaktualizuj przypisania grup u dostawcy tożsamości.",
|
"oidcManagedNote": "Członkostwo w grupach jest zarządzane przez dostawcę OIDC. Członkowie są automatycznie synchronizowani podczas logowania. Aby zmienić członkostwo w grupach, zaktualizuj przypisania grup u dostawcy tożsamości.",
|
||||||
"removeBannedRating": "Usuń",
|
"removeBannedRating": "Usuń",
|
||||||
"removeMember": "Usuń",
|
"removeMember": "Usuń",
|
||||||
|
"sliderOverrideWarning": "Ręczne dodawanie lub usuwanie ocen zastąpi suwak. Użyj suwaka powyżej, aby powrócić do standardowego progu wiekowego.",
|
||||||
"title": "Grupy użytkowników",
|
"title": "Grupy użytkowników",
|
||||||
"unratedNote": "Gry bez kategorii wiekowej będą również ukryte przed użytkownikami w tej grupie."
|
"unratedNote": "Gry bez kategorii wiekowej będą również ukryte przed użytkownikami w tej grupie."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
{{ $t("users.admin.groups.addMember") }}
|
{{ $t("users.admin.groups.addMember") }}
|
||||||
</option>
|
</option>
|
||||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
||||||
<option
|
<option
|
||||||
v-for="user in availableUsers"
|
v-for="user in availableUsers"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
@@ -100,6 +100,7 @@
|
|||||||
>
|
>
|
||||||
{{ user.displayName }} ({{ user.username }})
|
{{ user.displayName }} ({{ user.username }})
|
||||||
</option>
|
</option>
|
||||||
|
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="rounded-md bg-blue-600 px-2 py-1 text-sm font-semibold text-white hover:bg-blue-500"
|
class="rounded-md bg-blue-600 px-2 py-1 text-sm font-semibold text-white hover:bg-blue-500"
|
||||||
@@ -111,93 +112,179 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banned Ratings Section -->
|
<!-- Age Restriction Slider Section -->
|
||||||
<div class="mt-8 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
|
<div class="mt-8 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
|
||||||
<h2 class="text-base font-semibold text-zinc-100 mb-4">
|
<h2 class="text-base font-semibold text-zinc-100 mb-4">
|
||||||
{{ $t("users.admin.groups.bannedRatings") }}
|
{{ $t("users.admin.groups.maximumContentAge") }}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p class="text-sm text-zinc-400 mb-4">
|
||||||
|
{{ $t("users.admin.groups.maximumContentAgeDescription") }}
|
||||||
|
</p>
|
||||||
<p class="text-sm text-zinc-400 mb-4">
|
<p class="text-sm text-zinc-400 mb-4">
|
||||||
{{ $t("users.admin.groups.unratedNote") }}
|
{{ $t("users.admin.groups.unratedNote") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="group.bannedAgeRatings.length === 0 && !showAddRating"
|
v-if="hasManualOverrides"
|
||||||
class="text-sm text-zinc-400"
|
class="mb-4 rounded-md bg-yellow-900/30 border border-yellow-700/50 p-3"
|
||||||
>
|
>
|
||||||
{{ $t("users.admin.groups.noBannedRatings") }}
|
<p class="text-sm text-yellow-300">
|
||||||
|
{{ $t("users.admin.groups.manualOverrideNote") }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="max-w-md">
|
||||||
<div
|
<div class="flex items-center justify-between mb-2">
|
||||||
v-for="(br, idx) in group.bannedAgeRatings"
|
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
||||||
:key="br.id"
|
<span class="text-sm font-medium text-zinc-100">
|
||||||
class="flex items-center gap-2"
|
{{ sliderAge }}+
|
||||||
>
|
|
||||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center rounded-full bg-zinc-800 px-2.5 py-0.5 text-sm font-medium text-zinc-100"
|
|
||||||
>
|
|
||||||
{{ br.organization }}: {{ br.rating }}
|
|
||||||
</span>
|
</span>
|
||||||
<button
|
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
||||||
type="button"
|
</div>
|
||||||
class="text-red-400 hover:text-red-300 text-sm"
|
<input
|
||||||
@click="removeRating(idx)"
|
v-model.number="sliderAge"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="18"
|
||||||
|
step="1"
|
||||||
|
class="w-full cursor-pointer accent-blue-600"
|
||||||
|
/>
|
||||||
|
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<div class="flex justify-between text-xs text-zinc-500 mt-1">
|
||||||
|
<span>0</span>
|
||||||
|
<span>6</span>
|
||||||
|
<span>12</span>
|
||||||
|
<span>18</span>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sliderBannedPreview.length > 0" class="mt-4">
|
||||||
|
<p class="text-sm text-zinc-400 mb-2">
|
||||||
|
{{ $t("users.admin.groups.bannedRatings") }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<span
|
||||||
|
v-for="bp in sliderBannedPreview"
|
||||||
|
:key="`${bp.organization}:${bp.rating}`"
|
||||||
|
class="inline-flex items-center rounded-full bg-red-900/30 border border-red-700/50 px-2 py-0.5 text-xs text-red-300"
|
||||||
>
|
>
|
||||||
{{ $t("users.admin.groups.removeBannedRating") }}
|
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||||
</button>
|
{{ bp.organization }}: {{ bp.rating }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="mt-4">
|
||||||
<div v-if="showAddRating" class="mt-4 flex items-center gap-2">
|
<p class="text-sm text-zinc-400">
|
||||||
<select
|
{{ $t("users.admin.groups.noBannedRatings") }}
|
||||||
v-model="newRatingOrg"
|
</p>
|
||||||
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
|
|
||||||
>
|
|
||||||
<option v-for="org in organizations" :key="org" :value="org">
|
|
||||||
{{ org }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
v-model="newRatingValue"
|
|
||||||
:disabled="!newRatingOrg"
|
|
||||||
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
|
|
||||||
>
|
|
||||||
<option v-for="r in availableRatingsForOrg" :key="r" :value="r">
|
|
||||||
{{ r }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-md bg-blue-600 px-2 py-1 text-sm font-semibold text-white hover:bg-blue-500"
|
|
||||||
:disabled="!newRatingOrg || !newRatingValue"
|
|
||||||
@click="addRating"
|
|
||||||
>
|
|
||||||
{{ $t("add") }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="text-zinc-400 hover:text-zinc-300 text-sm"
|
|
||||||
@click="showAddRating = false"
|
|
||||||
>
|
|
||||||
{{ $t("cancel") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="!showAddRating"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="mt-4 text-sm text-blue-400 hover:text-blue-300"
|
class="mt-4 rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500"
|
||||||
@click="showAddRating = true"
|
@click="applySlider"
|
||||||
>
|
>
|
||||||
{{ $t("users.admin.groups.addBannedRating") }}
|
{{ $t("users.admin.groups.applyAgeRestriction") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced: Manual Overrides -->
|
||||||
|
<div class="mt-8 rounded-lg border border-zinc-800 bg-zinc-900 p-6">
|
||||||
|
<details>
|
||||||
|
<summary class="cursor-pointer text-base font-semibold text-zinc-100">
|
||||||
|
{{ $t("users.admin.groups.advancedOverrides") }}
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm text-zinc-400 mb-4">
|
||||||
|
{{ $t("users.admin.groups.sliderOverrideWarning") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="group.bannedAgeRatings.length === 0 && !showAddRating"
|
||||||
|
class="text-sm text-zinc-400"
|
||||||
|
>
|
||||||
|
{{ $t("users.admin.groups.noBannedRatings") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(br, idx) in group.bannedAgeRatings"
|
||||||
|
:key="br.id"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full bg-zinc-800 px-2.5 py-0.5 text-sm font-medium text-zinc-100"
|
||||||
|
>
|
||||||
|
{{ br.organization }}: {{ br.rating }}
|
||||||
|
</span>
|
||||||
|
<!-- eslint-enable @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-red-400 hover:text-red-300 text-sm"
|
||||||
|
@click="removeRating(idx)"
|
||||||
|
>
|
||||||
|
{{ $t("users.admin.groups.removeBannedRating") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showAddRating" class="mt-4 flex items-center gap-2">
|
||||||
|
<select
|
||||||
|
v-model="newRatingOrg"
|
||||||
|
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
|
||||||
|
>
|
||||||
|
<option v-for="org in organizations" :key="org" :value="org">
|
||||||
|
{{ org }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
v-model="newRatingValue"
|
||||||
|
:disabled="!newRatingOrg"
|
||||||
|
class="rounded-md bg-zinc-800 px-2 py-1 text-sm text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 focus:outline-blue-600"
|
||||||
|
>
|
||||||
|
<option v-for="r in availableRatingsForOrg" :key="r" :value="r">
|
||||||
|
{{ r }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-blue-600 px-2 py-1 text-sm font-semibold text-white hover:bg-blue-500"
|
||||||
|
:disabled="!newRatingOrg || !newRatingValue"
|
||||||
|
@click="addRating"
|
||||||
|
>
|
||||||
|
{{ $t("add") }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-zinc-400 hover:text-zinc-300 text-sm"
|
||||||
|
@click="showAddRating = false"
|
||||||
|
>
|
||||||
|
{{ $t("cancel") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!showAddRating"
|
||||||
|
type="button"
|
||||||
|
class="mt-4 text-sm text-blue-400 hover:text-blue-300"
|
||||||
|
@click="showAddRating = true"
|
||||||
|
>
|
||||||
|
{{ $t("users.admin.groups.addBannedRating") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AuthMec } from "~/prisma/client/enums";
|
import type { AuthMec } from "~/prisma/client/enums";
|
||||||
import { getAvailableRatings } from "~/utils/ageRatings";
|
import {
|
||||||
|
getAvailableRatings,
|
||||||
|
getBannedRatingsForMaxAge,
|
||||||
|
inferMaxAgeFromBannedRatings,
|
||||||
|
} from "~/utils/ageRatings";
|
||||||
import { AgeRatingOrganization } from "~/prisma/client/enums";
|
import { AgeRatingOrganization } from "~/prisma/client/enums";
|
||||||
|
|
||||||
useHead({ title: "Edit Group" });
|
useHead({ title: "Edit Group" });
|
||||||
@@ -239,6 +326,18 @@ const newRatingValue = ref("");
|
|||||||
|
|
||||||
const organizations = Object.values(AgeRatingOrganization);
|
const organizations = Object.values(AgeRatingOrganization);
|
||||||
|
|
||||||
|
// Slider state
|
||||||
|
const sliderAge = ref(18);
|
||||||
|
|
||||||
|
const sliderBannedPreview = computed(() =>
|
||||||
|
getBannedRatingsForMaxAge(sliderAge.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasManualOverrides = computed(() => {
|
||||||
|
if (!group.value) return false;
|
||||||
|
return inferMaxAgeFromBannedRatings(group.value.bannedAgeRatings) === null;
|
||||||
|
});
|
||||||
|
|
||||||
const availableRatingsForOrg = computed(() => {
|
const availableRatingsForOrg = computed(() => {
|
||||||
if (!newRatingOrg.value) return [];
|
if (!newRatingOrg.value) return [];
|
||||||
return getAvailableRatings(newRatingOrg.value as AgeRatingOrganization);
|
return getAvailableRatings(newRatingOrg.value as AgeRatingOrganization);
|
||||||
@@ -255,6 +354,10 @@ const fetchGroup = async () => {
|
|||||||
);
|
);
|
||||||
editName.value = group.value.name;
|
editName.value = group.value.name;
|
||||||
editDescription.value = group.value.description;
|
editDescription.value = group.value.description;
|
||||||
|
|
||||||
|
// Initialize slider from existing banned ratings
|
||||||
|
const inferred = inferMaxAgeFromBannedRatings(group.value.bannedAgeRatings);
|
||||||
|
sliderAge.value = inferred ?? 18;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch all users for member add dropdown
|
// Fetch all users for member add dropdown
|
||||||
@@ -323,6 +426,16 @@ const removeMember = async (userId: string) => {
|
|||||||
await fetchGroup();
|
await fetchGroup();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const applySlider = async () => {
|
||||||
|
if (!group.value) return;
|
||||||
|
const bannedRatings = getBannedRatingsForMaxAge(sliderAge.value);
|
||||||
|
await $dropFetch(`/api/v1/admin/groups/${groupId}/ratings`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { bannedRatings },
|
||||||
|
});
|
||||||
|
await fetchGroup();
|
||||||
|
};
|
||||||
|
|
||||||
const addRating = async () => {
|
const addRating = async () => {
|
||||||
if (!group.value || !newRatingOrg.value || !newRatingValue.value) return;
|
if (!group.value || !newRatingOrg.value || !newRatingValue.value) return;
|
||||||
const newRatings = [
|
const newRatings = [
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ const GB_BOARD_TO_ORG: Record<string, AgeRatingOrganization> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function lowercaseNormMap(
|
function lowercaseNormMap(
|
||||||
ratings: Record<string, string>,
|
ratings: Record<string, { value: string; age: number }>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.values(ratings).map((v) => [v.toLowerCase(), v]),
|
Object.values(ratings).map((r) => [r.value.toLowerCase(), r.value]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,14 +127,14 @@ const GB_RATING_NORMALIZE: Record<
|
|||||||
> = {
|
> = {
|
||||||
[AgeRatingOrganization.ESRB]: {
|
[AgeRatingOrganization.ESRB]: {
|
||||||
...lowercaseNormMap(ESRBRating),
|
...lowercaseNormMap(ESRBRating),
|
||||||
"early childhood": ESRBRating.EC,
|
"early childhood": ESRBRating.EC.value,
|
||||||
everyone: ESRBRating.E,
|
everyone: ESRBRating.E.value,
|
||||||
"everyone 10+": ESRBRating.E10,
|
"everyone 10+": ESRBRating.E10.value,
|
||||||
teen: ESRBRating.T,
|
teen: ESRBRating.T.value,
|
||||||
mature: ESRBRating.M,
|
mature: ESRBRating.M.value,
|
||||||
"mature 17+": ESRBRating.M,
|
"mature 17+": ESRBRating.M.value,
|
||||||
"adults only": ESRBRating.AO,
|
"adults only": ESRBRating.AO.value,
|
||||||
"adults only 18+": ESRBRating.AO,
|
"adults only 18+": ESRBRating.AO.value,
|
||||||
},
|
},
|
||||||
[AgeRatingOrganization.PEGI]: lowercaseNormMap(PEGIRating),
|
[AgeRatingOrganization.PEGI]: lowercaseNormMap(PEGIRating),
|
||||||
[AgeRatingOrganization.CERO]: lowercaseNormMap(CEROrating),
|
[AgeRatingOrganization.CERO]: lowercaseNormMap(CEROrating),
|
||||||
@@ -142,9 +142,9 @@ const GB_RATING_NORMALIZE: Record<
|
|||||||
[AgeRatingOrganization.GRAC]: lowercaseNormMap(GRACRating),
|
[AgeRatingOrganization.GRAC]: lowercaseNormMap(GRACRating),
|
||||||
[AgeRatingOrganization.ACB]: {
|
[AgeRatingOrganization.ACB]: {
|
||||||
...lowercaseNormMap(ACBRating),
|
...lowercaseNormMap(ACBRating),
|
||||||
"ma 15+": ACBRating.MA15,
|
"ma 15+": ACBRating.MA15.value,
|
||||||
"r 18+": ACBRating.R18,
|
"r 18+": ACBRating.R18.value,
|
||||||
"refused classification": ACBRating.RC,
|
"refused classification": ACBRating.RC.value,
|
||||||
},
|
},
|
||||||
[AgeRatingOrganization.ClassInd]: {},
|
[AgeRatingOrganization.ClassInd]: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,49 +109,49 @@ const IGDB_CATEGORY_TO_ORG: Record<number, AgeRatingOrganization> = {
|
|||||||
|
|
||||||
const IGDB_RATING_TO_STRING: Record<number, string> = {
|
const IGDB_RATING_TO_STRING: Record<number, string> = {
|
||||||
// PEGI
|
// PEGI
|
||||||
1: PEGIRating["3"],
|
1: PEGIRating["3"].value,
|
||||||
2: PEGIRating["7"],
|
2: PEGIRating["7"].value,
|
||||||
3: PEGIRating["12"],
|
3: PEGIRating["12"].value,
|
||||||
4: PEGIRating["16"],
|
4: PEGIRating["16"].value,
|
||||||
5: PEGIRating["18"],
|
5: PEGIRating["18"].value,
|
||||||
// ESRB
|
// ESRB
|
||||||
7: ESRBRating.EC,
|
7: ESRBRating.EC.value,
|
||||||
8: ESRBRating.E,
|
8: ESRBRating.E.value,
|
||||||
9: ESRBRating.E10,
|
9: ESRBRating.E10.value,
|
||||||
10: ESRBRating.T,
|
10: ESRBRating.T.value,
|
||||||
11: ESRBRating.M,
|
11: ESRBRating.M.value,
|
||||||
12: ESRBRating.AO,
|
12: ESRBRating.AO.value,
|
||||||
// CERO
|
// CERO
|
||||||
13: CEROrating.A,
|
13: CEROrating.A.value,
|
||||||
14: CEROrating.B,
|
14: CEROrating.B.value,
|
||||||
15: CEROrating.C,
|
15: CEROrating.C.value,
|
||||||
16: CEROrating.D,
|
16: CEROrating.D.value,
|
||||||
17: CEROrating.Z,
|
17: CEROrating.Z.value,
|
||||||
// USK
|
// USK
|
||||||
18: USKRating["0"],
|
18: USKRating["0"].value,
|
||||||
19: USKRating["6"],
|
19: USKRating["6"].value,
|
||||||
20: USKRating["12"],
|
20: USKRating["12"].value,
|
||||||
21: USKRating["16"],
|
21: USKRating["16"].value,
|
||||||
22: USKRating["18"],
|
22: USKRating["18"].value,
|
||||||
// GRAC
|
// GRAC
|
||||||
23: GRACRating.ALL,
|
23: GRACRating.ALL.value,
|
||||||
24: GRACRating["12"],
|
24: GRACRating["12"].value,
|
||||||
25: GRACRating["15"],
|
25: GRACRating["15"].value,
|
||||||
26: GRACRating["18"],
|
26: GRACRating["18"].value,
|
||||||
// CLASS_IND
|
// CLASS_IND
|
||||||
28: ClassIndRating.L,
|
28: ClassIndRating.L.value,
|
||||||
29: ClassIndRating["10"],
|
29: ClassIndRating["10"].value,
|
||||||
30: ClassIndRating["12"],
|
30: ClassIndRating["12"].value,
|
||||||
31: ClassIndRating["14"],
|
31: ClassIndRating["14"].value,
|
||||||
32: ClassIndRating["16"],
|
32: ClassIndRating["16"].value,
|
||||||
33: ClassIndRating["18"],
|
33: ClassIndRating["18"].value,
|
||||||
// ACB
|
// ACB
|
||||||
34: ACBRating.G,
|
34: ACBRating.G.value,
|
||||||
35: ACBRating.PG,
|
35: ACBRating.PG.value,
|
||||||
36: ACBRating.M,
|
36: ACBRating.M.value,
|
||||||
37: ACBRating.MA15,
|
37: ACBRating.MA15.value,
|
||||||
38: ACBRating.R18,
|
38: ACBRating.R18.value,
|
||||||
39: ACBRating.RC,
|
39: ACBRating.RC.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://api-docs.igdb.com/?shell#game
|
// https://api-docs.igdb.com/?shell#game
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ const STEAM_RATING_KEY_TO_ORG: Record<string, AgeRatingOrganization> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function lowercaseNormMap(
|
function lowercaseNormMap(
|
||||||
ratings: Record<string, string>,
|
ratings: Record<string, { value: string; age: number }>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.values(ratings).map((v) => [v.toLowerCase(), v]),
|
Object.values(ratings).map((r) => [r.value.toLowerCase(), r.value]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+100
-39
@@ -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.
|
* 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.
|
* 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 = {
|
export const ESRBRating = {
|
||||||
EC: "EC",
|
EC: { value: "EC", age: 3 },
|
||||||
E: "E",
|
E: { value: "E", age: 6 },
|
||||||
E10: "E10",
|
E10: { value: "E10", age: 10 },
|
||||||
T: "T",
|
T: { value: "T", age: 13 },
|
||||||
M: "M",
|
M: { value: "M", age: 17 },
|
||||||
AO: "AO",
|
AO: { value: "AO", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const PEGIRating = {
|
export const PEGIRating = {
|
||||||
"3": "3",
|
"3": { value: "3", age: 3 },
|
||||||
"7": "7",
|
"7": { value: "7", age: 7 },
|
||||||
"12": "12",
|
"12": { value: "12", age: 12 },
|
||||||
"16": "16",
|
"16": { value: "16", age: 16 },
|
||||||
"18": "18",
|
"18": { value: "18", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const CEROrating = {
|
export const CEROrating = {
|
||||||
A: "A",
|
A: { value: "A", age: 0 },
|
||||||
B: "B",
|
B: { value: "B", age: 12 },
|
||||||
C: "C",
|
C: { value: "C", age: 15 },
|
||||||
D: "D",
|
D: { value: "D", age: 17 },
|
||||||
Z: "Z",
|
Z: { value: "Z", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const USKRating = {
|
export const USKRating = {
|
||||||
"0": "0",
|
"0": { value: "0", age: 0 },
|
||||||
"6": "6",
|
"6": { value: "6", age: 6 },
|
||||||
"12": "12",
|
"12": { value: "12", age: 12 },
|
||||||
"16": "16",
|
"16": { value: "16", age: 16 },
|
||||||
"18": "18",
|
"18": { value: "18", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const GRACRating = {
|
export const GRACRating = {
|
||||||
ALL: "ALL",
|
ALL: { value: "ALL", age: 0 },
|
||||||
"12": "12",
|
"12": { value: "12", age: 12 },
|
||||||
"15": "15",
|
"15": { value: "15", age: 15 },
|
||||||
"18": "18",
|
"18": { value: "18", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ClassIndRating = {
|
export const ClassIndRating = {
|
||||||
L: "L",
|
L: { value: "L", age: 0 },
|
||||||
"10": "10",
|
"10": { value: "10", age: 10 },
|
||||||
"12": "12",
|
"12": { value: "12", age: 12 },
|
||||||
"14": "14",
|
"14": { value: "14", age: 14 },
|
||||||
"16": "16",
|
"16": { value: "16", age: 16 },
|
||||||
"18": "18",
|
"18": { value: "18", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const ACBRating = {
|
export const ACBRating = {
|
||||||
G: "G",
|
G: { value: "G", age: 0 },
|
||||||
PG: "PG",
|
PG: { value: "PG", age: 8 },
|
||||||
M: "M",
|
M: { value: "M", age: 15 },
|
||||||
MA15: "MA15",
|
MA15: { value: "MA15", age: 15 },
|
||||||
R18: "R18",
|
R18: { value: "R18", age: 18 },
|
||||||
RC: "RC",
|
RC: { value: "RC", age: 18 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const RATINGS_FOR_ORGANIZATION = {
|
export const RATINGS_FOR_ORGANIZATION = {
|
||||||
@@ -73,8 +80,62 @@ export const RATINGS_FOR_ORGANIZATION = {
|
|||||||
[AgeRatingOrganization.GRAC]: GRACRating,
|
[AgeRatingOrganization.GRAC]: GRACRating,
|
||||||
[AgeRatingOrganization.ClassInd]: ClassIndRating,
|
[AgeRatingOrganization.ClassInd]: ClassIndRating,
|
||||||
[AgeRatingOrganization.ACB]: ACBRating,
|
[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[] {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user