mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-07 19:55:46 -05:00
Can delete build
This commit is contained in:
@@ -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")}
|
||||
</LinkButton>
|
||||
),
|
||||
canEdit && (
|
||||
<FormWithConfirm
|
||||
key="delete"
|
||||
dialogHeading={t("builds:deleteConfirm", { title })}
|
||||
fields={[["buildToDeleteId", id]]}
|
||||
>
|
||||
<Button
|
||||
className="build__small-text"
|
||||
variant="minimal-destructive"
|
||||
tiny
|
||||
type="submit"
|
||||
>
|
||||
{t("common:actions.delete")}
|
||||
</Button>
|
||||
</FormWithConfirm>
|
||||
),
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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];
|
||||
|
||||
4
app/db/models/builds/deleteById.sql
Normal file
4
app/db/models/builds/deleteById.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
delete from
|
||||
"Build"
|
||||
where
|
||||
"id" = @id;
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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<typeof loader>();
|
||||
|
||||
// xxx: disable button if too many builds or something
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div className="stack items-end">
|
||||
@@ -46,6 +74,7 @@ export default function UserBuildsPage() {
|
||||
{data.builds.map((build) => (
|
||||
<BuildCard
|
||||
key={build.id}
|
||||
id={build.id}
|
||||
title={build.title}
|
||||
description={build.description}
|
||||
headGearSplId={build.headGearSplId}
|
||||
|
||||
@@ -123,6 +123,11 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
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,
|
||||
|
||||
@@ -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}}'?"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"actions.edit": "Edit",
|
||||
"actions.add": "Add",
|
||||
"actions.remove": "Remove",
|
||||
"actions.delete": "Delete",
|
||||
|
||||
"results": "Results",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user