import { z } from "zod"; import { type TFunction, type Namespace } from "react-i18next"; import { type RouteMatch } from "@remix-run/react"; import type navItems from "~/components/layout/nav-items.json"; export function notFoundIfFalsy(value: T | null | undefined): T { if (!value) throw new Response(null, { status: 404 }); return value; } export function notFoundIfNullLike(value: T | null | undefined): T { if (value === null || value === undefined) throw new Response(null, { status: 404 }); return value; } export function badRequestIfFalsy(value: T | null | undefined): T { if (!value) throw new Response(null, { status: 400 }); return value; } /** Parse formData of a request with the given schema. Throws HTTP 400 response if fails. */ export async function parseRequestFormData({ request, schema, }: { request: Request; schema: T; }): Promise> { try { // False alarm // eslint-disable-next-line @typescript-eslint/no-unsafe-return return schema.parse(formDataToObject(await request.formData())); } catch (e) { if (e instanceof z.ZodError) { console.error(e); throw new Response(JSON.stringify(e), { status: 400 }); } throw e; } } export async function safeParseRequestFormData({ request, schema, }: { request: Request; schema: T; }): Promise< { success: true; data: z.infer } | { success: false; errors: string[] } > { const parsed = schema.safeParse(formDataToObject(await request.formData())); // this implementation is somewhat redundant but it's the only way I got types to work nice if (!parsed.success) { return { success: false, errors: parsed.error.errors.map((error) => error.message), }; } return { success: true, data: parsed.data, }; } function formDataToObject(formData: FormData) { const result: Record = {}; for (const [key, value] of formData.entries()) { const newValue = String(value); const existingValue = result[key]; if (Array.isArray(existingValue)) { existingValue.push(newValue); } else if (typeof existingValue === "string") { result[key] = [existingValue, newValue]; } else { result[key] = newValue; } } return result; } /** Asserts condition is truthy. Throws a new `Response` with given status code if falsy. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- same format as TS docs: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions export function validate( condition: any, message?: string, status = 400 ): asserts condition { if (condition) return; throw new Response( message ? JSON.stringify({ validationError: message }) : undefined, { status, } ); } export type Breadcrumb = | { imgPath: string; type: "IMAGE"; href: string } | { text: string; type: "TEXT"; href: string }; /** * Our custom type for route handles - the keys are defined by us or * libraries that parse them. * * Can be set per route using `export const handle: SendouRouteHandle = { };` * Can be accessed for all currently active routes via the `useMatches()` hook. */ export type SendouRouteHandle = { /** The i18n translation files used for this route, via remix-i18next */ i18n?: Namespace; /** * A function that returns the breadcrumb text that should be displayed in * the component */ breadcrumb?: (args: { match: RouteMatch; t: TFunction<"common", undefined>; }) => Breadcrumb | Array | undefined; /** The name of a navItem that is active on this route. See nav-items.json */ navItemName?: (typeof navItems)[number]["name"]; };