diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..8828813d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,275 @@ +Project Context + + Svelte 5.x with runes system ($state, $derived, $effect, $props, $bindable) + SvelteKit for full-stack applications with file-based routing + TypeScript for type safety and better developer experience + Component-scoped styling with CSS custom properties + Progressive enhancement and performance-first approach + Modern build tooling (Vite) with optimizations + +Development Standards +Architecture + + Use Svelte 5 runes system for all reactivity instead of legacy stores + Organize components by feature or domain for scalability + Separate presentation components from logic-heavy components + Extract reusable logic into composable functions + Implement proper component composition with slots and snippets + Use SvelteKit's file-based routing with proper load functions + +TypeScript Integration + + Enable strict mode in tsconfig.json for maximum type safety + Define interfaces for component props using $props() syntax + Type event handlers, refs, and SvelteKit's generated types + Use generic types for reusable components + Leverage $types.ts files generated by SvelteKit + Implement proper type checking with svelte-check + +Component Design + + Follow single responsibility principle for components + Use diff --git a/libs/frontend/src/lib/components/nav/pagination.svelte b/libs/frontend/src/lib/components/nav/pagination.svelte index 194a61d6..0638b7c7 100644 --- a/libs/frontend/src/lib/components/nav/pagination.svelte +++ b/libs/frontend/src/lib/components/nav/pagination.svelte @@ -2,7 +2,7 @@ import { page } from "$app/stores"; import { DEFAULT_PAGE } from "types"; import { Button } from "../ui/button"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; let { currentPage, diff --git a/libs/frontend/src/lib/components/nav/toggle-theme.svelte b/libs/frontend/src/lib/components/nav/toggle-theme.svelte index 50e8014a..e48f07dd 100644 --- a/libs/frontend/src/lib/components/nav/toggle-theme.svelte +++ b/libs/frontend/src/lib/components/nav/toggle-theme.svelte @@ -5,7 +5,7 @@ import Toggle from "../ui/toggle/toggle.svelte"; import { cn } from "@/utils/cn"; import { Toggle as TogglePrimitive } from "bits-ui"; - import type { DataTestIdProp } from "@/config/test-ids"; + import type { DataTestIdProp } from "types"; type $$Props = TogglePrimitive.RootProps & { class?: string }; diff --git a/libs/frontend/src/lib/components/nav/user-dropdown.svelte b/libs/frontend/src/lib/components/nav/user-dropdown.svelte index ee48e912..c9303539 100644 --- a/libs/frontend/src/lib/components/nav/user-dropdown.svelte +++ b/libs/frontend/src/lib/components/nav/user-dropdown.svelte @@ -2,7 +2,7 @@ import * as DropdownMenu from "#/ui/dropdown-menu"; import * as Avatar from "#/ui/avatar"; import { frontendUrls } from "types"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { authenticatedUserInfo } from "@/stores"; import { Button } from "#/ui/button"; diff --git a/libs/frontend/src/lib/components/typography/markdown.svelte b/libs/frontend/src/lib/components/typography/markdown.svelte index 821e4719..02913b65 100644 --- a/libs/frontend/src/lib/components/typography/markdown.svelte +++ b/libs/frontend/src/lib/components/typography/markdown.svelte @@ -1,5 +1,4 @@ + + diff --git a/libs/frontend/src/lib/components/ui/button-group/button-group-text.svelte b/libs/frontend/src/lib/components/ui/button-group/button-group-text.svelte new file mode 100644 index 00000000..20245d50 --- /dev/null +++ b/libs/frontend/src/lib/components/ui/button-group/button-group-text.svelte @@ -0,0 +1,30 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render mergedProps.children?.()} +
+{/if} diff --git a/libs/frontend/src/lib/components/ui/button-group/button-group.svelte b/libs/frontend/src/lib/components/ui/button-group/button-group.svelte new file mode 100644 index 00000000..b82c0d2a --- /dev/null +++ b/libs/frontend/src/lib/components/ui/button-group/button-group.svelte @@ -0,0 +1,48 @@ + + + + +
+ {@render children?.()} +
diff --git a/libs/frontend/src/lib/components/ui/button-group/index.ts b/libs/frontend/src/lib/components/ui/button-group/index.ts new file mode 100644 index 00000000..476bef81 --- /dev/null +++ b/libs/frontend/src/lib/components/ui/button-group/index.ts @@ -0,0 +1,13 @@ +import Root from "./button-group.svelte"; +import Text from "./button-group-text.svelte"; +import Separator from "./button-group-separator.svelte"; + +export { + Root, + Text, + Separator, + // + Root as ButtonGroup, + Text as ButtonGroupText, + Separator as ButtonGroupSeparator +}; diff --git a/libs/frontend/src/lib/components/ui/button/button.svelte b/libs/frontend/src/lib/components/ui/button/button.svelte index 9c750ee6..15169478 100644 --- a/libs/frontend/src/lib/components/ui/button/button.svelte +++ b/libs/frontend/src/lib/components/ui/button/button.svelte @@ -45,7 +45,7 @@ diff --git a/libs/frontend/src/lib/config/api.ts b/libs/frontend/src/lib/config/api.ts deleted file mode 100644 index 9e908aa6..00000000 --- a/libs/frontend/src/lib/config/api.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BanType } from "types"; - -export const apiUrls = { - ACCOUNT_PREFERENCES: `/api/account/preferences`, - EXECUTE_CODE: `/api/execute-code`, - PROGRAMMING_LANGUAGES: `/api/programming-languages`, - SUBMIT_CODE: `/api/submit-code`, - SUBMIT_GAME: `/api/submit-game`, - SUPPORTED_LANGUAGES: `/api/supported-languages`, - commentById: (id: string) => `/api/comment/${id}`, - commentByIdComment: (id: string) => `/api/comment/${id}/comment`, - commentByIdVote: (id: string) => `/api/comment/${id}/vote`, - puzzleByIdComment: (id: string) => `/api/puzzles/${id}/comment`, - submissionById: (id: string) => `/api/submission/${id}`, - userByUsername: (username: string) => `/api/user/${username}`, - userByUsernamePuzzle: (username: string) => `/api/user/${username}/puzzle`, - usernameIsAvailable: (username: string) => - `/api/username-is-available/${username}`, - moderationPuzzleByIdApprove: (id: string) => - `/api/moderation/puzzle/${id}/approve`, - moderationPuzzleByIdRevise: (id: string) => - `/api/moderation/puzzle/${id}/revise`, - moderationReportByIdResolve: (id: string) => - `/api/moderation/report/${id}/resolve`, - REPORT: `/api/report`, - moderationUserByIdBanByType: (id: string, banType: BanType) => - `/api/moderation/user/${id}/ban/${banType}`, - moderationUserByIdUnban: (id: string) => `/api/moderation/user/${id}/unban`, - moderationUserByIdBanHistory: (id: string) => - `/api/moderation/user/${id}/ban/history` -} as const; diff --git a/libs/frontend/src/lib/config/local-storage.ts b/libs/frontend/src/lib/config/local-storage.ts index 56ba8bf5..26fa29b4 100644 --- a/libs/frontend/src/lib/config/local-storage.ts +++ b/libs/frontend/src/lib/config/local-storage.ts @@ -1,5 +1,6 @@ export const localStorageKeys = { AUTHENTICATED_USER_INFO: "authenticatedUserInfo", PREFERENCES: "preferences", - THEME: "theme" + THEME: "theme", + LANGUAGES: "languages" } as const; diff --git a/libs/frontend/src/lib/features/authentication/login/components/login-form.svelte b/libs/frontend/src/lib/features/authentication/login/components/login-form.svelte index 4c996ce4..4ad9dd37 100644 --- a/libs/frontend/src/lib/features/authentication/login/components/login-form.svelte +++ b/libs/frontend/src/lib/features/authentication/login/components/login-form.svelte @@ -14,7 +14,7 @@ import GenericAlert from "@/components/ui/alert/generic-alert.svelte"; import { isHttpErrorCode } from "@/utils/is-http-error-code"; import { page } from "$app/state"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import EyeClosed from "@lucide/svelte/icons/eye-closed"; import Eye from "@lucide/svelte/icons/eye"; import Button from "#/ui/button/button.svelte"; @@ -76,6 +76,7 @@ class="pr-10" /> - - diff --git a/libs/frontend/src/lib/features/puzzles/components/create-puzzle-form.svelte b/libs/frontend/src/lib/features/puzzles/components/create-puzzle-form.svelte index 4bba7d25..9029a4a1 100644 --- a/libs/frontend/src/lib/features/puzzles/components/create-puzzle-form.svelte +++ b/libs/frontend/src/lib/features/puzzles/components/create-puzzle-form.svelte @@ -7,7 +7,7 @@ import GenericAlert from "@/components/ui/alert/generic-alert.svelte"; import { isHttpErrorCode } from "@/utils/is-http-error-code"; import { page } from "$app/stores"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; let { data diff --git a/libs/frontend/src/lib/features/puzzles/components/delete-puzzle-confirmation-dialog.svelte b/libs/frontend/src/lib/features/puzzles/components/delete-puzzle-confirmation-dialog.svelte index b1e57db6..6629b7b9 100644 --- a/libs/frontend/src/lib/features/puzzles/components/delete-puzzle-confirmation-dialog.svelte +++ b/libs/frontend/src/lib/features/puzzles/components/delete-puzzle-confirmation-dialog.svelte @@ -3,7 +3,7 @@ import * as Dialog from "@/components/ui/dialog"; import * as Form from "@/components/ui/form"; import Input from "@/components/ui/input/input.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { superForm, type SuperValidated } from "sveltekit-superforms"; import { zod4Client } from "sveltekit-superforms/adapters"; import { deletePuzzleSchema, type DeletePuzzle } from "types"; diff --git a/libs/frontend/src/lib/features/puzzles/components/edit-puzzle-form.svelte b/libs/frontend/src/lib/features/puzzles/components/edit-puzzle-form.svelte index c2b573bb..6a4c3d71 100644 --- a/libs/frontend/src/lib/features/puzzles/components/edit-puzzle-form.svelte +++ b/libs/frontend/src/lib/features/puzzles/components/edit-puzzle-form.svelte @@ -25,7 +25,7 @@ import LanguageSelect from "./language-select.svelte"; import Codemirror from "@/features/game/components/codemirror.svelte"; import { languages } from "@/stores/languages"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; let { data diff --git a/libs/frontend/src/lib/features/puzzles/components/play-puzzle.svelte b/libs/frontend/src/lib/features/puzzles/components/play-puzzle.svelte index 4c82c9da..030a4f42 100644 --- a/libs/frontend/src/lib/features/puzzles/components/play-puzzle.svelte +++ b/libs/frontend/src/lib/features/puzzles/components/play-puzzle.svelte @@ -1,6 +1,7 @@ + + + Error - CodinCod + + + + +
+
+
+ +
+ +
+

+ {#if isAuthError} + Authentication Required + {:else if page.status === 404} + Page Not Found + {:else} + Something Went Wrong + {/if} +

+ +

+ {#if isAuthError} + Your session has expired or you don't have permission to access this + page. Please log in again. + {:else if page.status === 404} + The page you're looking for doesn't exist or has been moved. + {:else if page.status >= 500} + We're experiencing technical difficulties. Please try again later. + {:else} + {page.error?.message || "An unexpected error occurred."} + {/if} +

+ + {#if !isAuthError} +

+ Error code: {page.status} +

+ {/if} +
+
+ +
+ {#if isAuthError} + + {:else} + + {/if} + + {#if !isAuthError && page.status !== 404} + + {/if} +
+ + {#if import.meta.env.DEV && page.error} +
+ + Debug Information (Dev Only) + +
{JSON.stringify(
+						{
+							message: page.error.message,
+							status: page.status,
+							path: page.url.pathname,
+							stack: (page.error as Error & { stack?: string }).stack
+						},
+						null,
+						2
+					)}
+
+ {/if} +
+
diff --git a/libs/frontend/src/routes/(authenticated)/logout/+page.server.ts b/libs/frontend/src/routes/(authenticated)/logout/+page.server.ts index 09045049..412010f8 100644 --- a/libs/frontend/src/routes/(authenticated)/logout/+page.server.ts +++ b/libs/frontend/src/routes/(authenticated)/logout/+page.server.ts @@ -3,7 +3,8 @@ import { cookieKeys, environment, frontendUrls, - getCookieOptions + getCookieOptions, + httpRequestMethod } from "types"; import type { Actions } from "./$types"; import { env } from "$env/dynamic/private"; @@ -18,7 +19,7 @@ export const actions = { // Call the backend logout endpoint to clear the httpOnly cookie const response = await fetch(backendUrl, { - method: "POST", + method: httpRequestMethod.POST, headers: { Cookie: `${cookieKeys.TOKEN}=${token ?? ""}` } diff --git a/libs/frontend/src/routes/(authenticated)/multiplayer/+page.svelte b/libs/frontend/src/routes/(authenticated)/multiplayer/+page.svelte index fd7680c9..7bcfe229 100644 --- a/libs/frontend/src/routes/(authenticated)/multiplayer/+page.svelte +++ b/libs/frontend/src/routes/(authenticated)/multiplayer/+page.svelte @@ -11,7 +11,6 @@ import CountdownTimer from "@/components/ui/countdown-timer/countdown-timer.svelte"; import CustomGameDialog from "@/features/multiplayer/components/custom-game-dialog.svelte"; import JoinByInviteDialog from "@/features/multiplayer/components/join-by-invite-dialog.svelte"; - import WaitingRoomChat from "@/features/multiplayer/components/waiting-room-chat.svelte"; import { buildWebSocketUrl } from "@/config/websocket"; import { authenticatedUserInfo } from "@/stores"; import { WebSocketManager } from "@/websocket/websocket-manager.svelte"; @@ -29,10 +28,13 @@ type RoomStateResponse, type WaitingRoomRequest, type WaitingRoomResponse, - type GameOptions + type GameOptions, + type ChatMessage } from "types"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { currentTime } from "@/stores/current-time"; + import Chat from "@/features/chat/components/chat.svelte"; + import { Input } from "#/ui/input"; let room: RoomStateResponse | undefined = $state(); let rooms: RoomOverviewResponse[] = $state([]); @@ -42,9 +44,8 @@ $state(); let customGameDialogOpen = $state(false); let joinByInviteDialogOpen = $state(false); - let chatMessages = $state< - Array<{ username: string; message: string; timestamp: Date }> - >([]); + let chatMessages = $state>([]); + let showInviteCode = $state(false); const queryParamKeys = { ROOM_ID: "roomId" @@ -94,8 +95,9 @@ chatMessages.push({ username: data.username, message: data.message, - timestamp: new Date(data.timestamp) + createdAt: new Date(data.createdAt) }); + chatMessages = chatMessages; // Trigger reactivity } break; @@ -160,7 +162,6 @@ if (room?.roomId) updateRoomIdInUrl(); }); - // Auto-redirect when countdown reaches zero $effect(() => { if (!pendingGameStart) return; @@ -317,15 +318,24 @@
{#if room.inviteCode}
-

- 🔒 Private Game - Invite Code: -

+

Invite Code

- +
diff --git a/libs/frontend/src/routes/(authenticated)/multiplayer/[id]/+page.svelte b/libs/frontend/src/routes/(authenticated)/multiplayer/[id]/+page.svelte index a29c9a22..e9ca48d8 100644 --- a/libs/frontend/src/routes/(authenticated)/multiplayer/[id]/+page.svelte +++ b/libs/frontend/src/routes/(authenticated)/multiplayer/[id]/+page.svelte @@ -12,7 +12,8 @@ import Loader from "@/components/ui/loader/loader.svelte"; import LogicalUnit from "@/components/ui/logical-unit/logical-unit.svelte"; import * as Resizable from "@/components/ui/resizable"; - import { apiUrls } from "@/config/api"; + import { buildBackendUrl } from "@/config/backend"; + import { backendUrls } from "types"; import { buildWebSocketUrl } from "@/config/websocket"; import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; import Chat from "@/features/chat/components/chat.svelte"; @@ -49,7 +50,7 @@ type GameRequest, type GameResponse } from "types"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; function isUserIdInUserList( userId: string, @@ -196,10 +197,13 @@ userId: $authenticatedUserInfo.userId }; - await fetchWithAuthenticationCookie(apiUrls.SUBMIT_GAME, { - body: JSON.stringify(gameSubmissionParams), - method: httpRequestMethod.POST - }); + await fetchWithAuthenticationCookie( + buildBackendUrl(backendUrls.SUBMISSION_GAME), + { + body: JSON.stringify(gameSubmissionParams), + method: httpRequestMethod.POST + } + ); sendGameMessage({ event: gameEventEnum.SUBMITTED_PLAYER diff --git a/libs/frontend/src/routes/(authenticated)/puzzles/[id]/edit/+page.server.ts b/libs/frontend/src/routes/(authenticated)/puzzles/[id]/edit/+page.server.ts index 1c351b79..28bb23de 100644 --- a/libs/frontend/src/routes/(authenticated)/puzzles/[id]/edit/+page.server.ts +++ b/libs/frontend/src/routes/(authenticated)/puzzles/[id]/edit/+page.server.ts @@ -4,17 +4,19 @@ import { buildBackendUrl } from "@/config/backend"; import { backendUrls, cookieKeys, + DELETE, deletePuzzleSchema, editPuzzleSchema, + ERROR_MESSAGES, environment, + frontendUrls, httpResponseCodes, PUT, type EditPuzzle } from "types"; -import { error, fail } from "@sveltejs/kit"; +import { error, fail, redirect } from "@sveltejs/kit"; import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie.js"; import type { PageServerLoadEvent, RequestEvent } from "./$types.js"; -import { handleDeletePuzzleForm } from "../../../../api/handle-delete-puzzle-form.js"; export async function load({ fetch, params, cookies }: PageServerLoadEvent) { const id = params.id; @@ -47,7 +49,38 @@ export async function load({ fetch, params, cookies }: PageServerLoadEvent) { } export const actions = { - deletePuzzle: handleDeletePuzzleForm, + deletePuzzle: async ({ params, request }: RequestEvent) => { + const deletePuzzleForm = await superValidate( + request, + zod4(deletePuzzleSchema) + ); + + if (!deletePuzzleForm.valid) { + return fail(httpResponseCodes.CLIENT_ERROR.BAD_REQUEST, { + deletePuzzleForm + }); + } + + const id = deletePuzzleForm.data.id; + const deletePuzzleUrl = buildBackendUrl(backendUrls.puzzleById(id)); + + const response = await fetchWithAuthenticationCookie(deletePuzzleUrl, { + headers: { + "Content-Type": "application/json", + Cookie: request.headers.get("cookie") || "" + }, + method: DELETE + }); + + if (!response.ok) { + return fail(response.status, { + deletePuzzleForm, + error: ERROR_MESSAGES.PUZZLE.FAILED_TO_DELETE + }); + } + + redirect(httpResponseCodes.REDIRECTION.SEE_OTHER, frontendUrls.PUZZLES); + }, editPuzzle: async ({ params, request }: RequestEvent) => { const form = await superValidate(request, zod4(editPuzzleSchema)); @@ -72,7 +105,7 @@ export const actions = { if (!response.ok) { return fail(response.status, { - error: "Failed to update the puzzle.", + error: ERROR_MESSAGES.PUZZLE.FAILED_TO_UPDATE, form }); } diff --git a/libs/frontend/src/routes/(authenticated)/puzzles/create/+page.server.ts b/libs/frontend/src/routes/(authenticated)/puzzles/create/+page.server.ts index c2791af1..fddd5537 100644 --- a/libs/frontend/src/routes/(authenticated)/puzzles/create/+page.server.ts +++ b/libs/frontend/src/routes/(authenticated)/puzzles/create/+page.server.ts @@ -1,6 +1,13 @@ import { superValidate } from "sveltekit-superforms"; import { zod4 } from "sveltekit-superforms/adapters"; -import { backendUrls, createPuzzleSchema, frontendUrls, POST } from "types"; +import { + backendUrls, + createPuzzleSchema, + ERROR_MESSAGES, + frontendUrls, + httpResponseCodes, + POST +} from "types"; import { buildBackendUrl } from "@/config/backend"; import { fail, redirect } from "@sveltejs/kit"; import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; @@ -17,7 +24,10 @@ export const actions = { const form = await superValidate(request, zod4(createPuzzleSchema)); if (!form.valid) { - fail(400, { form }); + return fail(httpResponseCodes.CLIENT_ERROR.BAD_REQUEST, { + form, + message: ERROR_MESSAGES.FORM.VALIDATION_ERRORS + }); } const cookie = request.headers.get("cookie") || ""; @@ -37,11 +47,14 @@ export const actions = { const data = await result.json(); if (!result.ok) { - fail(400, { form, message: data.message }); + return fail(httpResponseCodes.CLIENT_ERROR.BAD_REQUEST, { + form, + message: data.message || ERROR_MESSAGES.PUZZLE.FAILED_TO_CREATE + }); } const editPuzzleUrl = frontendUrls.puzzleByIdEdit(data._id); - throw redirect(302, editPuzzleUrl); + throw redirect(httpResponseCodes.REDIRECTION.FOUND, editPuzzleUrl); } }; diff --git a/libs/frontend/src/routes/(unauthenticated-only)/login/+page.server.ts b/libs/frontend/src/routes/(unauthenticated-only)/login/+page.server.ts index 120fd149..39fb7295 100644 --- a/libs/frontend/src/routes/(unauthenticated-only)/login/+page.server.ts +++ b/libs/frontend/src/routes/(unauthenticated-only)/login/+page.server.ts @@ -2,10 +2,18 @@ import { superValidate } from "sveltekit-superforms"; import { zod4 } from "sveltekit-superforms/adapters"; import type { RequestEvent } from "./$types"; import { fail, redirect } from "@sveltejs/kit"; -import { frontendUrls, httpResponseCodes, loginSchema } from "types"; +import { + backendUrls, + ERROR_MESSAGES, + frontendUrls, + httpRequestMethod, + httpResponseCodes, + loginSchema +} from "types"; import { setCookie } from "@/features/authentication/utils/set-cookie"; -import { login } from "../../api/login"; import { searchParamKeys } from "@/config/search-params"; +import { buildBackendUrl } from "@/config/backend"; +import type { LoginRequest } from "types/dist/core/api/schema/auth/login.schema"; export async function load() { const form = await superValidate(zod4(loginSchema)); @@ -20,11 +28,20 @@ export const actions = { if (!form.valid) { return fail(httpResponseCodes.CLIENT_ERROR.BAD_REQUEST, { form, - message: "Form errors" + message: ERROR_MESSAGES.FORM.VALIDATION_ERRORS }); } - const result = await login(form.data.identifier, form.data.password); + const payload: LoginRequest = { + identifier: form.data.identifier, + password: form.data.password + }; + + const result = await fetch(buildBackendUrl(backendUrls.LOGIN), { + method: httpRequestMethod.POST, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); const data = await result.json(); if (!result.ok) { diff --git a/libs/frontend/src/routes/(unauthenticated-only)/register/+page.server.ts b/libs/frontend/src/routes/(unauthenticated-only)/register/+page.server.ts index 01c4bb98..3b182fca 100644 --- a/libs/frontend/src/routes/(unauthenticated-only)/register/+page.server.ts +++ b/libs/frontend/src/routes/(unauthenticated-only)/register/+page.server.ts @@ -4,6 +4,7 @@ import { fail, redirect } from "@sveltejs/kit"; import { buildBackendUrl } from "@/config/backend"; import { backendUrls, + ERROR_MESSAGES, frontendUrls, httpResponseCodes, POST, @@ -25,7 +26,7 @@ export const actions = { if (!form.valid) { return fail(httpResponseCodes.CLIENT_ERROR.BAD_REQUEST, { form, - message: "Form errors" + message: ERROR_MESSAGES.FORM.VALIDATION_ERRORS }); } diff --git a/libs/frontend/src/routes/+error.svelte b/libs/frontend/src/routes/+error.svelte index 2b19766e..908c779f 100644 --- a/libs/frontend/src/routes/+error.svelte +++ b/libs/frontend/src/routes/+error.svelte @@ -1,21 +1,108 @@ - - -

{page.status} - {page.error?.message || "Something broke."}

+ + Error - CodinCod + + + + +
+
+
+ +
+ +
+

+ {page.status === 404 ? "Page Not Found" : "Something Went Wrong"} +

+ +

+ {#if page.status === 404} + The page you're looking for doesn't exist or has been moved. + {:else if page.status >= 500} + We're experiencing technical difficulties. Please try again later. + {:else} + {page.error?.message || "An unexpected error occurred."} + {/if} +

+ + {#if page.status !== 404} +

+ Error code: {page.status} +

+ {/if} +
+
+ +
+ + + {#if page.status !== 404} + + {/if} +
-

Go back to the .

+ {#if import.meta.env.DEV && page.error} +
+ + Debug Information (Dev Only) + +
{JSON.stringify(
+						{
+							message: page.error.message,
+							status: page.status,
+							path: page.url.pathname,
+							stack: (page.error as Error & { stack?: string }).stack
+						},
+						null,
+						2
+					)}
+
+ {/if} +
diff --git a/libs/frontend/src/routes/api/account/preferences/+server.ts b/libs/frontend/src/routes/api/account/preferences/+server.ts deleted file mode 100644 index 41f1d142..00000000 --- a/libs/frontend/src/routes/api/account/preferences/+server.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls } from "types"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ request }: RequestEvent) { - const body = await request.text(); - - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.ACCOUNT_PREFERENCES), - { - body: body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} - -export async function PUT({ request }: RequestEvent) { - const body = await request.text(); - - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.ACCOUNT_PREFERENCES), - { - body: body, - headers: getCookieHeader(request), - method: httpRequestMethod.PUT - } - ); -} - -export async function GET({ request }: RequestEvent) { - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.ACCOUNT_PREFERENCES), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); -} - -export async function DELETE({ request }: RequestEvent) { - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.ACCOUNT_PREFERENCES), - { - headers: getCookieHeader(request), - method: httpRequestMethod.DELETE - } - ); -} diff --git a/libs/frontend/src/routes/api/comment/[id]/+server.ts b/libs/frontend/src/routes/api/comment/[id]/+server.ts deleted file mode 100644 index b404b592..00000000 --- a/libs/frontend/src/routes/api/comment/[id]/+server.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function GET({ params, request }: RequestEvent) { - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.commentById(params.id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); -} - -export async function DELETE({ params, request }: RequestEvent) { - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.commentById(params.id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.DELETE - } - ); -} diff --git a/libs/frontend/src/routes/api/comment/[id]/comment/+server.ts b/libs/frontend/src/routes/api/comment/[id]/comment/+server.ts deleted file mode 100644 index 24d47f91..00000000 --- a/libs/frontend/src/routes/api/comment/[id]/comment/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "../vote/$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ params, request }: RequestEvent) { - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.commentByIdComment(params.id)), - { - body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} diff --git a/libs/frontend/src/routes/api/comment/[id]/vote/+server.ts b/libs/frontend/src/routes/api/comment/[id]/vote/+server.ts deleted file mode 100644 index 2188e380..00000000 --- a/libs/frontend/src/routes/api/comment/[id]/vote/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ params, request }: RequestEvent) { - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.commentByIdVote(params.id)), - { - body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} diff --git a/libs/frontend/src/routes/api/execute-code/+server.ts b/libs/frontend/src/routes/api/execute-code/+server.ts deleted file mode 100644 index 429aa0be..00000000 --- a/libs/frontend/src/routes/api/execute-code/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ request }: RequestEvent) { - const body = await request.text(); - - return fetchWithAuthenticationCookie(buildBackendUrl(backendUrls.EXECUTE), { - body: body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - }); -} diff --git a/libs/frontend/src/routes/api/get-user-activity-by-username.ts b/libs/frontend/src/routes/api/get-user-activity-by-username.ts deleted file mode 100644 index 2bf64306..00000000 --- a/libs/frontend/src/routes/api/get-user-activity-by-username.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls } from "types"; - -export async function getUserActivityByUsername(username: string) { - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.userByUsernameActivity(username)) - ); -} diff --git a/libs/frontend/src/routes/api/handle-delete-puzzle-form.ts b/libs/frontend/src/routes/api/handle-delete-puzzle-form.ts deleted file mode 100644 index 086ac463..00000000 --- a/libs/frontend/src/routes/api/handle-delete-puzzle-form.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { redirect, type RequestEvent } from "@sveltejs/kit"; -import { fail, superValidate } from "sveltekit-superforms"; -import { zod4 } from "sveltekit-superforms/adapters"; -import { - backendUrls, - DELETE, - deletePuzzleSchema, - frontendUrls, - httpResponseCodes -} from "types"; - -export async function handleDeletePuzzleForm({ request }: RequestEvent) { - const deletePuzzleForm = await superValidate( - request, - zod4(deletePuzzleSchema) - ); - - if (!deletePuzzleForm.valid) { - // Again, return { form } and things will just work. - fail(400, { deletePuzzleForm }); - } - - const cookie = request.headers.get("cookie") || ""; - - // Prepare the url - const id = deletePuzzleForm.data.id; - const deletePuzzleUrl = buildBackendUrl(backendUrls.puzzleById(id)); - - // Update puzzle data to backend - const response = await fetchWithAuthenticationCookie(deletePuzzleUrl, { - headers: { - "Content-Type": "application/json", - Cookie: cookie - }, - method: DELETE - }); - - if (!response.ok) { - fail(response.status, { - deletePuzzleForm, - error: "Failed to delete the puzzle." - }); - } - - if (response.ok) { - redirect(httpResponseCodes.REDIRECTION.SEE_OTHER, frontendUrls.PUZZLES); - } - - // Display a success status message - return { - deletePuzzleForm, - message: "Puzzle deleted successfully!", - success: true - }; -} diff --git a/libs/frontend/src/routes/api/login.ts b/libs/frontend/src/routes/api/login.ts deleted file mode 100644 index 2257d564..00000000 --- a/libs/frontend/src/routes/api/login.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, POST } from "types"; - -export async function login(identifier: string, password: string) { - return fetchWithAuthenticationCookie(buildBackendUrl(backendUrls.LOGIN), { - body: JSON.stringify({ identifier, password }), - method: POST - }); -} diff --git a/libs/frontend/src/routes/api/moderation/puzzle/[id]/approve/+server.ts b/libs/frontend/src/routes/api/moderation/puzzle/[id]/approve/+server.ts deleted file mode 100644 index b195ef69..00000000 --- a/libs/frontend/src/routes/api/moderation/puzzle/[id]/approve/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; - -export async function POST({ params, request }: RequestEvent) { - const { id } = params; - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.moderationPuzzleApprove(id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.POST, - body: JSON.stringify({}) - } - ); -} diff --git a/libs/frontend/src/routes/api/moderation/puzzle/[id]/revise/+server.ts b/libs/frontend/src/routes/api/moderation/puzzle/[id]/revise/+server.ts deleted file mode 100644 index b4688b01..00000000 --- a/libs/frontend/src/routes/api/moderation/puzzle/[id]/revise/+server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; - -export async function POST({ params, request }: RequestEvent) { - const { id } = params; - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.moderationPuzzleRevise(id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.POST, - body - } - ); -} diff --git a/libs/frontend/src/routes/api/moderation/report/[id]/resolve/+server.ts b/libs/frontend/src/routes/api/moderation/report/[id]/resolve/+server.ts deleted file mode 100644 index 68ba6b74..00000000 --- a/libs/frontend/src/routes/api/moderation/report/[id]/resolve/+server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; - -export async function POST({ params, request }: RequestEvent) { - const { id } = params; - const body = await request.json(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.moderationReportResolve(id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.POST, - body: JSON.stringify(body) - } - ); -} diff --git a/libs/frontend/src/routes/api/moderation/user/[id]/ban/[type]/+server.ts b/libs/frontend/src/routes/api/moderation/user/[id]/ban/[type]/+server.ts deleted file mode 100644 index bcac1a97..00000000 --- a/libs/frontend/src/routes/api/moderation/user/[id]/ban/[type]/+server.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { env } from "$env/dynamic/private"; -import { error, json } from "@sveltejs/kit"; -import type { RequestHandler } from "./$types"; -import { backendUrls, httpRequestMethod, isBanType } from "types"; - -export const POST: RequestHandler = async ({ request, params, cookies }) => { - const sessionToken = cookies.get("sessionToken"); - const userId = params.id; - const type = params.type; - - if (!sessionToken) { - throw error(401, "Unauthorized"); - } - - if (!userId) { - throw error(400, "User ID is required"); - } - - if (!isBanType(type)) { - throw error(400, "Invalid ban type"); - } - - const body = await request.json(); - const { duration, reason } = body; - - try { - const response = await fetch( - backendUrls.moderationUserByIdBanByType(userId, type), - { - method: httpRequestMethod.POST, - headers: { - "Content-Type": "application/json", - Cookie: `sessionToken=${sessionToken}` - }, - body: JSON.stringify({ duration, reason }) - } - ); - - if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ message: "Failed to ban user" })); - throw error(response.status, errorData.message); - } - - const result = await response.json(); - return json(result); - } catch (err) { - console.error("Error banning user:", err); - if (err instanceof Error && "status" in err) { - throw err; - } - throw error(500, "Internal server error"); - } -}; diff --git a/libs/frontend/src/routes/api/moderation/user/[id]/ban/history/+server.ts b/libs/frontend/src/routes/api/moderation/user/[id]/ban/history/+server.ts deleted file mode 100644 index 9a1ca749..00000000 --- a/libs/frontend/src/routes/api/moderation/user/[id]/ban/history/+server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { apiUrls } from "@/config/api"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ params, cookies }) => { - const sessionToken = cookies.get("sessionToken"); - const userId = params.id; - - if (!sessionToken) { - throw error(401, "Unauthorized"); - } - - if (!userId) { - throw error(400, "User ID is required"); - } - - try { - const response = await fetch(apiUrls.moderationUserByIdBanHistory(userId), { - method: "GET", - headers: { - Cookie: `sessionToken=${sessionToken}` - } - }); - - if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ message: "Failed to fetch ban history" })); - throw error(response.status, errorData.message); - } - - const result = await response.json(); - return json(result); - } catch (err) { - console.error("Error fetching ban history:", err); - if (err instanceof Error && "status" in err) { - throw err; - } - throw error(500, "Internal server error"); - } -}; diff --git a/libs/frontend/src/routes/api/moderation/user/[id]/unban/+server.ts b/libs/frontend/src/routes/api/moderation/user/[id]/unban/+server.ts deleted file mode 100644 index eb557a3e..00000000 --- a/libs/frontend/src/routes/api/moderation/user/[id]/unban/+server.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { env } from "$env/dynamic/private"; -import { error, json } from "@sveltejs/kit"; -import type { RequestHandler } from "./$types"; -import { apiUrls } from "@/config/api"; - -export const POST: RequestHandler = async ({ request, params, cookies }) => { - const sessionToken = cookies.get("sessionToken"); - const userId = params.id; - - if (!sessionToken) { - throw error(401, "Unauthorized"); - } - - if (!userId) { - throw error(400, "User ID is required"); - } - - const body = await request.json(); - const { reason } = body; - - try { - const response = await fetch(apiUrls.moderationUserByIdUnban(userId), { - method: "POST", - headers: { - "Content-Type": "application/json", - Cookie: `sessionToken=${sessionToken}` - }, - body: JSON.stringify({ reason }) - }); - - if (!response.ok) { - const errorData = await response - .json() - .catch(() => ({ message: "Failed to unban user" })); - throw error(response.status, errorData.message); - } - - const result = await response.json(); - return json(result); - } catch (err) { - console.error("Error unbanning user:", err); - if (err instanceof Error && "status" in err) { - throw err; - } - throw error(500, "Internal server error"); - } -}; diff --git a/libs/frontend/src/routes/api/programming-languages/+server.ts b/libs/frontend/src/routes/api/programming-languages/+server.ts deleted file mode 100644 index a1fe88d9..00000000 --- a/libs/frontend/src/routes/api/programming-languages/+server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import { buildBackendUrl } from "@/config/backend"; -import type { RequestEvent } from "./$types"; -import { getCookieHeader } from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { json } from "@sveltejs/kit"; - -export async function GET({ fetch, request }: RequestEvent) { - const response = await fetch( - buildBackendUrl(backendUrls.PROGRAMMING_LANGUAGE), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); - - const { languages } = await response.json(); - - return json({ languages }); -} diff --git a/libs/frontend/src/routes/api/puzzles/[id]/comment/+server.ts b/libs/frontend/src/routes/api/puzzles/[id]/comment/+server.ts deleted file mode 100644 index 1545872f..00000000 --- a/libs/frontend/src/routes/api/puzzles/[id]/comment/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ params, request }: RequestEvent) { - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.puzzleByIdComment(params.id)), - { - body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} diff --git a/libs/frontend/src/routes/api/submission/[id]/+server.ts b/libs/frontend/src/routes/api/submission/[id]/+server.ts deleted file mode 100644 index 4c9b8d8e..00000000 --- a/libs/frontend/src/routes/api/submission/[id]/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; -import type { RequestEvent } from "./$types"; - -export async function GET({ params, request }: RequestEvent) { - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.submissionById(params.id)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); -} diff --git a/libs/frontend/src/routes/api/submit-code/+server.ts b/libs/frontend/src/routes/api/submit-code/+server.ts deleted file mode 100644 index 1ab10c84..00000000 --- a/libs/frontend/src/routes/api/submit-code/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ request }: RequestEvent) { - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.SUBMISSION), - { - body: body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} diff --git a/libs/frontend/src/routes/api/submit-game/+server.ts b/libs/frontend/src/routes/api/submit-game/+server.ts deleted file mode 100644 index b9d7e68c..00000000 --- a/libs/frontend/src/routes/api/submit-game/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { buildBackendUrl } from "@/config/backend"; - -export async function POST({ request }: RequestEvent) { - const body = await request.text(); - - return await fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.SUBMISSION_GAME), - { - body: body, - headers: getCookieHeader(request), - method: httpRequestMethod.POST - } - ); -} diff --git a/libs/frontend/src/routes/api/supported-languages/+server.ts b/libs/frontend/src/routes/api/supported-languages/+server.ts deleted file mode 100644 index 1f0005e6..00000000 --- a/libs/frontend/src/routes/api/supported-languages/+server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { backendUrls, httpRequestMethod } from "types"; -import { buildBackendUrl } from "@/config/backend"; -import type { RequestEvent } from "./$types"; -import { getCookieHeader } from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { json } from "@sveltejs/kit"; - -export async function GET({ fetch, request }: RequestEvent) { - const response = await fetch( - buildBackendUrl(backendUrls.PROGRAMMING_LANGUAGE), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); - - const { languages } = await response.json(); - - const uniqueLanguages = Array.from( - new Set(languages.map((lang: { language: string }) => lang.language)) - ).sort(); - - return json({ languages: uniqueLanguages }); -} diff --git a/libs/frontend/src/routes/api/user/[username]/+server.ts b/libs/frontend/src/routes/api/user/[username]/+server.ts deleted file mode 100644 index 888b1272..00000000 --- a/libs/frontend/src/routes/api/user/[username]/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; - -export async function GET({ params, request }: RequestEvent) { - return fetchWithAuthenticationCookie( - buildBackendUrl(backendUrls.userByUsername(params.username)), - { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - } - ); -} diff --git a/libs/frontend/src/routes/api/user/[username]/puzzle/+server.ts b/libs/frontend/src/routes/api/user/[username]/puzzle/+server.ts deleted file mode 100644 index fb97e7da..00000000 --- a/libs/frontend/src/routes/api/user/[username]/puzzle/+server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { - fetchWithAuthenticationCookie, - getCookieHeader -} from "@/features/authentication/utils/fetch-with-authentication-cookie"; -import { backendUrls, httpRequestMethod } from "types"; -import type { RequestEvent } from "./$types"; - -export async function GET({ params, request, url }: RequestEvent) { - const username = params.username; - const userPuzzlesByUsernameUrl = buildBackendUrl( - backendUrls.userByUsernamePuzzle(username) - ); - - return fetchWithAuthenticationCookie(userPuzzlesByUsernameUrl + url.search, { - headers: getCookieHeader(request), - method: httpRequestMethod.GET - }); -} diff --git a/libs/frontend/src/routes/api/username-is-available/[username]/+server.ts b/libs/frontend/src/routes/api/username-is-available/[username]/+server.ts deleted file mode 100644 index c827b212..00000000 --- a/libs/frontend/src/routes/api/username-is-available/[username]/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { buildBackendUrl } from "@/config/backend"; -import { backendUrls } from "types"; -import type { RequestEvent } from "./$types"; - -export async function GET({ fetch, params }: RequestEvent) { - const username = params.username; - - return fetch( - buildBackendUrl(backendUrls.userByUsernameIsAvailable(username)) - ); -} diff --git a/libs/frontend/src/routes/leaderboards/+page.svelte b/libs/frontend/src/routes/leaderboards/+page.svelte new file mode 100644 index 00000000..e10eda2b --- /dev/null +++ b/libs/frontend/src/routes/leaderboards/+page.svelte @@ -0,0 +1,282 @@ + + + +

Leaderboards

+ + + + {#each Object.values(gameModeEnum) as mode} + + {/each} + + + + {#if loading} +
+ +
+ {/if} + + + {#if error} + + +

{error}

+
+
+ {/if} + + + {#if leaderboardData && !loading} + + + {gameModeNames[selectedMode]} Leaderboard + + Last updated: {formatDate(leaderboardData.lastUpdated)} + + + + + + + Rank + Player + Rating + Games + Win Rate + Best Score + Avg Score + + + + {#each leaderboardData.entries as entry} + + + {getRankBadge(entry.rank)} + #{entry.rank} + + + + {entry.username} + + + + + {Math.round(entry.rating)} + + + (±{Math.round(entry.glicko.rd)}) + + + +
+ {entry.gamesPlayed} + {entry.gamesWon}W +
+
+ +
+
+
+
+ + {(entry.winRate * 100).toFixed(1)}% + +
+
+ + {Math.round(entry.bestScore).toLocaleString()} + + + {Math.round(entry.averageScore).toLocaleString()} + +
+ {/each} +
+
+
+ +

+ Showing {(currentPage - 1) * pageSize + 1} to {Math.min( + currentPage * pageSize, + leaderboardData.totalEntries + )} of {leaderboardData.totalEntries} players +

+ + + + Page {currentPage} of {leaderboardData.totalPages} + + + +
+
+ {/if} +
diff --git a/libs/frontend/src/routes/maintenance/+page.svelte b/libs/frontend/src/routes/maintenance/+page.svelte index ff426dff..6ee100e5 100644 --- a/libs/frontend/src/routes/maintenance/+page.svelte +++ b/libs/frontend/src/routes/maintenance/+page.svelte @@ -3,7 +3,7 @@ import H1 from "@/components/typography/h1.svelte"; import P from "@/components/typography/p.svelte"; import Button from "@/components/ui/button/button.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; diff --git a/libs/frontend/src/routes/moderation/+page.server.ts b/libs/frontend/src/routes/moderation/+page.server.ts index 77d8cc18..6b726018 100644 --- a/libs/frontend/src/routes/moderation/+page.server.ts +++ b/libs/frontend/src/routes/moderation/+page.server.ts @@ -5,7 +5,9 @@ import { } from "@/features/authentication/utils/fetch-with-authentication-cookie"; import { backendUrls, + ERROR_MESSAGES, httpRequestMethod, + PAGINATION_CONFIG, reviewItemTypeEnum, type ReviewItem } from "types"; @@ -25,8 +27,10 @@ export async function load({ request, url }: PageServerLoadEvent) { // Get query parameters const type = url.searchParams.get("type") || reviewItemTypeEnum.PENDING_PUZZLE; - const page = url.searchParams.get("page") || "1"; - const limit = url.searchParams.get("limit") || "20"; + const page = + url.searchParams.get("page") || String(PAGINATION_CONFIG.DEFAULT_PAGE); + const limit = + url.searchParams.get("limit") || String(PAGINATION_CONFIG.DEFAULT_LIMIT); // Fetch review items from backend const reviewUrl = `${buildBackendUrl(backendUrls.MODERATION_REVIEW)}?type=${type}&page=${page}&limit=${limit}`; @@ -41,10 +45,15 @@ export async function load({ request, url }: PageServerLoadEvent) { return { reviewItems: { data: [], - pagination: { page: 1, limit: 20, total: 0, totalPages: 0 } + pagination: { + page: PAGINATION_CONFIG.DEFAULT_PAGE, + limit: PAGINATION_CONFIG.DEFAULT_LIMIT, + total: 0, + totalPages: 0 + } } as PaginatedResponse, currentType: type, - error: "Failed to fetch review items" + error: ERROR_MESSAGES.MODERATION.FAILED_TO_FETCH_REVIEW_ITEMS }; } @@ -59,10 +68,15 @@ export async function load({ request, url }: PageServerLoadEvent) { return { reviewItems: { data: [], - pagination: { page: 1, limit: 20, total: 0, totalPages: 0 } + pagination: { + page: PAGINATION_CONFIG.DEFAULT_PAGE, + limit: PAGINATION_CONFIG.DEFAULT_LIMIT, + total: 0, + totalPages: 0 + } } as PaginatedResponse, currentType: type, - error: "Failed to fetch review items" + error: ERROR_MESSAGES.MODERATION.FAILED_TO_FETCH_REVIEW_ITEMS }; } } diff --git a/libs/frontend/src/routes/moderation/+page.svelte b/libs/frontend/src/routes/moderation/+page.svelte index 9d10685a..5c2da904 100644 --- a/libs/frontend/src/routes/moderation/+page.svelte +++ b/libs/frontend/src/routes/moderation/+page.svelte @@ -20,8 +20,9 @@ import { Button } from "#/ui/button"; import { page } from "$app/state"; import { formattedDateYearMonthDay } from "@/utils/date-functions"; - import { testIds } from "@/config/test-ids"; - import { apiUrls } from "@/config/api"; + import { testIds } from "types"; + import { buildBackendUrl } from "@/config/backend"; + import { backendUrls } from "types"; import Pagination from "#/nav/pagination.svelte"; let { data }: { data: PageData } = $props(); @@ -79,9 +80,12 @@ async function handleApprove(id: string) { try { - const response = await fetch(apiUrls.moderationPuzzleByIdApprove(id), { - method: httpRequestMethod.POST - }); + const response = await fetch( + buildBackendUrl(backendUrls.moderationPuzzleApprove(id)), + { + method: httpRequestMethod.POST + } + ); if (!response.ok) { throw new Error("Failed to approve puzzle"); @@ -114,7 +118,7 @@ try { const response = await fetch( - apiUrls.moderationPuzzleByIdRevise(selectedPuzzleId), + buildBackendUrl(backendUrls.moderationPuzzleRevise(selectedPuzzleId)), { method: httpRequestMethod.POST, headers: { @@ -142,13 +146,16 @@ status: typeof reviewStatusEnum.RESOLVED | typeof reviewStatusEnum.REJECTED ) { try { - const response = await fetch(apiUrls.moderationReportByIdResolve(id), { - method: httpRequestMethod.POST, - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ status }) - }); + const response = await fetch( + buildBackendUrl(backendUrls.moderationReportResolve(id)), + { + method: httpRequestMethod.POST, + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ status }) + } + ); if (!response.ok) { throw new Error("Failed to resolve report"); @@ -184,7 +191,9 @@ try { const response = await fetch( - apiUrls.moderationUserByIdBanByType(selectedUserId, banType), + buildBackendUrl( + backendUrls.moderationUserByIdBanByType(selectedUserId, banType) + ), { method: httpRequestMethod.POST, headers: { @@ -219,15 +228,18 @@ } try { - const response = await fetch(apiUrls.moderationUserByIdUnban(userId), { - method: httpRequestMethod.POST, - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - reason: "Unbanned by moderator" - }) - }); + const response = await fetch( + buildBackendUrl(backendUrls.moderationUserByIdUnban(userId)), + { + method: httpRequestMethod.POST, + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + reason: "Unbanned by moderator" + }) + } + ); if (!response.ok) { throw new Error("Failed to unban user"); @@ -249,7 +261,7 @@ try { const response = await fetch( - apiUrls.moderationUserByIdBanHistory(userId), + buildBackendUrl(backendUrls.moderationUserByIdBanHistory(userId)), { method: httpRequestMethod.GET } diff --git a/libs/frontend/src/routes/profile/[username]/+page.server.ts b/libs/frontend/src/routes/profile/[username]/+page.server.ts index 0ae90207..d717ff4f 100644 --- a/libs/frontend/src/routes/profile/[username]/+page.server.ts +++ b/libs/frontend/src/routes/profile/[username]/+page.server.ts @@ -1,11 +1,19 @@ -import { getUserActivityByUsername } from "../../api/get-user-activity-by-username.js"; -import { activityTypeEnum, type PuzzleDto, type SubmissionDto } from "types"; +import { + activityTypeEnum, + backendUrls, + type PuzzleDto, + type SubmissionDto +} from "types"; import type { PageServerLoadEvent } from "./$types"; +import { buildBackendUrl } from "@/config/backend"; +import { fetchWithAuthenticationCookie } from "@/features/authentication/utils/fetch-with-authentication-cookie"; export async function load({ params }: PageServerLoadEvent) { const username = params.username; - const response = await getUserActivityByUsername(username); + const response = await fetchWithAuthenticationCookie( + buildBackendUrl(backendUrls.userByUsernameActivity(username)) + ); if (!response.ok) { console.error(response); } diff --git a/libs/frontend/src/routes/profile/[username]/+page.svelte b/libs/frontend/src/routes/profile/[username]/+page.svelte index 823da1bd..971642a5 100644 --- a/libs/frontend/src/routes/profile/[username]/+page.svelte +++ b/libs/frontend/src/routes/profile/[username]/+page.svelte @@ -5,7 +5,7 @@ import H1 from "@/components/typography/h1.svelte"; import * as Card from "@/components/ui/card"; import Container from "@/components/ui/container/container.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import ActivityGroup from "@/features/profile/components/activity-group.svelte"; import ActivityHeatmap from "@/features/profile/components/activity-heatmap.svelte"; import dayjs from "dayjs"; diff --git a/libs/frontend/src/routes/profile/[username]/puzzles/+page.server.ts b/libs/frontend/src/routes/profile/[username]/puzzles/+page.server.ts index 20bada8b..3ca37d15 100644 --- a/libs/frontend/src/routes/profile/[username]/puzzles/+page.server.ts +++ b/libs/frontend/src/routes/profile/[username]/puzzles/+page.server.ts @@ -1,6 +1,7 @@ import type { PageServerLoadEvent } from "./$types"; import { httpRequestMethod, type PaginatedQueryResponse } from "types"; -import { apiUrls } from "@/config/api"; +import { buildBackendUrl } from "@/config/backend"; +import { backendUrls } from "types"; export async function load({ fetch, @@ -10,7 +11,7 @@ export async function load({ }: PageServerLoadEvent) { const username = params.username; - const apiUrl = apiUrls.userByUsernamePuzzle(username); + const apiUrl = buildBackendUrl(backendUrls.userByUsernamePuzzle(username)); const apiUrlWithQueryParams = new URL(apiUrl, request.url); apiUrlWithQueryParams.search = url.search; diff --git a/libs/frontend/src/routes/profile/[username]/puzzles/+page.svelte b/libs/frontend/src/routes/profile/[username]/puzzles/+page.svelte index 27efdf85..aa9f3554 100644 --- a/libs/frontend/src/routes/profile/[username]/puzzles/+page.svelte +++ b/libs/frontend/src/routes/profile/[username]/puzzles/+page.svelte @@ -14,7 +14,7 @@ import LogicalUnit from "@/components/ui/logical-unit/logical-unit.svelte"; import PuzzleDifficultyBadge from "@/features/puzzles/components/puzzle-difficulty-badge.svelte"; import PuzzleVisibilityBadge from "@/features/puzzles/components/puzzle-visibility-badge.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { authenticatedUserInfo } from "@/stores/index.js"; let { data }: { data: PaginatedQueryResponse | undefined } = $props(); diff --git a/libs/frontend/src/routes/puzzles/+page.svelte b/libs/frontend/src/routes/puzzles/+page.svelte index 4758e823..0c283b9a 100644 --- a/libs/frontend/src/routes/puzzles/+page.svelte +++ b/libs/frontend/src/routes/puzzles/+page.svelte @@ -12,7 +12,7 @@ import LogicalUnit from "@/components/ui/logical-unit/logical-unit.svelte"; import PuzzleDifficultyBadge from "@/features/puzzles/components/puzzle-difficulty-badge.svelte"; import PuzzleVisibilityBadge from "@/features/puzzles/components/puzzle-visibility-badge.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { authenticatedUserInfo, isAuthenticated } from "@/stores"; let { data }: { data: PaginatedQueryResponse | undefined } = $props(); diff --git a/libs/frontend/src/routes/puzzles/[id]/+page.svelte b/libs/frontend/src/routes/puzzles/[id]/+page.svelte index 53125e15..4546a033 100644 --- a/libs/frontend/src/routes/puzzles/[id]/+page.svelte +++ b/libs/frontend/src/routes/puzzles/[id]/+page.svelte @@ -20,7 +20,7 @@ import H2 from "@/components/typography/h2.svelte"; import Comments from "@/features/comment/components/comments.svelte"; import AddCommentForm from "@/features/comment/components/add-comment-form.svelte"; - import { testIds } from "@/config/test-ids"; + import { testIds } from "types"; import { page } from "$app/state"; let { data } = $props(); diff --git a/libs/types/src/core/api/schema/account.schema.ts b/libs/types/src/core/api/schema/account.schema.ts new file mode 100644 index 00000000..3a1b25a1 --- /dev/null +++ b/libs/types/src/core/api/schema/account.schema.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { preferencesDtoSchema } from "../../preferences/schema/preferences-dto.schema.js"; +import { preferencesEntitySchema } from "../../preferences/schema/preferences-entity.schema.js"; +import { messageSchema } from "../../common/schema/message.schema.js"; + +// GET /account/preferences response +export const getPreferencesResponseSchema = preferencesDtoSchema; +export type GetPreferencesResponse = z.infer< + typeof getPreferencesResponseSchema +>; + +// POST /account/preferences request +export const createPreferencesRequestSchema = preferencesEntitySchema; +export type CreatePreferencesRequest = z.infer< + typeof createPreferencesRequestSchema +>; + +// POST /account/preferences response +export const createPreferencesResponseSchema = z.object({ + message: messageSchema, + preferences: preferencesDtoSchema, +}); +export type CreatePreferencesResponse = z.infer< + typeof createPreferencesResponseSchema +>; + +// PUT /account/preferences request +export const updatePreferencesRequestSchema = preferencesEntitySchema.partial(); +export type UpdatePreferencesRequest = z.infer< + typeof updatePreferencesRequestSchema +>; + +// PUT /account/preferences response +export const updatePreferencesResponseSchema = z.object({ + message: messageSchema, + preferences: preferencesDtoSchema, +}); +export type UpdatePreferencesResponse = z.infer< + typeof updatePreferencesResponseSchema +>; + +// DELETE /account/preferences response +export const deletePreferencesResponseSchema = z.object({ + message: messageSchema, +}); +export type DeletePreferencesResponse = z.infer< + typeof deletePreferencesResponseSchema +>; diff --git a/libs/types/src/core/api/schema/auth/login.schema.ts b/libs/types/src/core/api/schema/auth/login.schema.ts new file mode 100644 index 00000000..a428620a --- /dev/null +++ b/libs/types/src/core/api/schema/auth/login.schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { messageSchema } from "../../../common/schema/message.schema.js"; + +/** + * POST /login - User login + */ +export const loginRequestSchema = z.object({ + identifier: z.string().min(1, "Identifier is required"), + password: z.string().min(1, "Password is required"), +}); + +export const loginResponseSchema = messageSchema; + +export type LoginRequest = z.infer; +export type LoginResponse = z.infer; diff --git a/libs/types/src/core/api/schema/auth/logout.schema.ts b/libs/types/src/core/api/schema/auth/logout.schema.ts new file mode 100644 index 00000000..6b4cf51c --- /dev/null +++ b/libs/types/src/core/api/schema/auth/logout.schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; +import { messageSchema } from "../../../common/schema/message.schema.js"; + +/** + * POST /logout - User logout + */ +export const logoutResponseSchema = messageSchema; + +export type LogoutResponse = z.infer; diff --git a/libs/types/src/core/api/schema/auth/register.schema.ts b/libs/types/src/core/api/schema/auth/register.schema.ts new file mode 100644 index 00000000..e2f63b51 --- /dev/null +++ b/libs/types/src/core/api/schema/auth/register.schema.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { messageSchema } from "../../../common/schema/message.schema.js"; +import { USERNAME_CONFIG } from "../../../authentication/config/username-config.js"; +import { PASSWORD_CONFIG } from "../../../authentication/config/password-config.js"; + +export const registerRequestSchema = z.object({ + username: z + .string() + .min( + USERNAME_CONFIG.minUsernameLength, + `Username must be at least ${USERNAME_CONFIG.minUsernameLength} characters`, + ) + .max( + USERNAME_CONFIG.maxUsernameLength, + `Username cannot exceed ${USERNAME_CONFIG.maxUsernameLength} characters`, + ) + .regex( + USERNAME_CONFIG.allowedCharacters, + "Username can only contain letters, numbers, underscores, and hyphens", + ), + email: z.string().email("Invalid email address").optional(), + password: z + .string() + .min( + PASSWORD_CONFIG.minPasswordLength, + `Password must be at least ${PASSWORD_CONFIG.minPasswordLength} characters`, + ), +}); + +export const registerResponseSchema = messageSchema; + +export type RegisterRequest = z.infer; +export type RegisterResponse = z.infer; diff --git a/libs/types/src/core/api/schema/comment.schema.ts b/libs/types/src/core/api/schema/comment.schema.ts new file mode 100644 index 00000000..9a73dbe4 --- /dev/null +++ b/libs/types/src/core/api/schema/comment.schema.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { commentDtoSchema } from "../../comment/schema/comment-dto.schema.js"; +import { createCommentSchema } from "../../comment/schema/create-comment.schema.js"; +import { commentVoteRequestSchema } from "../../comment/schema/comment-vote.schema.js"; +import { messageSchema } from "../../common/schema/message.schema.js"; + +// GET /comment/:id response +export const getCommentByIdResponseSchema = commentDtoSchema; +export type GetCommentByIdResponse = z.infer< + typeof getCommentByIdResponseSchema +>; + +// DELETE /comment/:id response +export const deleteCommentResponseSchema = z.object({ + message: messageSchema, +}); +export type DeleteCommentResponse = z.infer; + +// POST /comment/:id/comment request +export const createReplyCommentRequestSchema = createCommentSchema; +export type CreateReplyCommentRequest = z.infer< + typeof createReplyCommentRequestSchema +>; + +// POST /comment/:id/comment response +export const createReplyCommentResponseSchema = commentDtoSchema; +export type CreateReplyCommentResponse = z.infer< + typeof createReplyCommentResponseSchema +>; + +// POST /comment/:id/vote request +export const voteCommentRequestSchema = commentVoteRequestSchema; +export type VoteCommentRequest = z.infer; + +// POST /comment/:id/vote response +export const voteCommentResponseSchema = z.object({ + message: messageSchema, + voteCount: z.number(), +}); +export type VoteCommentResponse = z.infer; diff --git a/libs/types/src/core/api/schema/execute-code.schema.ts b/libs/types/src/core/api/schema/execute-code.schema.ts new file mode 100644 index 00000000..50c91212 --- /dev/null +++ b/libs/types/src/core/api/schema/execute-code.schema.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { pistonExecutionRequestSchema } from "../../piston/schema/request.js"; +import { codeExecutionResponseSchema } from "../../piston/schema/code-execution-response.js"; + +// Request schema for code execution +export const executeCodeRequestSchema = pistonExecutionRequestSchema; +export type ExecuteCodeRequest = z.infer; + +// Response schema for code execution +export const executeCodeResponseSchema = codeExecutionResponseSchema; +export type ExecuteCodeResponse = z.infer; diff --git a/libs/types/src/core/api/schema/execute/execute-api.schema.ts b/libs/types/src/core/api/schema/execute/execute-api.schema.ts new file mode 100644 index 00000000..edc4ca92 --- /dev/null +++ b/libs/types/src/core/api/schema/execute/execute-api.schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { pistonExecutionRequestSchema } from "../../../piston/schema/request.js"; +import { codeExecutionResponseSchema } from "../../../piston/schema/code-execution-response.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * POST /execute - Execute code without saving submission + * Used for testing code before final submission + */ +export const executeCodeRequestSchema = z.object({ + code: z.string().min(1, "Code cannot be empty"), + language: z.string().min(1, "Language is required"), + testInput: z.string().default(""), + testOutput: z.string().default(""), +}); + +export const executeCodeResponseSchema = + codeExecutionResponseSchema.or(errorResponseSchema); + +export type ExecuteCodeRequest = z.infer; +export type ExecuteCodeResponse = z.infer; diff --git a/libs/types/src/core/api/schema/game/game-api.schema.ts b/libs/types/src/core/api/schema/game/game-api.schema.ts new file mode 100644 index 00000000..10894bed --- /dev/null +++ b/libs/types/src/core/api/schema/game/game-api.schema.ts @@ -0,0 +1,118 @@ +import { z } from "zod"; +import { objectIdSchema } from "../../../common/schema/object-id.js"; +import { gameEntitySchema } from "../../../game/schema/game-entity.schema.js"; +import { gameModeSchema } from "../../../game/schema/mode.schema.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; +import { gameVisibilitySchema } from "../../../game/schema/visibility.schema.js"; +import { MINIMUM_PLAYERS_IN_GAME } from "../../../game/config/game-config.js"; + +/** + * POST /game - Create a new multiplayer game + */ +export const createGameRequestSchema = z.object({ + puzzleId: objectIdSchema.optional(), + mode: gameModeSchema, + visibility: gameVisibilitySchema, + maxPlayers: z.number().int().min(MINIMUM_PLAYERS_IN_GAME), + timeLimit: z.number().int().min(60).max(3600).optional(), // in seconds +}); + +export const createGameResponseSchema = gameEntitySchema + .extend({ + inviteCode: z.string().optional(), // For private games + }) + .or(errorResponseSchema); + +export type CreateGameRequest = z.infer; +export type CreateGameResponse = z.infer; + +/** + * GET /game/:id - Get game details + */ +export const getGameByIdRequestSchema = z.object({ + id: objectIdSchema, +}); + +export const getGameByIdResponseSchema = + gameEntitySchema.or(errorResponseSchema); + +export type GetGameByIdRequest = z.infer; +export type GetGameByIdResponse = z.infer; + +/** + * GET /game - List available games + */ +export const listGamesRequestSchema = z.object({ + visibility: gameVisibilitySchema.optional(), + mode: gameModeSchema.optional(), + status: z.enum(["waiting", "in_progress", "completed"]).optional(), + page: z.number().int().positive().default(1), + pageSize: z.number().int().positive().max(50).default(20), +}); + +export const listGamesResponseSchema = z + .object({ + items: z.array(gameEntitySchema), + page: z.number().int().positive(), + pageSize: z.number().int().positive(), + totalPages: z.number().int().nonnegative(), + totalItems: z.number().int().nonnegative(), + }) + .or(errorResponseSchema); + +export type ListGamesRequest = z.infer; +export type ListGamesResponse = z.infer; + +/** + * POST /game/:id/join - Join a game + */ +export const joinGameRequestSchema = z.object({ + gameId: objectIdSchema, + inviteCode: z.string().optional(), +}); + +export const joinGameResponseSchema = z + .object({ + success: z.boolean(), + message: z.string(), + game: gameEntitySchema.optional(), + }) + .or(errorResponseSchema); + +export type JoinGameRequest = z.infer; +export type JoinGameResponse = z.infer; + +/** + * POST /game/:id/leave - Leave a game + */ +export const leaveGameRequestSchema = z.object({ + gameId: objectIdSchema, +}); + +export const leaveGameResponseSchema = z + .object({ + success: z.boolean(), + message: z.string(), + }) + .or(errorResponseSchema); + +export type LeaveGameRequest = z.infer; +export type LeaveGameResponse = z.infer; + +/** + * POST /game/:id/start - Start a game (host only) + */ +export const startGameRequestSchema = z.object({ + gameId: objectIdSchema, +}); + +export const startGameResponseSchema = z + .object({ + success: z.boolean(), + message: z.string(), + startTime: z.date().or(z.string()).optional(), + }) + .or(errorResponseSchema); + +export type StartGameRequest = z.infer; +export type StartGameResponse = z.infer; diff --git a/libs/types/src/core/api/schema/game/leaderboard-api.schema.ts b/libs/types/src/core/api/schema/game/leaderboard-api.schema.ts new file mode 100644 index 00000000..08db8c74 --- /dev/null +++ b/libs/types/src/core/api/schema/game/leaderboard-api.schema.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import { objectIdSchema } from "../../../common/schema/object-id.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; +import { gameModeSchema } from "../../../game/schema/mode.schema.js"; +import { acceptedDateSchema } from "../../../common/schema/accepted-date.js"; + +/** + * GET /game/:id/leaderboard - Get ranked leaderboard for a game + */ +export const getGameLeaderboardRequestSchema = z.object({ + gameId: objectIdSchema, +}); + +export const getGameLeaderboardResponseSchema = z + .object({ + gameId: objectIdSchema, + mode: gameModeSchema, + leaderboard: z.array( + z.object({ + userId: objectIdSchema, + username: z.string(), + score: z.number(), + timeSpent: z.number(), // in seconds + codeLength: z.number().int().nonnegative().optional(), + successRate: z.number().min(0).max(1), + rank: z.number().int().positive(), + programmingLanguage: z.string().optional(), + }), + ), + totalPlayers: z.number().int().nonnegative(), + }) + .or(errorResponseSchema); + +export type GetGameLeaderboardRequest = z.infer< + typeof getGameLeaderboardRequestSchema +>; +export type GetGameLeaderboardResponse = z.infer< + typeof getGameLeaderboardResponseSchema +>; + +/** + * GET /game/:id/stats - Get game statistics and metadata + */ +export const getGameStatsRequestSchema = z.object({ + gameId: objectIdSchema, +}); + +export const getGameStatsResponseSchema = z + .object({ + gameId: objectIdSchema, + mode: gameModeSchema, + description: z.string(), + displayMetrics: z.array(z.string()), + playerCount: z.number().int().nonnegative(), + submissionCount: z.number().int().nonnegative(), + createdAt: acceptedDateSchema, + options: z + .object({ + mode: gameModeSchema, + maxPlayers: z.number().int().positive(), + timeLimit: z.number().int().positive().optional(), + }) + .optional(), + }) + .or(errorResponseSchema); + +export type GetGameStatsRequest = z.infer; +export type GetGameStatsResponse = z.infer; diff --git a/libs/types/src/core/api/schema/leaderboard/leaderboard-api.schema.ts b/libs/types/src/core/api/schema/leaderboard/leaderboard-api.schema.ts new file mode 100644 index 00000000..8e01c12d --- /dev/null +++ b/libs/types/src/core/api/schema/leaderboard/leaderboard-api.schema.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { gameModeSchema } from "../../../game/schema/mode.schema.js"; +import { leaderboardEntrySchema } from "../../../leaderboard/schema/leaderboard-entry.schema.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * GET /leaderboard/:gameMode - Get leaderboard for a specific game mode + */ +export const getLeaderboardRequestSchema = z.object({ + gameMode: gameModeSchema, + page: z.number().int().positive().default(1), + pageSize: z.number().int().positive().max(100).default(50), +}); + +export const getLeaderboardResponseSchema = z + .object({ + gameMode: gameModeSchema, + entries: z.array(leaderboardEntrySchema), + page: z.number().int().positive(), + pageSize: z.number().int().positive(), + totalEntries: z.number().int().nonnegative(), + totalPages: z.number().int().nonnegative(), + lastUpdated: z.date().or(z.string()), + }) + .or(errorResponseSchema); + +export type GetLeaderboardRequest = z.infer; +export type GetLeaderboardResponse = z.infer< + typeof getLeaderboardResponseSchema +>; + +/** + * GET /leaderboard/user/:userId - Get user's rankings across all game modes + */ +export const getUserLeaderboardStatsRequestSchema = z.object({ + userId: z.string(), +}); + +export const getUserLeaderboardStatsResponseSchema = z + .object({ + userId: z.string(), + username: z.string(), + rankings: z.record( + z.string(), // Game mode as string key + z.object({ + rank: z.number().int().positive().optional(), + rating: z.number(), + gamesPlayed: z.number().int().nonnegative(), + winRate: z.number().min(0).max(1), + }), + ), + }) + .or(errorResponseSchema); + +export type GetUserLeaderboardStatsRequest = z.infer< + typeof getUserLeaderboardStatsRequestSchema +>; +export type GetUserLeaderboardStatsResponse = z.infer< + typeof getUserLeaderboardStatsResponseSchema +>; diff --git a/libs/types/src/core/api/schema/moderation.schema.ts b/libs/types/src/core/api/schema/moderation.schema.ts new file mode 100644 index 00000000..b6199785 --- /dev/null +++ b/libs/types/src/core/api/schema/moderation.schema.ts @@ -0,0 +1,101 @@ +import { z } from "zod"; +import { messageSchema } from "../../common/schema/message.schema.js"; +import { paginatedQueryResponseSchema } from "../../common/schema/paginated-query-response.schema.js"; +import { paginatedQuerySchema } from "../../common/schema/paginated-query.schema.js"; +import { reviewItemSchema } from "../../moderation/schema/review-item.schema.js"; +import { + approvePuzzleSchema, + revisePuzzleSchema, +} from "../../moderation/schema/puzzle-moderation.schema.js"; +import { reportEntitySchema } from "../../moderation/schema/report.schema.js"; +import { userBanEntitySchema } from "../../moderation/schema/user-ban.schema.js"; + +// GET /moderation/review query params +export const getModerationReviewQuerySchema = paginatedQuerySchema; +export type GetModerationReviewQuery = z.infer< + typeof getModerationReviewQuerySchema +>; + +// GET /moderation/review response +export const getModerationReviewResponseSchema = + paginatedQueryResponseSchema.extend({ + items: z.array(reviewItemSchema), + }); +export type GetModerationReviewResponse = z.infer< + typeof getModerationReviewResponseSchema +>; + +// POST /moderation/puzzle/:id/approve request +export const approvePuzzleRequestSchema = approvePuzzleSchema; +export type ApprovePuzzleRequest = z.infer; + +// POST /moderation/puzzle/:id/approve response +export const approvePuzzleResponseSchema = z.object({ + message: messageSchema, +}); +export type ApprovePuzzleResponse = z.infer; + +// POST /moderation/puzzle/:id/revise request +export const revisePuzzleRequestSchema = revisePuzzleSchema; +export type RevisePuzzleRequest = z.infer; + +// POST /moderation/puzzle/:id/revise response +export const revisePuzzleResponseSchema = z.object({ + message: messageSchema, +}); +export type RevisePuzzleResponse = z.infer; + +// POST /moderation/report/:id/resolve request +export const resolveReportRequestSchema = z.object({ + action: z.enum(["accept", "reject"]), + notes: z.string().optional(), +}); +export type ResolveReportRequest = z.infer; + +// POST /moderation/report/:id/resolve response +export const resolveReportResponseSchema = z.object({ + message: messageSchema, +}); +export type ResolveReportResponse = z.infer; + +// POST /moderation/user/:id/ban/:type request +export const banUserRequestSchema = z.object({ + reason: z.string().min(10, "Reason must be at least 10 characters"), + duration: z.number().positive().optional(), // Duration in seconds, optional for permanent bans +}); +export type BanUserRequest = z.infer; + +// POST /moderation/user/:id/ban/:type response +export const banUserResponseSchema = z.object({ + message: messageSchema, + ban: userBanEntitySchema, +}); +export type BanUserResponse = z.infer; + +// GET /moderation/user/:id/ban/history response +export const getBanHistoryResponseSchema = z.object({ + bans: z.array(userBanEntitySchema), +}); +export type GetBanHistoryResponse = z.infer; + +// POST /moderation/user/:id/unban response +export const unbanUserResponseSchema = z.object({ + message: messageSchema, +}); +export type UnbanUserResponse = z.infer; + +// POST /report request +export const createReportRequestSchema = reportEntitySchema.omit({ + createdAt: true, + updatedAt: true, + status: true, + resolvedBy: true, +}); +export type CreateReportRequest = z.infer; + +// POST /report response +export const createReportResponseSchema = z.object({ + message: messageSchema, + report: reportEntitySchema, +}); +export type CreateReportResponse = z.infer; diff --git a/libs/types/src/core/api/schema/programming-language.schema.ts b/libs/types/src/core/api/schema/programming-language.schema.ts new file mode 100644 index 00000000..4feb6a60 --- /dev/null +++ b/libs/types/src/core/api/schema/programming-language.schema.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { programmingLanguageDtoSchema } from "../../programming-language/schema/programming-language-dto.schema.js"; +import { paginatedQueryResponseSchema } from "../../common/schema/paginated-query-response.schema.js"; + +// GET /programming-language response +export const programmingLanguagesResponseSchema = z.object({ + languages: z.array(programmingLanguageDtoSchema), +}); +export type ProgrammingLanguagesResponse = z.infer< + typeof programmingLanguagesResponseSchema +>; + +// GET /programming-language/:id response +export const programmingLanguageByIdResponseSchema = + programmingLanguageDtoSchema; +export type ProgrammingLanguageByIdResponse = z.infer< + typeof programmingLanguageByIdResponseSchema +>; + +// GET /programming-language/supported (filtered unique languages) +export const supportedLanguagesResponseSchema = z.object({ + languages: z.array(z.string()), +}); +export type SupportedLanguagesResponse = z.infer< + typeof supportedLanguagesResponseSchema +>; diff --git a/libs/types/src/core/api/schema/programming-language/programming-language-api.schema.ts b/libs/types/src/core/api/schema/programming-language/programming-language-api.schema.ts new file mode 100644 index 00000000..b0c7bb39 --- /dev/null +++ b/libs/types/src/core/api/schema/programming-language/programming-language-api.schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { programmingLanguageDtoSchema } from "../../../programming-language/schema/programming-language-dto.schema.js"; +import { objectIdSchema } from "../../../common/schema/object-id.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * GET /programming-language - List all available programming languages + */ +export const getProgrammingLanguagesResponseSchema = z.array( + programmingLanguageDtoSchema, +); + +export type GetProgrammingLanguagesResponse = z.infer< + typeof getProgrammingLanguagesResponseSchema +>; + +/** + * GET /programming-language/:id - Get programming language by ID + */ +export const getProgrammingLanguageByIdRequestSchema = z.object({ + id: objectIdSchema, +}); + +export const getProgrammingLanguageByIdResponseSchema = + programmingLanguageDtoSchema.or(errorResponseSchema); + +export type GetProgrammingLanguageByIdRequest = z.infer< + typeof getProgrammingLanguageByIdRequestSchema +>; +export type GetProgrammingLanguageByIdResponse = z.infer< + typeof getProgrammingLanguageByIdResponseSchema +>; diff --git a/libs/types/src/core/api/schema/puzzle.schema.ts b/libs/types/src/core/api/schema/puzzle.schema.ts new file mode 100644 index 00000000..cb365d23 --- /dev/null +++ b/libs/types/src/core/api/schema/puzzle.schema.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { puzzleDtoSchema } from "../../puzzle/schema/puzzle-dto.schema.js"; +import { paginatedQueryResponseSchema } from "../../common/schema/paginated-query-response.schema.js"; +import { paginatedQuerySchema } from "../../common/schema/paginated-query.schema.js"; +import { commentDtoSchema } from "../../comment/schema/comment-dto.schema.js"; +import { createCommentSchema } from "../../comment/schema/create-comment.schema.js"; +import { solutionSchema } from "../../puzzle/schema/solution.schema.js"; +import { messageSchema } from "../../common/schema/message.schema.js"; + +// GET /puzzle query params +export const getPuzzlesQuerySchema = paginatedQuerySchema; +export type GetPuzzlesQuery = z.infer; + +// GET /puzzle response +export const getPuzzlesResponseSchema = paginatedQueryResponseSchema.extend({ + items: z.array(puzzleDtoSchema), +}); +export type GetPuzzlesResponse = z.infer; + +// GET /puzzle/:id response +export const getPuzzleByIdResponseSchema = puzzleDtoSchema; +export type GetPuzzleByIdResponse = z.infer; + +// POST /puzzle/:id/comment request +export const createPuzzleCommentRequestSchema = createCommentSchema; +export type CreatePuzzleCommentRequest = z.infer< + typeof createPuzzleCommentRequestSchema +>; + +// POST /puzzle/:id/comment response +export const createPuzzleCommentResponseSchema = commentDtoSchema; +export type CreatePuzzleCommentResponse = z.infer< + typeof createPuzzleCommentResponseSchema +>; + +// GET /puzzle/:id/comment query params +export const getPuzzleCommentsQuerySchema = paginatedQuerySchema; +export type GetPuzzleCommentsQuery = z.infer< + typeof getPuzzleCommentsQuerySchema +>; + +// GET /puzzle/:id/comment response +export const getPuzzleCommentsResponseSchema = + paginatedQueryResponseSchema.extend({ + items: z.array(commentDtoSchema), + }); +export type GetPuzzleCommentsResponse = z.infer< + typeof getPuzzleCommentsResponseSchema +>; + +// GET /puzzle/:id/solution response +export const getPuzzleSolutionResponseSchema = z.object({ + solutions: z.array(solutionSchema), +}); +export type GetPuzzleSolutionResponse = z.infer< + typeof getPuzzleSolutionResponseSchema +>; + +// DELETE /puzzle/:id response +export const deletePuzzleResponseSchema = z.object({ + message: messageSchema, +}); +export type DeletePuzzleResponse = z.infer; diff --git a/libs/types/src/core/api/schema/puzzle/puzzle-api.schema.ts b/libs/types/src/core/api/schema/puzzle/puzzle-api.schema.ts new file mode 100644 index 00000000..0ae80ca5 --- /dev/null +++ b/libs/types/src/core/api/schema/puzzle/puzzle-api.schema.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; +import { paginatedQuerySchema } from "../../../common/schema/paginated-query.schema.js"; +import { paginatedQueryResponseSchema } from "../../../common/schema/paginated-query-response.schema.js"; +import { puzzleEntitySchema } from "../../../puzzle/schema/puzzle-entity.schema.js"; +import { objectIdSchema } from "../../../common/schema/object-id.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * GET /puzzle - List puzzles with pagination + */ +export const getPuzzlesRequestSchema = paginatedQuerySchema; + +export const getPuzzlesResponseSchema = paginatedQueryResponseSchema.extend({ + items: z.array(puzzleEntitySchema), +}); + +export type GetPuzzlesRequest = z.infer; +export type GetPuzzlesResponse = z.infer; + +/** + * GET /puzzle/:id - Get single puzzle by ID + */ +export const getPuzzleByIdRequestSchema = z.object({ + id: objectIdSchema, +}); + +export const getPuzzleByIdResponseSchema = + puzzleEntitySchema.or(errorResponseSchema); + +export type GetPuzzleByIdRequest = z.infer; +export type GetPuzzleByIdResponse = z.infer; + +/** + * POST /puzzle - Create new puzzle + */ +export const createPuzzleRequestSchema = z.object({ + title: z.string().min(1).max(200), + description: z.string().min(1), + difficulty: z.enum(["easy", "medium", "hard"]), + validators: z + .array( + z.object({ + input: z.string(), + output: z.string(), + }), + ) + .min(1), + tags: z.array(z.string()).optional(), +}); + +export const createPuzzleResponseSchema = + puzzleEntitySchema.or(errorResponseSchema); + +export type CreatePuzzleRequest = z.infer; +export type CreatePuzzleResponse = z.infer; diff --git a/libs/types/src/core/api/schema/submission.schema.ts b/libs/types/src/core/api/schema/submission.schema.ts new file mode 100644 index 00000000..eca03748 --- /dev/null +++ b/libs/types/src/core/api/schema/submission.schema.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import { submissionDtoSchema } from "../../submission/schema/submission-dto.schema.js"; + +// GET /submission/:id response +export const getSubmissionByIdResponseSchema = submissionDtoSchema; +export type GetSubmissionByIdResponse = z.infer< + typeof getSubmissionByIdResponseSchema +>; + +// POST /submission/game request +export const submitGameRequestSchema = z.object({ + gameId: z.string(), + code: z.string(), + language: z.string(), +}); +export type SubmitGameRequest = z.infer; + +// POST /submission/game response +export const submitGameResponseSchema = submissionDtoSchema; +export type SubmitGameResponse = z.infer; diff --git a/libs/types/src/core/api/schema/submission/submission-api.schema.ts b/libs/types/src/core/api/schema/submission/submission-api.schema.ts new file mode 100644 index 00000000..58c2f6b3 --- /dev/null +++ b/libs/types/src/core/api/schema/submission/submission-api.schema.ts @@ -0,0 +1,108 @@ +import { z } from "zod"; +import { objectIdSchema } from "../../../common/schema/object-id.js"; +import { submissionEntitySchema } from "../../../submission/schema/submission-entity.schema.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * POST /submission - Submit code for evaluation + * This replaces the generic DTO approach with specific types + */ +export const submitCodeRequestSchema = z.object({ + puzzleId: objectIdSchema, + programmingLanguageId: objectIdSchema, + code: z.string().min(1, "Code cannot be empty"), + userId: objectIdSchema, // Should come from authenticated session +}); + +export const submitCodeResponseSchema = z + .object({ + // Specific fields returned on submission + submissionId: z.string(), + code: z.string(), + puzzleId: z.string(), + programmingLanguageId: z.string(), + userId: z.string(), + codeLength: z.number().int().positive(), + result: z.object({ + successRate: z.number().min(0).max(1), + passed: z.number().int().nonnegative(), + failed: z.number().int().nonnegative(), + total: z.number().int().positive(), + }), + createdAt: z.date().or(z.string()), + }) + .or(errorResponseSchema); + +export type SubmitCodeRequest = z.infer; +export type SubmitCodeResponse = z.infer; + +/** + * GET /submission/:id - Get submission by ID + */ +export const getSubmissionByIdRequestSchema = z.object({ + id: objectIdSchema, +}); + +export const getSubmissionByIdResponseSchema = + submissionEntitySchema.or(errorResponseSchema); + +export type GetSubmissionByIdRequest = z.infer< + typeof getSubmissionByIdRequestSchema +>; +export type GetSubmissionByIdResponse = z.infer< + typeof getSubmissionByIdResponseSchema +>; + +/** + * GET /submission - List user submissions + */ +export const listSubmissionsRequestSchema = z.object({ + userId: objectIdSchema.optional(), + puzzleId: objectIdSchema.optional(), + page: z.number().int().positive().default(1), + pageSize: z.number().int().positive().max(100).default(20), +}); + +export const listSubmissionsResponseSchema = z + .object({ + items: z.array(submissionEntitySchema), + page: z.number().int().positive(), + pageSize: z.number().int().positive(), + totalPages: z.number().int().nonnegative(), + totalItems: z.number().int().nonnegative(), + }) + .or(errorResponseSchema); + +export type ListSubmissionsRequest = z.infer< + typeof listSubmissionsRequestSchema +>; +export type ListSubmissionsResponse = z.infer< + typeof listSubmissionsResponseSchema +>; + +/** + * POST /submission/game - Submit an existing submission to a game + */ +export const submitToGameRequestSchema = z.object({ + gameId: objectIdSchema, + submissionId: objectIdSchema, + userId: objectIdSchema, +}); + +export const submitToGameResponseSchema = z + .object({ + success: z.boolean(), + message: z.string(), + game: z + .object({ + id: objectIdSchema, + status: z.enum(["waiting", "in_progress", "completed"]), + playerCount: z.number().int().nonnegative(), + }) + .optional(), + leaderboardPosition: z.number().int().positive().optional(), + }) + .or(errorResponseSchema); + +export type SubmitToGameRequest = z.infer; +export type SubmitToGameResponse = z.infer; diff --git a/libs/types/src/core/api/schema/submit-code.schema.ts b/libs/types/src/core/api/schema/submit-code.schema.ts new file mode 100644 index 00000000..a18354f1 --- /dev/null +++ b/libs/types/src/core/api/schema/submit-code.schema.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; +import { submissionEntitySchema } from "../../submission/schema/submission-entity.schema.js"; +import { messageSchema } from "../../common/schema/message.schema.js"; + +// Submit code request - includes puzzle ID, code, language +export const submitCodeRequestSchema = z.object({ + puzzleId: z.string(), + code: z.string(), + language: z.string(), +}); +export type SubmitCodeRequest = z.infer; + +// Submit code response - returns submission details +export const submitCodeResponseSchema = submissionEntitySchema; +export type SubmitCodeResponse = z.infer; diff --git a/libs/types/src/core/api/schema/user.schema.ts b/libs/types/src/core/api/schema/user.schema.ts new file mode 100644 index 00000000..8190e625 --- /dev/null +++ b/libs/types/src/core/api/schema/user.schema.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { userDtoSchema } from "../../user/schema/user-dto.schema.js"; +import { userActivitySchema } from "../../user/schema/user-activity.schema.js"; +import { paginatedQueryResponseSchema } from "../../common/schema/paginated-query-response.schema.js"; +import { paginatedQuerySchema } from "../../common/schema/paginated-query.schema.js"; +import { puzzleDtoSchema } from "../../puzzle/schema/puzzle-dto.schema.js"; + +// GET /user/:username response +export const getUserByUsernameResponseSchema = userDtoSchema; +export type GetUserByUsernameResponse = z.infer< + typeof getUserByUsernameResponseSchema +>; + +// GET /user/:username/puzzle query +export const getUserPuzzlesQuerySchema = paginatedQuerySchema; +export type GetUserPuzzlesQuery = z.infer; + +// GET /user/:username/puzzle response +export const getUserPuzzlesResponseSchema = paginatedQueryResponseSchema.extend( + { + items: z.array(puzzleDtoSchema), + }, +); +export type GetUserPuzzlesResponse = z.infer< + typeof getUserPuzzlesResponseSchema +>; + +// GET /user/:username/activity response +export const getUserActivityResponseSchema = z.array(userActivitySchema); +export type GetUserActivityResponse = z.infer< + typeof getUserActivityResponseSchema +>; + +// GET /user/:username/isAvailable response +export const usernameIsAvailableResponseSchema = z.object({ + available: z.boolean(), +}); +export type UsernameIsAvailableResponse = z.infer< + typeof usernameIsAvailableResponseSchema +>; diff --git a/libs/types/src/core/api/schema/user/user-api.schema.ts b/libs/types/src/core/api/schema/user/user-api.schema.ts new file mode 100644 index 00000000..79fe6768 --- /dev/null +++ b/libs/types/src/core/api/schema/user/user-api.schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { userDtoSchema } from "../../../user/schema/user-dto.schema.js"; +import { errorResponseSchema } from "../../../common/schema/error-response.schema.js"; + +/** + * GET /user/me - Get current user information + */ +export const getCurrentUserResponseSchema = + userDtoSchema.or(errorResponseSchema); + +export type GetCurrentUserResponse = z.infer< + typeof getCurrentUserResponseSchema +>; diff --git a/libs/types/src/core/comment/schema/comment-dto.schema.ts b/libs/types/src/core/comment/schema/comment-dto.schema.ts index 887dda4a..c8204259 100644 --- a/libs/types/src/core/comment/schema/comment-dto.schema.ts +++ b/libs/types/src/core/comment/schema/comment-dto.schema.ts @@ -4,7 +4,7 @@ import { commentEntitySchema } from "./comment-entity.schema.js"; export const commentDtoSchema = commentEntitySchema.extend({ _id: objectIdSchema, - comments: z.array(objectIdSchema), + comments: z.array(objectIdSchema).optional(), parentId: objectIdSchema, }); diff --git a/libs/types/src/core/common/config/backend-urls.ts b/libs/types/src/core/common/config/backend-urls.ts index 740f8c10..057f618e 100644 --- a/libs/types/src/core/common/config/backend-urls.ts +++ b/libs/types/src/core/common/config/backend-urls.ts @@ -43,6 +43,12 @@ export const backendUrls = { submissionById: (id: string) => `${baseRoute}/submission/${id}`, SUBMISSION_GAME: `${baseRoute}/submission/game`, + // leaderboard routes + LEADERBOARD_RECALCULATE: `${baseRoute}/leaderboard/recalculate`, + leaderboardByGameMode: (gameMode: string) => + `${baseRoute}/leaderboard/${gameMode}`, + leaderboardUserById: (id: string) => `${baseRoute}/leaderboard/user/${id}`, + // moderation routes MODERATION_REVIEW: `${baseRoute}/moderation/review`, moderationPuzzleApprove: (id: string) => @@ -64,4 +70,5 @@ export const backendParams = { USERNAME: ":username", ID: ":id", TYPE: ":type", + GAME_MODE: ":gameMode", } as const; diff --git a/libs/types/src/core/common/config/error-messages.ts b/libs/types/src/core/common/config/error-messages.ts new file mode 100644 index 00000000..b871276d --- /dev/null +++ b/libs/types/src/core/common/config/error-messages.ts @@ -0,0 +1,40 @@ +export const ERROR_MESSAGES = { + FORM: { + VALIDATION_ERRORS: "Form validation errors", + REQUIRED_FIELD: "This field is required", + }, + FETCH: { + FAILED_TO_LOAD: "Failed to load data", + FAILED_TO_FETCH: "Failed to fetch", + NETWORK_ERROR: "Network error occurred", + }, + AUTHENTICATION: { + INVALID_CREDENTIALS: "Invalid email/username or password", + UNAUTHORIZED: "You are not authorized to perform this action", + SESSION_EXPIRED: "Your session has expired. Please login again", + AUTHENTICATION_REQUIRED: "Authentication required", + }, + MODERATION: { + FAILED_TO_FETCH_REVIEW_ITEMS: "Failed to fetch review items", + }, + PUZZLE: { + FAILED_TO_DELETE: "Failed to delete the puzzle", + FAILED_TO_UPDATE: "Failed to update the puzzle", + FAILED_TO_CREATE: "Failed to create the puzzle", + NOT_FOUND: "Puzzle not found", + }, + GAME: { + NOT_FOUND: "Game not found", + USER_NOT_IN_GAME: "User not in this game", + ALREADY_FINISHED: "Game has already finished", + FAILED_TO_START: "Failed to start game", + }, + SERVER: { + INTERNAL_ERROR: "Internal server error", + DATABASE_ERROR: "Database error occurred", + }, + GENERIC: { + SOMETHING_WENT_WRONG: "Something went wrong", + TRY_AGAIN_LATER: "Please try again later", + }, +} as const; diff --git a/libs/types/src/core/common/config/pagination.ts b/libs/types/src/core/common/config/pagination.ts new file mode 100644 index 00000000..df83bb6b --- /dev/null +++ b/libs/types/src/core/common/config/pagination.ts @@ -0,0 +1,11 @@ +/** + * Default pagination configuration values + */ +export const PAGINATION_CONFIG = { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 20, + DEFAULT_LIMIT_LEADERBOARD: 50, + MIN_PAGE: 1, + MIN_LIMIT: 1, + MAX_LIMIT: 100, +} as const; diff --git a/libs/frontend/src/lib/config/test-ids.ts b/libs/types/src/core/common/config/test-ids.ts similarity index 85% rename from libs/frontend/src/lib/config/test-ids.ts rename to libs/types/src/core/common/config/test-ids.ts index eb8e30db..a9cf167c 100644 --- a/libs/frontend/src/lib/config/test-ids.ts +++ b/libs/types/src/core/common/config/test-ids.ts @@ -1,4 +1,4 @@ -import type { ValueOf } from "types"; +import type { ValueOf } from "../types/value-of.js"; export const DATA_TESTID_STRING = "data-testid"; @@ -52,6 +52,7 @@ export const testIds = { // error component ERROR_COMPONENT_ANCHOR_HOMEPAGE: "error-component-anchor-homepage", + ERROR_COMPONENT_BUTTON_RELOAD: "error-component-button-reload", // login form LOGIN_FORM_BUTTON_LOGIN: "login-form-button-login", @@ -77,6 +78,8 @@ export const testIds = { MULTIPLAYER_PAGE_BUTTON_JOIN_BY_INVITE: "multiplayer-page-button-join-by-invite", MULTIPLAYER_PAGE_BUTTON_COPY_INVITE: "multiplayer-page-button-copy-invite", + MULTIPLAYER_PAGE_BUTTON_TOGGLE_INVITE_CODE: + "multiplayer-page-button-toggle-invite-code", // custom game dialog CUSTOM_GAME_DIALOG_BUTTON_CANCEL: "custom-game-dialog-button-cancel", @@ -86,6 +89,26 @@ export const testIds = { JOIN_BY_INVITE_DIALOG_BUTTON_CANCEL: "join-by-invite-dialog-button-cancel", JOIN_BY_INVITE_DIALOG_BUTTON_JOIN: "join-by-invite-dialog-button-join", + // leaderboard page + LEADERBOARD_PAGE_BUTTON_MODE_FASTEST: "leaderboard-page-button-mode-fastest", + LEADERBOARD_PAGE_BUTTON_MODE_SHORTEST: + "leaderboard-page-button-mode-shortest", + LEADERBOARD_PAGE_BUTTON_MODE_BACKWARDS: + "leaderboard-page-button-mode-backwards", + LEADERBOARD_PAGE_BUTTON_MODE_HARDCORE: + "leaderboard-page-button-mode-hardcore", + LEADERBOARD_PAGE_BUTTON_MODE_DEBUG: "leaderboard-page-button-mode-debug", + LEADERBOARD_PAGE_BUTTON_MODE_TYPERACER: + "leaderboard-page-button-mode-typeracer", + LEADERBOARD_PAGE_BUTTON_MODE_EFFICIENCY: + "leaderboard-page-button-mode-efficiency", + LEADERBOARD_PAGE_BUTTON_MODE_INCREMENTAL: + "leaderboard-page-button-mode-incremental", + LEADERBOARD_PAGE_BUTTON_MODE_RANDOM: "leaderboard-page-button-mode-random", + LEADERBOARD_PAGE_BUTTON_PREVIOUS_PAGE: + "leaderboard-page-button-previous-page", + LEADERBOARD_PAGE_BUTTON_NEXT_PAGE: "leaderboard-page-button-next-page", + // waiting room chat WAITING_ROOM_CHAT_BUTTON_SEND: "waiting-room-chat-button-send", @@ -170,7 +193,7 @@ export const testIds = { // user hover card component USER_HOVER_CARD_COMPONENT_ANCHOR_USER_PROFILE: - "user-hover-card-component-anchor-user-profile" + "user-hover-card-component-anchor-user-profile", } as const; type DataTestIdMap = typeof testIds; diff --git a/libs/types/src/core/common/schema/error-response.schema.ts b/libs/types/src/core/common/schema/error-response.schema.ts index 2b06d0b3..0d0192fc 100644 --- a/libs/types/src/core/common/schema/error-response.schema.ts +++ b/libs/types/src/core/common/schema/error-response.schema.ts @@ -3,6 +3,7 @@ import { z } from "zod"; export const errorResponseSchema = z.object({ message: z.string(), error: z.string(), + details: z.array(z.any()).optional(), // Validation error details }); export type ErrorResponse = z.infer; diff --git a/libs/types/src/core/game/enum/game-mode-enum.ts b/libs/types/src/core/game/enum/game-mode-enum.ts index 032e58b4..6f66645b 100644 --- a/libs/types/src/core/game/enum/game-mode-enum.ts +++ b/libs/types/src/core/game/enum/game-mode-enum.ts @@ -1,5 +1,20 @@ export const gameModeEnum = { - FASTEST: "fastest", - SHORTEST: "shortest", - RANDOM: "random", + // Core modes + FASTEST: "fastest", // Complete the task in the shortest time + SHORTEST: "shortest", // Write the least amount of code (characters) + + // Challenge modes + BACKWARDS: "backwards", // Work from output to input + HARDCORE: "hardcore", // One attempt only, no tries + DEBUG: "debug", // Fix broken code instead of writing from scratch + TYPERACER: "typeracer", // Copy code perfectly, fastest wins + + // Advanced modes + EFFICIENCY: "efficiency", // Focus on writing the most efficient code (computationally or memory) + INCREMENTAL: "incremental", // Requirements added each minute + + // Special modes + RANDOM: "random", // Randomized game mode } as const; + +// dont fucking add random game modes that don't belong diff --git a/libs/types/src/core/game/enum/game-tournament-enum.ts b/libs/types/src/core/game/enum/game-tournament-enum.ts new file mode 100644 index 00000000..3368259f --- /dev/null +++ b/libs/types/src/core/game/enum/game-tournament-enum.ts @@ -0,0 +1,5 @@ +export const gameTournamentStyleEnum = { + BEST_OF_THREE: "best_of_three", // Multiple rounds, win 2 out of 3 + ELIMINATION: "elimination", // Tournament bracket style + BATTLE_ROYALE: "battle_royale", // Many players, last one standing +} as const; diff --git a/libs/types/src/core/game/schema/game-entity.schema.ts b/libs/types/src/core/game/schema/game-entity.schema.ts index da7298fa..06800356 100644 --- a/libs/types/src/core/game/schema/game-entity.schema.ts +++ b/libs/types/src/core/game/schema/game-entity.schema.ts @@ -3,7 +3,7 @@ import { gameOptionsSchema } from "./game-options.schema.js"; import { userDtoSchema } from "../../user/schema/user-dto.schema.js"; import { puzzleDtoSchema } from "../../puzzle/schema/puzzle-dto.schema.js"; import { acceptedDateSchema } from "../../common/schema/accepted-date.js"; -import { submissionDtoSchema } from "../../submission/schema/submission-dto.schema.js"; +import { gameSubmissionSchema } from "./game-submission.schema.js"; import { objectIdSchema } from "../../common/schema/object-id.js"; export const gameEntitySchema = z.object({ @@ -15,7 +15,7 @@ export const gameEntitySchema = z.object({ options: gameOptionsSchema, createdAt: acceptedDateSchema, playerSubmissions: z - .array(objectIdSchema.or(submissionDtoSchema)) + .array(objectIdSchema.or(gameSubmissionSchema)) .prefault([]), }); export type GameEntity = z.infer; diff --git a/libs/types/src/core/game/schema/game-submission.schema.ts b/libs/types/src/core/game/schema/game-submission.schema.ts new file mode 100644 index 00000000..d6a33ea3 --- /dev/null +++ b/libs/types/src/core/game/schema/game-submission.schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; +import { submissionDtoSchema } from "../../submission/schema/submission-dto.schema.js"; + +/** + * Extended submission schema specifically for game submissions. + * Includes computed fields like codeLength that are added by the backend. + */ +export const gameSubmissionSchema = submissionDtoSchema.extend({ + codeLength: z.number().int().nonnegative().optional(), + timeSpent: z.number().nonnegative().optional(), // Time spent in seconds +}); + +export type GameSubmission = z.infer; + +export function isGameSubmission(data: unknown): data is GameSubmission { + return gameSubmissionSchema.safeParse(data).success; +} diff --git a/libs/types/src/core/game/schema/mode.schema.ts b/libs/types/src/core/game/schema/mode.schema.ts index 878f9a01..be67f34e 100644 --- a/libs/types/src/core/game/schema/mode.schema.ts +++ b/libs/types/src/core/game/schema/mode.schema.ts @@ -2,7 +2,11 @@ import { z } from "zod"; import { getValues } from "../../../utils/functions/get-values.js"; import { gameModeEnum } from "../enum/game-mode-enum.js"; -export const gameModeSchema = z - .enum(getValues(gameModeEnum)) - .prefault(gameModeEnum.FASTEST); +export const gameModes = getValues(gameModeEnum); + +export const gameModeSchema = z.enum(gameModes).prefault(gameModeEnum.FASTEST); export type GameMode = z.infer; + +export function isGameMode(data: unknown): data is GameMode { + return gameModeSchema.safeParse(data).success; +} diff --git a/libs/types/src/core/game/schema/waiting-room-response.schema.ts b/libs/types/src/core/game/schema/waiting-room-response.schema.ts index cfbaf40f..40fc15a0 100644 --- a/libs/types/src/core/game/schema/waiting-room-response.schema.ts +++ b/libs/types/src/core/game/schema/waiting-room-response.schema.ts @@ -52,7 +52,7 @@ const chatMessageResponseSchema = z.object({ event: z.literal(waitingRoomEventEnum.CHAT_MESSAGE), username: z.string(), message: z.string(), - timestamp: acceptedDateSchema, + createdAt: acceptedDateSchema, }); export const waitingRoomResponseSchema = z.discriminatedUnion("event", [ diff --git a/libs/types/src/core/leaderboard/config/leaderboard-config.ts b/libs/types/src/core/leaderboard/config/leaderboard-config.ts new file mode 100644 index 00000000..f08c0664 --- /dev/null +++ b/libs/types/src/core/leaderboard/config/leaderboard-config.ts @@ -0,0 +1,24 @@ +/** + * Leaderboard configuration and thresholds + */ +export const LEADERBOARD_CONFIG = { + RATING_THRESHOLDS: { + LEGENDARY: 2000, + MASTER: 1800, + EXPERT: 1600, + ADVANCED: 1400, + BEGINNER: 0, + }, + COLORS: { + LEGENDARY: "text-purple-600 dark:text-purple-400", + MASTER: "text-blue-600 dark:text-blue-400", + EXPERT: "text-green-600 dark:text-green-400", + ADVANCED: "text-yellow-600 dark:text-yellow-400", + BEGINNER: "text-gray-600 dark:text-gray-400", + }, + MEDALS: { + FIRST: "🥇", + SECOND: "🥈", + THIRD: "🥉", + }, +} as const; diff --git a/libs/types/src/core/leaderboard/schema/leaderboard-entry.schema.ts b/libs/types/src/core/leaderboard/schema/leaderboard-entry.schema.ts new file mode 100644 index 00000000..bbcf21b8 --- /dev/null +++ b/libs/types/src/core/leaderboard/schema/leaderboard-entry.schema.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; +import { objectIdSchema } from "../../common/schema/object-id.js"; +import { glickoRatingSchema } from "./user-metrics.schema.js"; + +/** + * Single entry in a leaderboard + */ +export const leaderboardEntrySchema = z.object({ + rank: z.number().int().positive(), + userId: objectIdSchema, + username: z.string(), + rating: z.number(), + glicko: glickoRatingSchema, + gamesPlayed: z.number().int().nonnegative(), + gamesWon: z.number().int().nonnegative(), + winRate: z.number().min(0).max(1), + bestScore: z.number().nonnegative(), + averageScore: z.number().nonnegative(), +}); + +export type LeaderboardEntry = z.infer; diff --git a/libs/types/src/core/leaderboard/schema/user-metrics.schema.ts b/libs/types/src/core/leaderboard/schema/user-metrics.schema.ts new file mode 100644 index 00000000..959af68e --- /dev/null +++ b/libs/types/src/core/leaderboard/schema/user-metrics.schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; +import { objectIdSchema } from "../../common/schema/object-id.js"; +import { gameModeSchema } from "../../game/schema/mode.schema.js"; +import { acceptedDateSchema } from "../../common/schema/accepted-date.js"; + +/** + * Glicko-2 rating components + */ +export const glickoRatingSchema = z.object({ + rating: z.number().default(1500), // Base rating + rd: z.number().default(350), // Rating deviation + volatility: z.number().default(0.06), // Volatility + lastUpdated: acceptedDateSchema.default(() => new Date()), +}); + +export type GlickoRating = z.infer; + +/** + * User metrics per game mode + */ +export const gameModeMetricsSchema = z.object({ + gamesPlayed: z.number().int().nonnegative().default(0), + gamesWon: z.number().int().nonnegative().default(0), + bestScore: z.number().nonnegative().default(0), + averageScore: z.number().nonnegative().default(0), + totalScore: z.number().nonnegative().default(0), + glickoRating: glickoRatingSchema, + rank: z.number().int().positive().optional(), // Position in leaderboard + lastGameDate: acceptedDateSchema.optional(), +}); + +export type GameModeMetrics = z.infer; + +/** + * User metrics entity - stores aggregated performance data + */ +export const userMetricsEntitySchema = z.object({ + _id: objectIdSchema.optional(), + userId: objectIdSchema, + + // Metrics per game mode + fastest: gameModeMetricsSchema.optional(), + shortest: gameModeMetricsSchema.optional(), + backwards: gameModeMetricsSchema.optional(), + hardcore: gameModeMetricsSchema.optional(), + debug: gameModeMetricsSchema.optional(), + typeracer: gameModeMetricsSchema.optional(), + efficiency: gameModeMetricsSchema.optional(), + incremental: gameModeMetricsSchema.optional(), + random: gameModeMetricsSchema.optional(), + + // Overall stats + totalGamesPlayed: z.number().int().nonnegative().default(0), + totalGamesWon: z.number().int().nonnegative().default(0), + + // Tracking for incremental updates + lastProcessedGameDate: acceptedDateSchema.default(() => new Date(0)), // Epoch + lastCalculationDate: acceptedDateSchema.default(() => new Date()), + + createdAt: acceptedDateSchema.default(() => new Date()), + updatedAt: acceptedDateSchema.default(() => new Date()), +}); + +export type UserMetricsEntity = z.infer; diff --git a/libs/types/src/core/programming-language/schema/create-programming-language.schema.ts b/libs/types/src/core/programming-language/schema/create-programming-language.schema.ts index 236c8bea..bf723e49 100644 --- a/libs/types/src/core/programming-language/schema/create-programming-language.schema.ts +++ b/libs/types/src/core/programming-language/schema/create-programming-language.schema.ts @@ -1,8 +1,5 @@ import { z } from "zod"; -/** - * Schema for creating a new programming language - */ export const createProgrammingLanguageSchema = z.object({ language: z.string().min(1, "Language name is required"), version: z.string().min(1, "Language version is required"), diff --git a/libs/types/src/core/puzzle/schema/puzzle-dto.schema.ts b/libs/types/src/core/puzzle/schema/puzzle-dto.schema.ts index 340178d7..8694b612 100644 --- a/libs/types/src/core/puzzle/schema/puzzle-dto.schema.ts +++ b/libs/types/src/core/puzzle/schema/puzzle-dto.schema.ts @@ -18,6 +18,5 @@ export const puzzleDtoSchema = basePuzzleDtoSchema.extend({ export type PuzzleDto = z.infer; export function isPuzzleDto(data: unknown): data is PuzzleDto { - console.log({ result: puzzleDtoSchema.safeParse(data) }); return puzzleDtoSchema.safeParse(data).success; } diff --git a/libs/types/src/core/submission/schema/submission-entity.schema.ts b/libs/types/src/core/submission/schema/submission-entity.schema.ts index ef9f58b1..47147ebb 100644 --- a/libs/types/src/core/submission/schema/submission-entity.schema.ts +++ b/libs/types/src/core/submission/schema/submission-entity.schema.ts @@ -8,11 +8,13 @@ import { puzzleResultInformationSchema } from "../../piston/schema/puzzle-result export const submissionEntitySchema = z.object({ code: z.string().optional(), - codeLength: z.number().optional(), + // codelenght shouldn't be added here, since it should be derived from the code itself + // codelength should also be returned by a more specific dto/schema instead of this one, this one is too generic programmingLanguage: objectIdSchema.or(programmingLanguageDtoSchema), createdAt: acceptedDateSchema.prefault(() => new Date()), puzzle: objectIdSchema.or(puzzleDtoSchema), result: puzzleResultInformationSchema, user: objectIdSchema.or(userDtoSchema), }); + export type SubmissionEntity = z.infer; diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 086dca1d..8d180873 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -36,7 +36,10 @@ export * from "./core/common/config/backend-urls.js"; export * from "./core/common/config/cookie.js"; export * from "./core/common/config/environment.js"; export * from "./core/common/config/default-values-query-params.js"; +export * from "./core/common/config/error-messages.js"; export * from "./core/common/config/frontend-urls.js"; +export * from "./core/common/config/pagination.js"; +export * from "./core/common/config/test-ids.js"; export * from "./core/common/config/web-socket-urls.js"; export * from "./core/common/enum/websocket-close-codes.js"; export * from "./core/common/enum/http-response-codes.js"; @@ -60,6 +63,7 @@ export * from "./core/game/enum/game-visibility-enum.js"; export * from "./core/game/schema/game-dto.schema.js"; export * from "./core/game/schema/game-entity.schema.js"; export * from "./core/game/schema/game-user-info.schema.js"; +export * from "./core/game/schema/game-submission.schema.js"; export * from "./core/game/schema/waiting-room-request.schema.js"; export * from "./core/game/schema/waiting-room-response.schema.js"; export * from "./core/game/schema/game-request.schema.js"; @@ -68,6 +72,11 @@ export * from "./core/game/schema/mode.schema.js"; export * from "./core/game/schema/game-options.schema.js"; export * from "./core/game/schema/visibility.schema.js"; +// leaderboard +export * from "./core/leaderboard/config/leaderboard-config.js"; +export * from "./core/leaderboard/schema/user-metrics.schema.js"; +export * from "./core/leaderboard/schema/leaderboard-entry.schema.js"; + // moderation export * from "./core/moderation/config/report-config.js"; export * from "./core/moderation/config/ban-config.js"; @@ -143,6 +152,29 @@ export * from "./core/user/schema/user-entity.schema.js"; export * from "./core/user/schema/user-vote-entity.schema.js"; export * from "./core/user/schema/user-profile.schema.js"; +// api - request/response types for all endpoints +export * from "./core/api/schema/execute-code.schema.js"; +export * from "./core/api/schema/submit-code.schema.js"; +export * from "./core/api/schema/programming-language.schema.js"; +export * from "./core/api/schema/user.schema.js"; +export * from "./core/api/schema/comment.schema.js"; +export * from "./core/api/schema/submission.schema.js"; +export * from "./core/api/schema/puzzle.schema.js"; +export * from "./core/api/schema/account.schema.js"; +export * from "./core/api/schema/moderation.schema.js"; + +// New specific API endpoint types (v2 - more specific, less generic) +export * as AuthAPI from "./core/api/schema/auth/login.schema.js"; +export * as RegisterAPI from "./core/api/schema/auth/register.schema.js"; +export * as LogoutAPI from "./core/api/schema/auth/logout.schema.js"; +export * as PuzzleAPI from "./core/api/schema/puzzle/puzzle-api.schema.js"; +export * as UserAPI from "./core/api/schema/user/user-api.schema.js"; +export * as ProgrammingLanguageAPI from "./core/api/schema/programming-language/programming-language-api.schema.js"; +export * as SubmissionAPI from "./core/api/schema/submission/submission-api.schema.js"; +export * as ExecuteAPI from "./core/api/schema/execute/execute-api.schema.js"; +export * as GameAPI from "./core/api/schema/game/game-api.schema.js"; +export * as LeaderboardAPI from "./core/api/schema/leaderboard/leaderboard-api.schema.js"; + // utils - constants export * from "./utils/constants/http-methods.js"; diff --git a/libs/types/src/utils/functions/get-user-id-from-user.ts b/libs/types/src/utils/functions/get-user-id-from-user.ts index 57ed52f3..5b43cc43 100644 --- a/libs/types/src/utils/functions/get-user-id-from-user.ts +++ b/libs/types/src/utils/functions/get-user-id-from-user.ts @@ -9,7 +9,7 @@ export function getUserIdFromUser(user: unknown): string | null { } if (typeof user === "object" && user !== null && "_id" in user) { - const id = (user as any)._id; + const id = user._id; return id ? String(id) : null; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d535280..85153c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@fastify/websocket': specifier: ^10.0.1 version: 10.0.1 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 bcryptjs: specifier: ^3.0.2 version: 3.0.2 @@ -50,6 +53,9 @@ importers: mongoose: specifier: ^8.5.1 version: 8.19.2 + node-cron: + specifier: ^4.2.1 + version: 4.2.1 zod: specifier: ^4.1.12 version: 4.1.12 @@ -106,6 +112,22 @@ importers: specifier: ^3.0.8 version: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + libs/e2e: + dependencies: + types: + specifier: workspace:* + version: link:../types + devDependencies: + '@playwright/test': + specifier: ^1.48.2 + version: 1.56.1 + '@types/node': + specifier: ^22.10.2 + version: 22.18.13 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + libs/frontend: dependencies: '@codemirror/autocomplete': @@ -162,9 +184,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - codemirror: - specifier: ^6.0.1 - version: 6.0.2 codemirror-lang-elixir: specifier: ^4.0.0 version: 4.0.0 @@ -193,9 +212,12 @@ importers: '@eslint/js': specifier: ^9.7.0 version: 9.38.0 + '@internationalized/date': + specifier: ^3.10.0 + version: 3.10.0 '@lucide/svelte': - specifier: ^0.548.0 - version: 0.548.0(svelte@5.41.4) + specifier: ^0.552.0 + version: 0.552.0(svelte@5.41.4) '@sveltejs/adapter-node': specifier: 5.2.12 version: 5.2.12(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))) @@ -227,8 +249,8 @@ importers: specifier: ^8.22.0 version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) bits-ui: - specifier: ^1.8.0 - version: 1.8.0(svelte@5.41.4) + specifier: ^2.14.1 + version: 2.14.1(@internationalized/date@3.10.0)(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4) eslint: specifier: ^9.10.0 version: 9.38.0(jiti@2.6.1) @@ -1136,8 +1158,8 @@ packages: '@lezer/rust@1.0.2': resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} - '@lucide/svelte@0.548.0': - resolution: {integrity: sha512-Iwh5GXK8+tE1lBjYoBPfOhBiWv6/K/XinZ/Bjx/2Qys86ufZ/CbjHKacYyZeGnyWQSsjcr7P3xavkmCMhv/1RA==} + '@lucide/svelte@0.552.0': + resolution: {integrity: sha512-8wQF1YUKgaXiFidPdHM5NKESArKHLrgf8A1EAOjvqRmdVlL2KFsPxTchez/lMUOJKxXeDgrRKfXQuaN1O5KlhQ==} peerDependencies: svelte: ^5 @@ -1166,6 +1188,11 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@playwright/test@1.56.1': + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1574,6 +1601,12 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + + '@types/node@22.18.13': + resolution: {integrity: sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==} + '@types/node@24.9.1': resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} @@ -1805,11 +1838,12 @@ packages: birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} - bits-ui@1.8.0: - resolution: {integrity: sha512-CXD6Orp7l8QevNDcRPLXc/b8iMVgxDWT2LyTwsdLzJKh9CxesOmPuNePSPqAxKoT59FIdU4aFPS1k7eBdbaCxg==} - engines: {node: '>=18', pnpm: '>=8.7.0'} + bits-ui@2.14.1: + resolution: {integrity: sha512-FkQTBDF+BLh5fgwioi04JJD8kpsQ+pVjPwzxUYYd39pYR0uKAyMLmoKo3EAWJIszV7fjTv1ZiNzxTkpGutJ32w==} + engines: {node: '>=20'} peerDependencies: - svelte: ^5.11.0 + '@internationalized/date': ^3.8.1 + svelte: ^5.33.0 blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -1884,9 +1918,6 @@ packages: codemirror-lang-prolog@0.1.0: resolution: {integrity: sha512-l8UvvCy3ub9kHbREFPG44xhHNG/AuCwkQEbLANfppHi1qZEWdr59ChSo4ZVu5XmC4PrHH3aMUHF+E2KS/V+LpA==} - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - 'codin-cod@file:': resolution: {directory: '', type: directory} @@ -1933,9 +1964,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1967,6 +1995,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2229,6 +2261,11 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2511,6 +2548,10 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2633,6 +2674,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-cron@4.2.1: + resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} + engines: {node: '>=6.0.0'} + normalize-url@8.1.0: resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} @@ -2723,6 +2768,16 @@ packages: typescript: optional: true + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} + hasBin: true + postcss-load-config@3.1.4: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} @@ -2926,6 +2981,15 @@ packages: peerDependencies: svelte: ^5.7.0 + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -3070,6 +3134,12 @@ packages: peerDependencies: svelte: ^5.0.0 + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} + engines: {node: '>=18', pnpm: '>=8.7.0'} + peerDependencies: + svelte: ^5.30.2 + svelte-toolbelt@0.4.6: resolution: {integrity: sha512-k8OUvXBUifHZcAlWeY/HLg/4J0v5m2iOfOhn8fDmjt4AP8ZluaDh9eBFus9lFiLX6O5l6vKqI1dKL5wy7090NQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -3227,6 +3297,9 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -4152,7 +4225,7 @@ snapshots: '@lezer/highlight': 1.2.2 '@lezer/lr': 1.4.2 - '@lucide/svelte@0.548.0(svelte@5.41.4)': + '@lucide/svelte@0.552.0(svelte@5.41.4)': dependencies: svelte: 5.41.4 @@ -4178,6 +4251,10 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@playwright/test@1.56.1': + dependencies: + playwright: 1.56.1 + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.5': @@ -4534,6 +4611,12 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node-cron@3.0.11': {} + + '@types/node@22.18.13': + dependencies: + undici-types: 6.21.0 + '@types/node@24.9.1': dependencies: undici-types: 7.16.0 @@ -4803,17 +4886,18 @@ snapshots: birpc@0.2.14: {} - bits-ui@1.8.0(svelte@5.41.4): + bits-ui@2.14.1(@internationalized/date@3.10.0)(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 - css.escape: 1.5.1 esm-env: 1.2.2 - runed: 0.23.4(svelte@5.41.4) + runed: 0.35.1(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4) svelte: 5.41.4 - svelte-toolbelt: 0.7.1(svelte@5.41.4) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4) tabbable: 6.3.0 + transitivePeerDependencies: + - '@sveltejs/kit' blake3-wasm@2.1.5: {} @@ -4889,16 +4973,6 @@ snapshots: '@lezer/highlight': 1.2.2 '@lezer/lr': 1.4.2 - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.19.1 - '@codemirror/commands': 6.10.0 - '@codemirror/language': 6.11.3 - '@codemirror/lint': 6.9.1 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.6 - 'codin-cod@file:': {} color-convert@2.0.1: @@ -4937,8 +5011,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css.escape@1.5.1: {} - cssesc@3.0.0: {} dayjs@1.11.18: {} @@ -4955,6 +5027,8 @@ snapshots: defu@6.1.4: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} devalue@5.4.2: {} @@ -5319,6 +5393,9 @@ snapshots: forwarded@0.2.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5560,6 +5637,8 @@ snapshots: loupe@3.2.1: {} + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5693,6 +5772,8 @@ snapshots: natural-compare@1.4.0: {} + node-cron@4.2.1: {} + normalize-url@8.1.0: optional: true @@ -5789,6 +5870,14 @@ snapshots: optionalDependencies: typescript: 5.9.3 + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + postcss-load-config@3.1.4(postcss@8.5.6): dependencies: lilconfig: 2.1.0 @@ -5936,6 +6025,15 @@ snapshots: esm-env: 1.2.2 svelte: 5.41.4 + runed@0.35.1(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4): + dependencies: + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.41.4 + optionalDependencies: + '@sveltejs/kit': 2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + sade@1.8.1: dependencies: mri: 1.2.0 @@ -6094,6 +6192,15 @@ snapshots: runed: 0.28.0(svelte@5.41.4) svelte: 5.41.4 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4): + dependencies: + clsx: 2.1.1 + runed: 0.35.1(@sveltejs/kit@2.47.3(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4)(vite@7.1.12(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.41.4) + style-to-object: 1.0.12 + svelte: 5.41.4 + transitivePeerDependencies: + - '@sveltejs/kit' + svelte-toolbelt@0.4.6(svelte@5.41.4): dependencies: clsx: 2.1.1 @@ -6257,6 +6364,8 @@ snapshots: ufo@1.6.1: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} undici@7.14.0: {}