feat: delete implementation

This commit is contained in:
redglow
2026-06-21 14:48:33 +02:00
parent 3d91bf0a63
commit 961cc6e5ae
6 changed files with 74 additions and 19 deletions
+26
View File
@@ -0,0 +1,26 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { deleteAnnouncement, getAnnouncements } from "./services";
const announcementsQueryKey = ["announcements"];
export function useAnnouncementsQuery() {
const announcementsQuery = useQuery({
queryKey: announcementsQueryKey,
queryFn: getAnnouncements,
retry: false,
});
return announcementsQuery;
}
export function useDeleteAnnouncementMutation() {
const queryClient = useQueryClient();
return useMutation({
async mutationFn(announcementId: number) {
await deleteAnnouncement(announcementId);
},
async onSuccess() {
await queryClient.invalidateQueries({ queryKey: ["announcements"] });
},
});
}
+8 -2
View File
@@ -1,3 +1,9 @@
import { QueryClient } from "@tanstack/react-query"; import { QueryCache, QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient(); export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError(error) {
console.error(error);
},
}),
});
+1 -1
View File
@@ -32,7 +32,7 @@ function RouteComponent() {
<Box <Box
component="form" component="form"
enctype="multipart/form-data" enctype="multipart/form-data"
action="/api/announcements/add" action="/api/announcements"
method="POST" method="POST"
sx={{ sx={{
display: "flex", display: "flex",
+18 -15
View File
@@ -1,31 +1,21 @@
import { Alert, Box, CircularProgress, Typography } from "@mui/material"; import { Alert, Box, CircularProgress, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { AnnouncementTable } from "../components/AnnouncementTable"; import { AnnouncementTable } from "../components/AnnouncementTable";
import { DeleteAnnouncementDialog } from "../components/DeleteAnnouncementDialog"; import { DeleteAnnouncementDialog } from "../components/DeleteAnnouncementDialog";
import { RouterButton } from "../components/RouterButton"; import { RouterButton } from "../components/RouterButton";
import { useBoolean } from "../components/use-boolean"; import { useBoolean } from "../components/use-boolean";
import { type Announcement, getAnnouncements } from "../services"; import { useAnnouncementsQuery, useDeleteAnnouncementMutation } from "../hooks";
import type { Announcement } from "../services";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
// the query to retrieve the announcements const announcementsQuery = useAnnouncementsQuery();
const announcementsQuery = useQuery({
queryKey: ["announcements"],
queryFn: getAnnouncements,
retry: false,
});
// log error only if we get one const deleteAnnouncementMutation = useDeleteAnnouncementMutation();
useEffect(() => {
if (announcementsQuery.error) {
console.log(announcementsQuery.error);
}
}, [announcementsQuery.error]);
// delete confirmation dialog status // delete confirmation dialog status
const [ const [
@@ -40,6 +30,7 @@ function RouteComponent() {
announcementForLatestDeleteAttempt, announcementForLatestDeleteAttempt,
setAnnouncementForLatestDeleteAttempt, setAnnouncementForLatestDeleteAttempt,
] = useState<Announcement | undefined>(undefined); ] = useState<Announcement | undefined>(undefined);
const openDeleteConfirmation = useCallback( const openDeleteConfirmation = useCallback(
(a: Announcement) => { (a: Announcement) => {
openDeleteConfirmationDialog(); openDeleteConfirmationDialog();
@@ -48,6 +39,18 @@ function RouteComponent() {
[openDeleteConfirmationDialog], [openDeleteConfirmationDialog],
); );
const confirmDelete = useCallback(() => {
closeDeleteConfirmation();
if (announcementForLatestDeleteAttempt === undefined) {
throw new Error("Cannot delete announcement without an attempt set.");
}
deleteAnnouncementMutation.mutate(announcementForLatestDeleteAttempt?.id);
}, [
announcementForLatestDeleteAttempt,
closeDeleteConfirmation,
deleteAnnouncementMutation.mutate,
]);
// handle loading / error / show values // handle loading / error / show values
return ( return (
<Box sx={{ flexDirection: "column", display: "flex", gap: "3em" }}> <Box sx={{ flexDirection: "column", display: "flex", gap: "3em" }}>
@@ -72,7 +75,7 @@ function RouteComponent() {
{announcementForLatestDeleteAttempt && ( {announcementForLatestDeleteAttempt && (
<DeleteAnnouncementDialog <DeleteAnnouncementDialog
announcement={announcementForLatestDeleteAttempt} announcement={announcementForLatestDeleteAttempt}
confirmDelete={closeDeleteConfirmation} confirmDelete={confirmDelete}
denyDelete={closeDeleteConfirmation} denyDelete={closeDeleteConfirmation}
isDeleteConfirmationOpen={isDeleteConfirmationOpen} isDeleteConfirmationOpen={isDeleteConfirmationOpen}
/> />
+7
View File
@@ -18,3 +18,10 @@ export async function getAnnouncements() {
const announcements = Announcements.parse(data); const announcements = Announcements.parse(data);
return announcements; return announcements;
} }
export async function deleteAnnouncement(announcementId: number) {
const response = await fetch(`/api/announcements/${announcementId}`, {
method: "DELETE",
});
await response.text();
}
+14 -1
View File
@@ -21,11 +21,15 @@ const AddInput = z.object({
}), }),
}); });
const DeleteInput = z.object({
announcementId: z.coerce.number().positive(),
});
app.get("/hello", (_, res) => { app.get("/hello", (_, res) => {
res.send("Hello Vite + React + TypeScript!"); res.send("Hello Vite + React + TypeScript!");
}); });
app.post("/api/announcements/add", upload.single("image"), async (req, res) => { app.post("/api/announcements", upload.single("image"), async (req, res) => {
const addInput = AddInput.parse(req); const addInput = AddInput.parse(req);
await db await db
.insertInto("announcements") .insertInto("announcements")
@@ -49,6 +53,15 @@ app.get("/api/announcements", async (_, res) => {
res.json(announcements); res.json(announcements);
}); });
app.delete("/api/announcements/:announcementId", async (req, res) => {
const { announcementId } = DeleteInput.parse(req.params);
await db
.deleteFrom("announcements")
.where("id", "=", announcementId)
.execute();
res.status(204).send();
});
ViteExpress.listen(app, 3000, () => ViteExpress.listen(app, 3000, () =>
console.log("Server is listening on port 3000..."), console.log("Server is listening on port 3000..."),
); );