Filter builds on user builds page Closes #1243

This commit is contained in:
Kalle 2023-04-23 15:19:17 +03:00
parent d0e4945985
commit a1ceff1fd2
5 changed files with 103 additions and 11 deletions

View File

@ -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({

View File

@ -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();

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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 }) => {