sendou.ink/app/utils/remix.ts
Kalle ef78d3a2c2
Tournament full (#1373)
* Got something going

* Style overwrites

* width != height

* More playing with lines

* Migrations

* Start bracket initial

* Unhardcode stage generation params

* Link to match page

* Matches page initial

* Support directly adding seed to map list generator

* Add docs

* Maps in matches page

* Add invariant about tie breaker map pool

* Fix PICNIC lacking tie breaker maps

* Only link in bracket when tournament has started

* Styled tournament roster inputs

* Prefer IGN in tournament match page

* ModeProgressIndicator

* Some conditional rendering

* Match action initial + better error display

* Persist bestOf in DB

* Resolve best of ahead of time

* Move brackets-manager to core

* Score reporting works

* Clear winner on score report

* ModeProgressIndicator: highlight winners

* Fix inconsistent input

* Better text when submitting match

* mapCountPlayedInSetWithCertainty that works

* UNDO_REPORT_SCORE implemented

* Permission check when starting tournament

* Remove IGN from upsert

* View match results page

* Source in DB

* Match page waiting for teams

* Move tournament bracket to feature folder

* REOPEN_MATCH initial

* Handle proper resetting of match

* Inline bracket-manager

* Syncify

* Transactions

* Handle match is locked gracefully

* Match page auto refresh

* Fix match refresh called "globally"

* Bracket autoupdate

* Move fillWithNullTillPowerOfTwo to utils with testing

* Fix map lists not visible after tournament started

* Optimize match events

* Show UI while in progress to members

* Fix start tournament alert not being responsive

* Teams can check in

* Fix map list 400

* xxx -> TODO

* Seeds page

* Remove map icons for team page

* Don't display link to seeds after tournament has started

* Admin actions initial

* Change captain admin action

* Make all hooks ts

* Admin actions functioning

* Fix validate error not displaying in CatchBoundary

* Adjust validate args order

* Remove admin loader

* Make delete team button menancing

* Only include checked in teams to bracket

* Optimize to.id route loads

* Working show map list generator toggle

* Update full tournaments flow

* Make full tournaments work with many start times

* Handle undefined in crud

* Dynamic stage banner

* Handle default strat if map list generation fails

* Fix crash on brackets if less than 2 teams

* Add commented out test for reference

* Add TODO

* Add players from team during register

* TrustRelationship

* Prefers not to host feature

* Last before merge

* Rename some vars

* More renames
2023-05-15 22:37:43 +03:00

135 lines
3.7 KiB
TypeScript

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<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 404 });
return value;
}
export function notFoundIfNullLike<T>(value: T | null | undefined): T {
if (value === null || value === undefined)
throw new Response(null, { status: 404 });
return value;
}
export function badRequestIfFalsy<T>(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<T extends z.ZodTypeAny>({
request,
schema,
}: {
request: Request;
schema: T;
}): Promise<z.infer<T>> {
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<T extends z.ZodTypeAny>({
request,
schema,
}: {
request: Request;
schema: T;
}): Promise<
{ success: true; data: z.infer<T> } | { 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<string, string | string[]> = {};
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 <Breadcrumb> component
*/
breadcrumb?: (args: {
match: RouteMatch;
t: TFunction<"common", undefined>;
}) => Breadcrumb | Array<Breadcrumb> | undefined;
/** The name of a navItem that is active on this route. See nav-items.json */
navItemName?: (typeof navItems)[number]["name"];
};