diff --git a/app/components/BuildCard.tsx b/app/components/BuildCard.tsx index 7bd3c83ce..5c4a871a2 100644 --- a/app/components/BuildCard.tsx +++ b/app/components/BuildCard.tsx @@ -10,12 +10,14 @@ import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; import { databaseTimestampToDate } from "~/utils/dates"; import { gearImageUrl, modeImageUrl, weaponImageUrl } from "~/utils/urls"; import { Ability } from "./Ability"; -import { LinkButton } from "./Button"; +import { Button, LinkButton } from "./Button"; +import { FormWithConfirm } from "./FormWithConfirm"; import { Image } from "./Image"; import { Popover } from "./Popover"; type BuildProps = Pick< Build, + | "id" | "title" | "description" | "clothesGearSplId" @@ -31,6 +33,7 @@ type BuildProps = Pick< }; export function BuildCard({ + id, title, description, weapons, @@ -42,7 +45,7 @@ export function BuildCard({ modes, canEdit = false, }: BuildProps) { - const { t } = useTranslation(["weapons", "builds"]); + const { t } = useTranslation(["weapons", "builds", "common"]); const { i18n } = useTranslation(); const isMounted = useIsMounted(); @@ -67,6 +70,22 @@ export function BuildCard({ {t("builds:buildCard.edit")} ), + canEdit && ( + + + + ), ].filter(Boolean); return ( diff --git a/app/constants.ts b/app/constants.ts index fa0498aaf..a9ad97def 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -32,6 +32,7 @@ export const BUILD = { TITLE_MAX_LENGTH: 100, DESCRIPTION_MAX_LENGTH: TWEET_LENGTH_MAX_LENGTH, MAX_WEAPONS_COUNT: 5, + MAX_COUNT: 250, } as const; export const PLUS_TIERS = [1, 2, 3]; diff --git a/app/db/models/builds/deleteById.sql b/app/db/models/builds/deleteById.sql new file mode 100644 index 000000000..48ca9b425 --- /dev/null +++ b/app/db/models/builds/deleteById.sql @@ -0,0 +1,4 @@ +delete from + "Build" +where + "id" = @id; \ No newline at end of file diff --git a/app/db/models/builds/queries.server.ts b/app/db/models/builds/queries.server.ts index 3b02b8e98..590ab345e 100644 --- a/app/db/models/builds/queries.server.ts +++ b/app/db/models/builds/queries.server.ts @@ -2,20 +2,22 @@ import { sql } from "~/db/sql"; import type { Build, BuildAbility, BuildWeapon, GearType } from "~/db/types"; import type { ModeShort } from "~/modules/in-game-lists"; import { modesShort } from "~/modules/in-game-lists"; +import invariant from "tiny-invariant"; +import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; import createBuildSql from "./createBuild.sql"; import createBuildWeaponSql from "./createBuildWeapon.sql"; import createBuildAbilitySql from "./createBuildAbility.sql"; import countByUserIdSql from "./countByUserId.sql"; import buildsByUserIdSql from "./buildsByUserId.sql"; -import invariant from "tiny-invariant"; -import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types"; +import deleteByIdSql from "./deleteById.sql"; const createBuildStm = sql.prepare(createBuildSql); const createBuildWeaponStm = sql.prepare(createBuildWeaponSql); const createBuildAbilityStm = sql.prepare(createBuildAbilitySql); const countByUserIdStm = sql.prepare(countByUserIdSql); const buildsByUserIdStm = sql.prepare(buildsByUserIdSql); +const deleteByIdStm = sql.prepare(deleteByIdSql); interface CreateArgs { ownerId: Build["ownerId"]; @@ -129,3 +131,7 @@ function dbAbilitiesToArrayOfArrays( [sorted[8]!, sorted[9]!, sorted[10]!, sorted[11]!], ]; } + +export function deleteById(id: Build["id"]) { + deleteByIdStm.run({ id }); +} diff --git a/app/routes/u.$identifier/builds/index.tsx b/app/routes/u.$identifier/builds/index.tsx index 63bd47c4b..26cca271d 100644 --- a/app/routes/u.$identifier/builds/index.tsx +++ b/app/routes/u.$identifier/builds/index.tsx @@ -1,15 +1,42 @@ +import type { ActionFunction } from "@remix-run/node"; import { json, type LoaderArgs } from "@remix-run/node"; import { useLoaderData, useMatches } from "@remix-run/react"; import { useTranslation } from "react-i18next"; +import { z } from "zod"; import { BuildCard } from "~/components/BuildCard"; import { LinkButton } from "~/components/Button"; import { Main } from "~/components/Main"; import { db } from "~/db"; -import { getUser, useUser } from "~/modules/auth"; +import { getUser, requireUser, useUser } from "~/modules/auth"; import { atOrError } from "~/utils/arrays"; -import { notFoundIfFalsy } from "~/utils/remix"; +import { notFoundIfFalsy, parseRequestFormData } from "~/utils/remix"; +import { actualNumber, id } from "~/utils/zod"; import { type UserPageLoaderData, userParamsSchema } from "../../u.$identifier"; +const buildsActionSchema = z.object({ + buildToDeleteId: z.preprocess(actualNumber, id), +}); + +export const action: ActionFunction = async ({ request }) => { + const user = await requireUser(request); + const data = await parseRequestFormData({ + request, + schema: buildsActionSchema, + }); + + if ( + !db.builds + .buildsByUserId(user.id) + .some((build) => build.id === data.buildToDeleteId) + ) { + throw new Response(null, { status: 400 }); + } + + db.builds.deleteById(data.buildToDeleteId); + + return null; +}; + export const handle = { i18n: ["weapons", "builds"], }; @@ -34,6 +61,7 @@ export default function UserBuildsPage() { const parentPageData = atOrError(useMatches(), -2).data as UserPageLoaderData; const data = useLoaderData(); + // xxx: disable button if too many builds or something return (
@@ -46,6 +74,7 @@ export default function UserBuildsPage() { {data.builds.map((build) => ( { schema: newBuildActionSchema, }); + const usersBuilds = db.builds.buildsByUserId(user.id); + if (usersBuilds.length >= BUILD.MAX_COUNT) { + throw new Response(null, { status: 400 }); + } + db.builds.create({ title: data.title, description: data.description, diff --git a/public/locales/en/builds.json b/public/locales/en/builds.json index 2922142b1..a8d9a1b2d 100644 --- a/public/locales/en/builds.json +++ b/public/locales/en/builds.json @@ -9,5 +9,7 @@ "forms.weapons": "Weapons", "forms.gear.HEAD": "Gear (head)", "forms.gear.CLOTHES": "Gear (clothes)", - "forms.gear.SHOES": "Gear (shoes)" + "forms.gear.SHOES": "Gear (shoes)", + + "deleteConfirm": "Delete build '{{title}}'?" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index dd4fcd788..6db053eaa 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -23,6 +23,7 @@ "actions.edit": "Edit", "actions.add": "Add", "actions.remove": "Remove", + "actions.delete": "Delete", "results": "Results",