From a1ceff1fd2d57ea69f5818b3450241607a82bf42 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 23 Apr 2023 15:19:17 +0300 Subject: [PATCH] Filter builds on user builds page Closes #1243 --- app/db/seed/index.ts | 6 +- app/hooks/useSearchParamState.tsx | 1 + app/routes/u.$identifier/builds/index.tsx | 88 +++++++++++++++++++++-- app/styles/u.css | 5 ++ e2e/builds.spec.ts | 14 ++-- 5 files changed, 103 insertions(+), 11 deletions(-) diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 9bcd14f52..68dda1d30 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -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({ diff --git a/app/hooks/useSearchParamState.tsx b/app/hooks/useSearchParamState.tsx index a4f736266..0a0fdb4ea 100644 --- a/app/hooks/useSearchParamState.tsx +++ b/app/hooks/useSearchParamState.tsx @@ -9,6 +9,7 @@ export function useSearchParamState({ }: { 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(); diff --git a/app/routes/u.$identifier/builds/index.tsx b/app/routes/u.$identifier/builds/index.tsx index 7be2b6a4a..3dad98c5c 100644 --- a/app/routes/u.$identifier/builds/index.tsx +++ b/app/routes/u.$identifier/builds/index.tsx @@ -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); + } }; 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(); + 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 (
{isOwnPage && ( @@ -94,9 +128,13 @@ export default function UserBuildsPage() { )}
)} - {data.builds.length > 0 ? ( + + {builds.length > 0 ? (
- {data.builds.map((build) => ( + {builds.map((build) => ( ))}
@@ -108,3 +146,45 @@ export default function UserBuildsPage() { ); } + +function WeaponFilters({ + weaponFilter, + setWeaponFilter, +}: { + weaponFilter: "ALL" | MainWeaponId; + setWeaponFilter: (weaponFilter: "ALL" | MainWeaponId) => void; +}) { + const { t } = useTranslation(["weapons", "builds"]); + const data = useLoaderData(); + + return ( +
+ + {mainWeaponIds.map((weaponId) => { + const count = data.weaponCounts[weaponId]; + + if (!count) return null; + + return ( + + ); + })} +
+ ); +} diff --git a/app/styles/u.css b/app/styles/u.css index 0bd40e399..543b0e984 100644 --- a/app/styles/u.css +++ b/app/styles/u.css @@ -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; diff --git a/e2e/builds.spec.ts b/e2e/builds.spec.ts index 5daf74093..dcc918e3d 100644 --- a/e2e/builds.spec.ts +++ b/e2e/builds.spec.ts @@ -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 }) => {