We do be merging

This commit is contained in:
Kalle 2026-01-18 21:52:52 +02:00
parent 92047ab365
commit 535e4482e7
26 changed files with 63 additions and 138 deletions

View File

@ -1,7 +1,6 @@
import { parseDate } from "@internationalized/date";
import { Check, Plus, Search, SquarePen, Trash } from "lucide-react";
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { Ability } from "~/components/Ability";
import { AddNewButton } from "~/components/AddNewButton";
import { Alert } from "~/components/Alert";
@ -26,8 +25,6 @@ import {
import { toastQueue } from "~/components/elements/Toast";
import { Flag } from "~/components/Flag";
import { FormMessage } from "~/components/FormMessage";
import { InputFormField } from "~/components/form/InputFormField";
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
import {
ModeImage,
SpecialWeaponImage,
@ -83,11 +80,6 @@ export const SECTIONS = [
},
{ title: "Sub Navigation", id: "sub-navigation", component: SubNavSection },
{ title: "Date Pickers", id: "date-pickers", component: DatePickerSection },
{
title: "Form Components",
id: "form-components",
component: FormComponentsSection,
},
{
title: "Splatoon Images",
id: "splatoon-images",
@ -1713,77 +1705,6 @@ function DatePickerSection({ id }: { id: string }) {
);
}
function FormComponentsSection({ id }: { id: string }) {
const methods = useForm();
return (
<Section>
<SectionTitle id={id}>Form Components</SectionTitle>
<div className="stack md">
<ComponentRow label="InputFormField">
<FormProvider {...methods}>
<form>
<InputFormField label="Username" name="username" required />
</form>
</FormProvider>
</ComponentRow>
<ComponentRow label="InputFormField with Placeholder">
<FormProvider {...methods}>
<form>
<InputFormField
label="Email"
name="email"
type="email"
placeholder="user@example.com"
/>
</form>
</FormProvider>
</ComponentRow>
<ComponentRow label="InputFormField with Bottom Text">
<FormProvider {...methods}>
<form>
<InputFormField
label="Website"
name="website"
type="url"
bottomText="Enter your personal or company website"
/>
</form>
</FormProvider>
</ComponentRow>
<ComponentRow label="TextAreaFormField">
<FormProvider {...methods}>
<form>
<TextAreaFormField
label="Description"
name="description"
maxLength={500}
/>
</form>
</FormProvider>
</ComponentRow>
<ComponentRow label="TextAreaFormField with Bottom Text">
<FormProvider {...methods}>
<form>
<TextAreaFormField
label="Bio"
name="bio"
maxLength={200}
bottomText="Tell us about yourself"
/>
</form>
</FormProvider>
</ComponentRow>
</div>
</Section>
);
}
function SplatoonImagesSection({ id }: { id: string }) {
return (
<Section>

View File

@ -2,11 +2,9 @@ import {
Map as MapIcon,
Mic,
Puzzle,
Star,
Trash,
Users,
Volume2,
X,
} from "lucide-react";
import * as React from "react";
import { useState } from "react";

View File

@ -4,10 +4,7 @@ import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepos
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { isSupporter } from "~/modules/permissions/utils";
import { clampThemeToGamut } from "~/utils/oklch-gamut";
import {
errorToast,
parseRequestPayload,
} from "~/utils/remix.server";
import { errorToast, parseRequestPayload } from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import { settingsEditSchema } from "../settings-schemas";

View File

@ -1,5 +1,11 @@
import { z } from "zod";
import { select, stringConstant, toggle } from "~/form/fields";
import { customField, select, stringConstant, toggle } from "~/form/fields";
import { themeInputSchema } from "~/utils/zod";
export const customThemeSchema = z.object({
_action: stringConstant("UPDATE_CUSTOM_THEME"),
newValue: customField({ initialValue: null }, themeInputSchema.nullable()),
});
export const clockFormatSchema = z.object({
_action: stringConstant("UPDATE_CLOCK_FORMAT"),
@ -38,6 +44,7 @@ export const updateNoScreenSchema = z.object({
});
export const settingsEditSchema = z.union([
customThemeSchema,
disableBuildAbilitySortingSchema,
disallowScrimPickupsFromUntrustedSchema,
updateNoScreenSchema,

View File

@ -306,10 +306,9 @@ export async function update({
bsky,
tag,
customTheme,
}: Pick<
Insertable<Tables["Team"]>,
"id" | "name" | "bio" | "bsky" | "tag"
> & { customTheme: CustomTheme | null }) {
}: Pick<Insertable<Tables["Team"]>, "id" | "name" | "bio" | "bsky" | "tag"> & {
customTheme: CustomTheme | null;
}) {
const customUrl = mySlugify(name);
const team = await db

View File

@ -37,10 +37,15 @@ describe("team page editing", () => {
dbReset();
});
it("adds valid custom css vars", async () => {
it("adds valid custom theme", async () => {
const response = await editTeamProfileAction(
{
css: JSON.stringify({ bg: "#fff" }),
customTheme: {
baseHue: 180,
baseChroma: 0.05,
accentHue: 200,
accentChroma: 0.1,
},
...DEFAULT_FIELDS,
},
{ user: "regular", params: { customUrl: "team-1" } },
@ -49,26 +54,27 @@ describe("team page editing", () => {
expect(response.status).toBe(302);
});
it("prevents adding custom css var of unknown property", async () => {
it("allows null custom theme", async () => {
const response = await editTeamProfileAction(
{
css: JSON.stringify({
"backdrop-filter": "#fff",
}),
customTheme: null,
...DEFAULT_FIELDS,
},
{ user: "regular", params: { customUrl: "team-1" } },
);
assertResponseErrored(response);
expect(response.status).toBe(302);
});
it("prevents adding custom css var of unknown value", async () => {
it("prevents adding custom theme with invalid values", async () => {
const response = await editTeamProfileAction(
{
css: JSON.stringify({
bg: "url(https://sendou.ink/u?q=1&_data=features%2Fuser-search%2Froutes%2Fu)",
}),
customTheme: {
baseHue: 500, // Invalid: max is 360
baseChroma: 0.05,
accentHue: 200,
accentChroma: 0.1,
},
...DEFAULT_FIELDS,
},
{ user: "regular", params: { customUrl: "team-1" } },

View File

@ -1,12 +1,6 @@
import { z } from "zod";
import { mySlugify } from "~/utils/urls";
import {
_action,
falsyToNull,
id,
safeStringSchema,
themeInputSchema,
} from "~/utils/zod";
import { _action, falsyToNull, id, themeInputSchema } from "~/utils/zod";
import * as TeamRepository from "./TeamRepository.server";
import { TEAM, TEAM_MEMBER_ROLES } from "./team-constants";
import { createTeamSchema } from "./team-schemas";

View File

@ -1,14 +1,13 @@
import { createMemoryRouter, RouterProvider } from "react-router";
import { describe, expect, test, vi } from "vitest";
import { render } from "vitest-browser-react";
import styles from "~/features/tournament-bracket/components/Bracket/bracket.module.css";
import type { TournamentManagerDataSet } from "~/modules/brackets-manager/types";
import type { Bracket as BracketType } from "../../core/Bracket";
import { EliminationBracketSide } from "./Elimination";
import { Bracket } from "./index";
import { RoundRobinBracket } from "./RoundRobin";
import { SwissBracket } from "./Swiss";
import "~/features/tournament-bracket/components/Bracket/bracket.css";
import "~/features/tournament-bracket/tournament-bracket.css";
const mockTournament = {
ctx: {
@ -770,7 +769,7 @@ describe("Single Elimination Bracket", () => {
<EliminationBracketSide bracket={bracket} type="single" isExpanded />,
);
const scores = screen.container.querySelectorAll(".bracket__match__score");
const scores = screen.container.querySelectorAll(`.${styles.matchScore}`);
expect(scores.length).toBeGreaterThan(0);
});
@ -827,7 +826,7 @@ describe("Single Elimination Bracket", () => {
// Should show exactly 2 round columns (Semifinals and Finals)
const roundColumns = screen.container.querySelectorAll(
".elim-bracket__round-column",
`.${styles.elimRoundColumn}`,
);
expect(roundColumns.length).toBe(2);
});
@ -911,7 +910,7 @@ describe("Double Elimination Bracket", () => {
// Small 4-team bracket only has Grand Finals (GF prefix), not regular WB rounds
const headerBox = screen.container.querySelector(
".bracket__match__header__box",
`.${styles.matchHeaderBox}`,
);
expect(headerBox?.textContent).toContain("GF");
expect(headerBox?.textContent).toContain("1.1");
@ -976,7 +975,9 @@ describe("Round Robin Bracket", () => {
<RoundRobinBracket bracket={bracket} />,
);
const tables = screen.container.querySelectorAll(".rr__placements-table");
const tables = screen.container.querySelectorAll(
`.${styles.rrPlacementsTable}`,
);
expect(tables.length).toBe(2);
});
});
@ -1014,7 +1015,7 @@ describe("Swiss Bracket", () => {
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
const scores = screen.container.querySelectorAll(".bracket__match__score");
const scores = screen.container.querySelectorAll(`.${styles.matchScore}`);
expect(scores.length).toBeGreaterThan(0);
});
@ -1026,7 +1027,9 @@ describe("Swiss Bracket", () => {
<SwissBracket bracket={bracket} bracketIdx={0} />,
);
const table = screen.container.querySelector(".rr__placements-table");
const table = screen.container.querySelector(
`.${styles.rrPlacementsTable}`,
);
expect(table).not.toBeNull();
});

View File

@ -10,9 +10,13 @@ const createMember = (userId: number) => ({
customUrl: null,
country: null,
twitch: null,
plusTier: null,
createdAt: 0,
isOwner: 0,
inGameName: null,
streamTwitch: null,
streamViewerCount: null,
streamThumbnailUrl: null,
});
const createTestTournament = (

View File

@ -35,7 +35,6 @@ import { navIconUrl, userResultsPage } from "~/utils/urls";
import { ordinalToRoundedSp } from "../../mmr/mmr-utils";
import { action } from "../actions/to.$id.seeds.server";
import { loader } from "../loaders/to.$id.seeds.server";
import styles from "../tournament.module.css";
import { useTournament } from "./to.$id";
import styles from "./to.$id.seeds.module.css";
export { loader, action };

View File

@ -15,11 +15,11 @@ import {
userPage,
} from "~/utils/urls";
import type { UserResultsLoaderData } from "../loaders/u.$identifier.results.server";
import styles from "../user-page.module.css";
import {
HIGHLIGHT_CHECKBOX_NAME,
HIGHLIGHT_TOURNAMENT_CHECKBOX_NAME,
} from "../user-page-constants";
import styles from "../user-page.module.css";
import { ParticipationPill } from "./ParticipationPill";
export type UserResultsTableProps = {

View File

@ -12,8 +12,6 @@ import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
export { action, loader };
import { mainStyles } from "~/components/Main";
export const handle: SendouRouteHandle = {
i18n: ["weapons", "builds", "gear"],
};

View File

@ -4,6 +4,7 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
import { userEvent } from "vitest/browser";
import { render } from "vitest-browser-react";
import { z } from "zod";
import labelStyles from "~/components/Label.module.css";
import { FormField } from "./FormField";
import {
array,
@ -277,8 +278,8 @@ describe("SendouForm", () => {
defaultValues: { bio: "123456789" },
});
const counter = screen.container.querySelector(".label__value");
expect(counter?.classList.contains("warning")).toBe(true);
const counter = screen.container.querySelector(`.${labelStyles.value}`);
expect(counter?.classList.contains(labelStyles.valueWarning)).toBe(true);
});
test("value counter shows error style when over max length", async () => {
@ -290,8 +291,8 @@ describe("SendouForm", () => {
defaultValues: { bio: "123456" },
});
const counter = screen.container.querySelector(".label__value");
expect(counter?.classList.contains("error")).toBe(true);
const counter = screen.container.querySelector(`.${labelStyles.value}`);
expect(counter?.classList.contains(labelStyles.valueError)).toBe(true);
});
test("typing updates value", async () => {

View File

@ -1,9 +1,8 @@
import { Plus, Trash } from "lucide-react";
import type * as React from "react";
import { useTranslation } from "react-i18next";
import { SendouButton } from "~/components/elements/Button";
import { FormMessage } from "~/components/FormMessage";
import { PlusIcon } from "~/components/icons/Plus";
import { TrashIcon } from "~/components/icons/Trash";
import type { FormFieldProps } from "../types";
import styles from "./ArrayFormField.module.css";
import { useTranslatedTexts } from "./FormFieldWrapper";
@ -75,7 +74,7 @@ export function ArrayFormField({
</div>
{count > min ? (
<SendouButton
icon={<TrashIcon />}
icon={<Trash />}
aria-label="Remove item"
size="small"
variant="minimal-destructive"
@ -93,7 +92,7 @@ export function ArrayFormField({
) : null}
<SendouButton
size="small"
icon={<PlusIcon />}
icon={<Plus />}
onPress={handleAdd}
isDisabled={count >= max}
className="m-0-auto"
@ -124,7 +123,7 @@ function ArrayItemFieldset({
<legend className={styles.headerLabel}>#{index + 1}</legend>
{canRemove ? (
<SendouButton
icon={<TrashIcon />}
icon={<Trash />}
aria-label="Remove item"
size="small"
variant="minimal-destructive"

View File

@ -15,13 +15,11 @@ import {
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import clsx from "clsx";
import { Star, Trash } from "lucide-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { SendouButton } from "~/components/elements/Button";
import { WeaponImage } from "~/components/Image";
import { StarIcon } from "~/components/icons/Star";
import { StarFilledIcon } from "~/components/icons/StarFilled";
import { TrashIcon } from "~/components/icons/Trash";
import { WeaponSelect } from "~/components/WeaponSelect";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import type { FormFieldProps } from "../types";
@ -213,9 +211,9 @@ function StaticWeaponItem({
size="small"
icon={
weapon.isFavorite ? (
<StarFilledIcon className={styles.starIconFilled} />
<Star className={styles.starIconFilled} fill="currentColor" />
) : (
<StarIcon className={styles.starIconOutlined} />
<Star className={styles.starIconOutlined} />
)
}
aria-label="Toggle favorite"
@ -225,7 +223,7 @@ function StaticWeaponItem({
<SendouButton
variant="minimal-destructive"
size="small"
icon={<TrashIcon />}
icon={<Trash />}
aria-label="Delete"
onPress={() => onRemove(weapon.id)}
/>
@ -293,9 +291,9 @@ function SortableWeaponItem({
size="small"
icon={
weapon.isFavorite ? (
<StarFilledIcon className={styles.starIconFilled} />
<Star className={styles.starIconFilled} fill="currentColor" />
) : (
<StarIcon className={styles.starIconOutlined} />
<Star className={styles.starIconOutlined} />
)
}
aria-label="Toggle favorite"
@ -305,7 +303,7 @@ function SortableWeaponItem({
<SendouButton
variant="minimal-destructive"
size="small"
icon={<TrashIcon />}
icon={<Trash />}
aria-label="Delete"
onPress={() => onRemove(weapon.id)}
/>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";