mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 15:08:44 -05:00
We do be merging
This commit is contained in:
parent
92047ab365
commit
535e4482e7
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user