Notification for canceling scrim

This commit is contained in:
Kalle
2025-06-08 10:12:03 +03:00
parent 92090ecc9d
commit a1ff02cfad
38 changed files with 178 additions and 7 deletions

View File

@@ -9,6 +9,8 @@
- `npm run typecheck` runs TypeScript type checking
- `npm run biome:fix` runs Biome code formatter and linter
- `npm run test:unit` runs all unit tests
- `npm run i18n:sync` syncs translation jsons with English and should always be run after adding new text to an English translation file
## Typescript
@@ -46,7 +48,12 @@
- down migrations are not needed, only up migrations
- every database id is of type number
## Playwright
## E2E testing
- library used for E2E testing is Playwright
- `page.goto` is forbidden, use the `navigate` function to do a page navigation
- to submit a form you use the `submit` function
## Unit testing
- library used for unit testing is Vitest

View File

@@ -62,7 +62,8 @@ export type Notification =
>
| NotificationItem<"SEASON_STARTED", { seasonNth: number }>
| NotificationItem<"SCRIM_NEW_REQUEST", { fromUsername: string }>
| NotificationItem<"SCRIM_SCHEDULED", { id: number; at: number }>;
| NotificationItem<"SCRIM_SCHEDULED", { id: number; at: number }>
| NotificationItem<"SCRIM_CANCELED", { id: number; at: number }>;
type NotificationItem<
T extends string,

View File

@@ -35,6 +35,7 @@ export const notificationNavIcon = (type: Notification["type"]) => {
return "medal";
case "SCRIM_NEW_REQUEST":
case "SCRIM_SCHEDULED":
case "SCRIM_CANCELED":
return "scrims";
default:
assertUnreachable(type);
@@ -78,6 +79,7 @@ export const notificationLink = (notification: Notification) => {
case "SCRIM_NEW_REQUEST": {
return scrimsPage();
}
case "SCRIM_CANCELED":
case "SCRIM_SCHEDULED": {
return scrimPage(notification.meta.id);
}
@@ -91,7 +93,10 @@ export const mapMetaForTranslation = (
notification: Notification,
language: string,
) => {
if (notification.type === "SCRIM_SCHEDULED") {
if (
notification.type === "SCRIM_SCHEDULED" ||
notification.type === "SCRIM_CANCELED"
) {
return {
...notification.meta,
timeString: notification.meta.at // TODO: after two weeks this check can be removed (all notifications will have `at`)

View File

@@ -1,4 +1,5 @@
import type { ActionFunctionArgs } from "@remix-run/node";
import { notify } from "~/features/notifications/core/notify.server";
import { requirePermission } from "~/modules/permissions/guards.server";
import {
notFoundIfFalsy,
@@ -6,10 +7,14 @@ import {
parseRequestPayload,
} from "~/utils/remix.server";
import { idObject } from "~/utils/zod";
import { databaseTimestampToDate } from "../../../utils/dates";
import {
databaseTimestampToDate,
databaseTimestampToJavascriptTimestamp,
} from "../../../utils/dates";
import { errorToast } from "../../../utils/remix.server";
import { requireUser } from "../../auth/core/user.server";
import * as ScrimPostRepository from "../ScrimPostRepository.server";
import * as Scrim from "../core/Scrim";
import { cancelScrimSchema } from "../scrims-schemas";
export const action = async ({ request, params }: ActionFunctionArgs) => {
@@ -33,5 +38,17 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
reason: data.reason,
});
notify({
userIds: Scrim.participantIdsListFromAccepted(post),
defaultSeenUserIds: [user.id],
notification: {
type: "SCRIM_CANCELED",
meta: {
id: post.id,
at: databaseTimestampToJavascriptTimestamp(post.at),
},
},
});
return null;
};

View File

@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import type { ScrimPost } from "../scrims-types";
import { participantIdsListFromAccepted } from "./Scrim";
type MockUser = { id: number };
type MockRequest = { isAccepted: boolean; users: MockUser[] };
function createPost(users: MockUser[], requests: MockRequest[]): ScrimPost {
return {
id: 1,
users,
requests,
createdAt: "",
updatedAt: "",
title: "",
description: "",
status: "open",
authorId: 0,
} as unknown as ScrimPost;
}
describe("participantIdsListFromAccepted", () => {
it("returns only post users if no accepted request", () => {
const post = createPost(
[{ id: 10 }, { id: 20 }],
[
{
isAccepted: false,
users: [{ id: 30 }],
},
],
);
const result = participantIdsListFromAccepted(post);
expect(result).toEqual([10, 20]);
});
it("returns post users and accepted request users", () => {
const post = createPost(
[{ id: 10 }, { id: 20 }],
[
{
isAccepted: false,
users: [{ id: 30 }],
},
{
isAccepted: true,
users: [{ id: 40 }, { id: 50 }],
},
],
);
const result = participantIdsListFromAccepted(post);
expect(result).toEqual([10, 20, 40, 50]);
});
it("returns post users if accepted request has no users", () => {
const post = createPost(
[{ id: 10 }],
[
{
isAccepted: true,
users: [],
},
],
);
const result = participantIdsListFromAccepted(post);
expect(result).toEqual([10]);
});
it("returns empty array if no users and no accepted request", () => {
const post = createPost([], []);
const result = participantIdsListFromAccepted(post);
expect(result).toEqual([]);
});
});

View File

@@ -1,3 +1,4 @@
import { logger } from "~/utils/logger";
import type { ScrimPost } from "../scrims-types";
/** Returns true if the original poster has accepted any of the requests. */
@@ -17,3 +18,20 @@ export function isParticipating(post: ScrimPost, userId: number) {
export function resolvePoolCode(postId: number) {
return `SC${postId % 10}`;
}
/**
* Returns an array of participant IDs from the given post object that is accepted (scrim page exists).
*/
export function participantIdsListFromAccepted(post: ScrimPost) {
const acceptedRequest = post.requests.find((r) => r.isAccepted);
if (!acceptedRequest) {
logger.warn(
`Scrim post ${post.id} has no accepted request, returning only post users.`,
);
}
return post.users
.map((u) => u.id)
.concat(acceptedRequest?.users.map((u) => u.id) ?? []);
}

View File

@@ -26,9 +26,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
return {
post,
chatUsers: await UserRepository.findChatUsersByUserIds(
post.users
.map((u) => u.id)
.concat(post.requests.at(0)?.users.map((u) => u.id) ?? []),
Scrim.participantIdsListFromAccepted(post),
),
};
};

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "logindforsøg afbrudt",
"auth.errors.failed": "Loginforsøg fejlet",
"auth.errors.discordPermissions": "Før at du kan oprette en profil på sendou.ink, skal sendou.ink have adgang til din Discordprofils navn, brugerbillede og sociale forbindelser (de sociale medier, som du har tilknyttet din discordprofil).",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Einloggen abgebrochen",
"auth.errors.failed": "Einloggen fehlgeschlagen",
"auth.errors.discordPermissions": "Für dein sendou.ink-Profil benötigt die Seite Zugriff auf den Namen, Avatar und verbundene Social-Media-Accounts in deinem Discord-Profil.",

View File

@@ -13,6 +13,7 @@
"pickup": "Pickup von {{username}}",
"status.booked": "Gebucht",
"status.pending": "Ausstehend",
"status.canceled": "",
"actions.request": "Anfragen",
"deleteModal.title": "Scrim post löschen?",
"deleteModal.prevented": "Der Post muss von Ersteller ({{username}}) gelöscht werden.",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} requested a scrim",
"notifications.title.SCRIM_SCHEDULED": "Scrim Scheduled",
"notifications.text.SCRIM_SCHEDULED": "New scrim scheduled at {{timeString}}",
"notifications.title.SCRIM_CANCELED": "Scrim Canceled",
"notifications.text.SCRIM_CANCELED": "The scrim at {{timeString}} was canceled",
"auth.errors.aborted": "Login Aborted",
"auth.errors.failed": "Login Failed",
"auth.errors.discordPermissions": "For your sendou.ink profile, the site needs access to your Discord profile's name, avatar and social connections.",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Ingreso cancelado",
"auth.errors.failed": "Ingreso fallido",
"auth.errors.discordPermissions": "Para tu perfil en sendou.ink, el sitio requiere aceso a tu nombre en Discord, avatar, y redes sociales.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Ingreso cancelado",
"auth.errors.failed": "Ingreso fallido",
"auth.errors.discordPermissions": "Para tu perfil en sendou.ink, el sitio requiere aceso a tu nombre en Discord, avatar, y redes sociales.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Connexion abandonnée",
"auth.errors.failed": "Connexion échouée",
"auth.errors.discordPermissions": "Pour mettre en place votre profil, sendou.ink a besoin de votre nom de profil Discord, de votre avatar et de vos réseaux connectés.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} vous demande de scrim",
"notifications.title.SCRIM_SCHEDULED": "Scrim Programmé",
"notifications.text.SCRIM_SCHEDULED": "Nouveau scrim programmé à {{timeString}}",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Connexion abandonnée",
"auth.errors.failed": "Connexion échouée",
"auth.errors.discordPermissions": "Pour mettre en place votre profil, sendou.ink a besoin de votre nom de profil Discord, de votre avatar et de vos réseaux connectés.",

View File

@@ -13,6 +13,7 @@
"pickup": "Pickup de {{username}}",
"status.booked": "Réservé",
"status.pending": "En attente",
"status.canceled": "",
"actions.request": "Demande",
"deleteModal.title": "Supprimer le post?",
"deleteModal.prevented": "Le post doit être supprimée par le propriétaire ({{username}})",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "הכניסה בוטלה",
"auth.errors.failed": "הכניסה נכשלה",
"auth.errors.discordPermissions": "עבור פרופיל sendou.ink שלך, האתר זקוק לגישה לשם, הפרופיל והקשרים החברתיים של פרופיל ה-Discord שלך.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Accesso cancellato",
"auth.errors.failed": "Accesso fallito",
"auth.errors.discordPermissions": "Per il tuo profilo di sendou.ink, il sito ha bisogno di accesso al nome utente, avatar e connessioni social del tuo profilo Discord.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "ログインを中断しました",
"auth.errors.failed": "ログインに失敗しました",
"auth.errors.discordPermissions": "sendou.ink は、Discord のプロファイル名、アバター、SNS連携をサイトのプロファイルに使用します。",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "로그인 중단됨",
"auth.errors.failed": "로그인 실패",
"auth.errors.discordPermissions": "sendou.ink 프로필을 위해 디스코드 프로필의 이름, 아바타와 연락처에 대한 접근이 필요합니다.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "",
"auth.errors.failed": "",
"auth.errors.discordPermissions": "",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Logowanie przerwane",
"auth.errors.failed": "Logowanie nieudane",
"auth.errors.discordPermissions": "Do twojego profilu sendou.ink, ta strona potrzebuje dostęp do twojej nazwy, avataru i połączeń konta Discord.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Login Abortado",
"auth.errors.failed": "Login Falhou",
"auth.errors.discordPermissions": "Para o seu perfil do sendou.ink, o site precisa de acesso ao nome do perfil do seu Discord, incluindo também o avatar e conexões sociais.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "{{fromUsername}} запросил скрим",
"notifications.title.SCRIM_SCHEDULED": "Скрим Запланирован",
"notifications.text.SCRIM_SCHEDULED": "Новый скрим запланирован на {{timeString}}",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "Вход отменён",
"auth.errors.failed": "Ошибка входа",
"auth.errors.discordPermissions": "Для вашего профиля на sendou.ink странице нужен доступ к вашему имени, аватару и привязанным аккаунтам соц. сетей в Discord.",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",

View File

@@ -75,6 +75,8 @@
"notifications.text.SCRIM_NEW_REQUEST": "",
"notifications.title.SCRIM_SCHEDULED": "",
"notifications.text.SCRIM_SCHEDULED": "",
"notifications.title.SCRIM_CANCELED": "",
"notifications.text.SCRIM_CANCELED": "",
"auth.errors.aborted": "登录中止",
"auth.errors.failed": "登录失败",
"auth.errors.discordPermissions": "为了完善您的sendou.ink个人资料网站需要获取您的Discord名字、头像和社交链接。",

View File

@@ -13,6 +13,7 @@
"pickup": "",
"status.booked": "",
"status.pending": "",
"status.canceled": "",
"actions.request": "",
"deleteModal.title": "",
"deleteModal.prevented": "",