mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Filter builds on user builds page Closes #1243
This commit is contained in:
parent
d0e4945985
commit
a1ceff1fd2
|
|
@ -418,7 +418,8 @@ function patrons() {
|
|||
const userIds = sql
|
||||
.prepare(`select "id" from "User" order by random() limit 50`)
|
||||
.all()
|
||||
.map((u) => u.id);
|
||||
.map((u) => u.id)
|
||||
.filter((id) => id !== NZAP_TEST_ID);
|
||||
|
||||
for (const id of userIds) {
|
||||
sql
|
||||
|
|
@ -818,6 +819,7 @@ const randomAbility = (legalTypes: AbilityType[]) => {
|
|||
return randomOrderAbilities.find((a) => legalTypes.includes(a.type))!.name;
|
||||
};
|
||||
|
||||
const adminWeaponPool = mainWeaponIds.filter(() => Math.random() > 0.8);
|
||||
function adminBuilds() {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const randomOrderHeadGear = shuffle(headGearIds.slice());
|
||||
|
|
@ -825,7 +827,7 @@ function adminBuilds() {
|
|||
const randomOrderShoesGear = shuffle(shoesGearIds.slice());
|
||||
// filter out sshot to prevent test flaking
|
||||
const randomOrderWeaponIds = shuffle(
|
||||
mainWeaponIds.filter((id) => id !== 40).slice()
|
||||
adminWeaponPool.filter((id) => id !== 40).slice()
|
||||
);
|
||||
|
||||
db.builds.create({
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export function useSearchParamState<T>({
|
|||
}: {
|
||||
defaultValue: T;
|
||||
name: string;
|
||||
/** Function to revive string from search params to value. If returns a null or undefined value then defaultValue gets used. */
|
||||
revive: (value: string) => T | null | undefined;
|
||||
}) {
|
||||
const [initialSearchParams] = useSearchParams();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { json, type LoaderArgs } from "@remix-run/node";
|
|||
import { useLoaderData, useMatches } from "@remix-run/react";
|
||||
import { z } from "zod";
|
||||
import { BuildCard } from "~/components/BuildCard";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { BUILD } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
|
@ -18,6 +18,10 @@ import {
|
|||
import { userNewBuildPage } from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { userParamsSchema, type UserPageLoaderData } from "../../u.$identifier";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { mainWeaponIds } from "~/modules/in-game-lists";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
|
||||
const buildsActionSchema = z.object({
|
||||
buildToDeleteId: z.preprocess(actualNumber, id),
|
||||
|
|
@ -61,7 +65,20 @@ export const loader = async ({ params, request }: LoaderArgs) => {
|
|||
throw new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ builds });
|
||||
return json({
|
||||
builds,
|
||||
weaponCounts: calculateWeaponCounts(),
|
||||
});
|
||||
|
||||
function calculateWeaponCounts() {
|
||||
return builds.reduce((acc, build) => {
|
||||
for (const weapon of build.weapons) {
|
||||
acc[weapon.weaponSplId] = (acc[weapon.weaponSplId] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<MainWeaponId, number>);
|
||||
}
|
||||
};
|
||||
|
||||
export default function UserBuildsPage() {
|
||||
|
|
@ -69,9 +86,26 @@ export default function UserBuildsPage() {
|
|||
const user = useUser();
|
||||
const parentPageData = atOrError(useMatches(), -2).data as UserPageLoaderData;
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [weaponFilter, setWeaponFilter] = useSearchParamState<
|
||||
"ALL" | MainWeaponId
|
||||
>({
|
||||
defaultValue: "ALL",
|
||||
name: "weapon",
|
||||
revive: (value) =>
|
||||
value === "ALL"
|
||||
? value
|
||||
: mainWeaponIds.find((id) => id === Number(value)),
|
||||
});
|
||||
|
||||
const isOwnPage = user?.id === parentPageData.id;
|
||||
|
||||
const builds =
|
||||
weaponFilter === "ALL"
|
||||
? data.builds
|
||||
: data.builds.filter((build) =>
|
||||
build.weapons.map((wpn) => wpn.weaponSplId).includes(weaponFilter)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="stack lg">
|
||||
{isOwnPage && (
|
||||
|
|
@ -94,9 +128,13 @@ export default function UserBuildsPage() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{data.builds.length > 0 ? (
|
||||
<WeaponFilters
|
||||
weaponFilter={weaponFilter}
|
||||
setWeaponFilter={setWeaponFilter}
|
||||
/>
|
||||
{builds.length > 0 ? (
|
||||
<div className="builds-container">
|
||||
{data.builds.map((build) => (
|
||||
{builds.map((build) => (
|
||||
<BuildCard key={build.id} build={build} canEdit={isOwnPage} />
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -108,3 +146,45 @@ export default function UserBuildsPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WeaponFilters({
|
||||
weaponFilter,
|
||||
setWeaponFilter,
|
||||
}: {
|
||||
weaponFilter: "ALL" | MainWeaponId;
|
||||
setWeaponFilter: (weaponFilter: "ALL" | MainWeaponId) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons", "builds"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="stack horizontal sm flex-wrap">
|
||||
<Button
|
||||
onClick={() => setWeaponFilter("ALL")}
|
||||
variant={weaponFilter === "ALL" ? undefined : "outlined"}
|
||||
size="tiny"
|
||||
className="u__build-filter-button"
|
||||
>
|
||||
{t("builds:stats.all")} ({data.builds.length})
|
||||
</Button>
|
||||
{mainWeaponIds.map((weaponId) => {
|
||||
const count = data.weaponCounts[weaponId];
|
||||
|
||||
if (!count) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={weaponId}
|
||||
onClick={() => setWeaponFilter(weaponId)}
|
||||
variant={weaponFilter === weaponId ? undefined : "outlined"}
|
||||
size="tiny"
|
||||
className="u__build-filter-button"
|
||||
>
|
||||
<WeaponImage weaponSplId={weaponId} variant="build" width={20} />
|
||||
{t(`weapons:MAIN_${weaponId}`)} ({count})
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,6 +237,11 @@
|
|||
--input-width: 16rem;
|
||||
}
|
||||
|
||||
.u__build-filter-button {
|
||||
padding: 0 var(--s-1-5) !important;
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.u__placements {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -58,13 +58,17 @@ test.describe("Builds", () => {
|
|||
|
||||
await expect(page.getByTestId("new-build-button")).toBeVisible();
|
||||
|
||||
await expect(page.getByAltText("Tenta Brella")).toBeVisible();
|
||||
await expect(page.getByAltText("Splat Brella")).toBeVisible();
|
||||
const firstBuildCard = page.getByTestId("build-card").first();
|
||||
|
||||
await expect(page.getByAltText("Tower Control")).toBeVisible();
|
||||
await expect(page.getByAltText("Splat Zones")).not.toBeVisible();
|
||||
await expect(firstBuildCard.getByAltText("Tenta Brella")).toBeVisible();
|
||||
await expect(firstBuildCard.getByAltText("Splat Brella")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("build-title")).toContainText("Test Build");
|
||||
await expect(firstBuildCard.getByAltText("Tower Control")).toBeVisible();
|
||||
await expect(firstBuildCard.getByAltText("Splat Zones")).not.toBeVisible();
|
||||
|
||||
await expect(firstBuildCard.getByTestId("build-title")).toContainText(
|
||||
"Test Build"
|
||||
);
|
||||
});
|
||||
|
||||
test("makes build private", async ({ page }) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user