feat: edit

This commit is contained in:
redglow
2026-06-21 16:17:22 +02:00
parent 8e19fcc899
commit 0dd783f751
8 changed files with 123 additions and 39 deletions
@@ -8,7 +8,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { Box, styled } from "@mui/system"; import { Box, styled } from "@mui/system";
import { DatePicker } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers";
import { type ChangeEvent, useState } from "react"; import { type ChangeEvent, useEffect, useState } from "react";
import type { Announcement } from "../services"; import type { Announcement } from "../services";
const VisuallyHiddenInput = styled("input")({ const VisuallyHiddenInput = styled("input")({
@@ -44,12 +44,22 @@ export function AddOrEditAnnouncement({
undefined, undefined,
); );
useEffect(() => {
if (announcement) {
setCurrentImageUrl(`/images/${announcement.image}`);
}
}, [announcement]);
return ( return (
<Box <Box
component="form" component="form"
encType="multipart/form-data" encType="multipart/form-data"
action="/api/announcements" action={
method="POST" announcement
? `/api/announcements/${announcement.id}`
: "/api/announcements"
}
method={"POST"}
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
+10 -1
View File
@@ -1,4 +1,5 @@
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { import {
@@ -10,6 +11,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { Box } from "@mui/system"; import { Box } from "@mui/system";
import type { Announcement } from "../services"; import type { Announcement } from "../services";
import { RouterIconButton } from "./RouterIconButton";
import { useBoolean } from "./use-boolean"; import { useBoolean } from "./use-boolean";
export interface AnnouncementTableRowProps { export interface AnnouncementTableRowProps {
@@ -22,6 +24,7 @@ export function AnnouncementTableRow({
announcement, announcement,
}: AnnouncementTableRowProps) { }: AnnouncementTableRowProps) {
const [isOpen, { toggle }] = useBoolean(); const [isOpen, { toggle }] = useBoolean();
return ( return (
<> <>
<TableRow> <TableRow>
@@ -38,8 +41,14 @@ export function AnnouncementTableRow({
</TableCell> </TableCell>
<TableCell>{announcement.publication_date.toString()}</TableCell> <TableCell>{announcement.publication_date.toString()}</TableCell>
<TableCell> <TableCell>
<RouterIconButton
to="/edit/$announcementId"
params={{ announcementId: announcement.id.toString() }}
>
<EditIcon />
</RouterIconButton>
<IconButton onClick={() => openDeleteConfirmation(announcement)}> <IconButton onClick={() => openDeleteConfirmation(announcement)}>
<DeleteIcon /> <DeleteIcon color="error" />
</IconButton> </IconButton>
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -0,0 +1,12 @@
// src/components/ui/mui-router-button.tsx
import { IconButton, type IconButtonProps } from "@mui/material";
import { createLink } from "@tanstack/react-router";
import { forwardRef } from "react";
// Create a router-compatible MUI IconButton
export const RouterIconButton = createLink(
forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
return <IconButton ref={ref} component="button" {...props} />;
}),
);
+21 -3
View File
@@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AddIndexRouteImport } from './routes/add/index' import { Route as AddIndexRouteImport } from './routes/add/index'
import { Route as EditAnnouncementIdRouteImport } from './routes/edit/$announcementId'
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
@@ -22,30 +23,39 @@ const AddIndexRoute = AddIndexRouteImport.update({
path: '/add/', path: '/add/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const EditAnnouncementIdRoute = EditAnnouncementIdRouteImport.update({
id: '/edit/$announcementId',
path: '/edit/$announcementId',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/edit/$announcementId': typeof EditAnnouncementIdRoute
'/add/': typeof AddIndexRoute '/add/': typeof AddIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/edit/$announcementId': typeof EditAnnouncementIdRoute
'/add': typeof AddIndexRoute '/add': typeof AddIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/edit/$announcementId': typeof EditAnnouncementIdRoute
'/add/': typeof AddIndexRoute '/add/': typeof AddIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/add/' fullPaths: '/' | '/edit/$announcementId' | '/add/'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/add' to: '/' | '/edit/$announcementId' | '/add'
id: '__root__' | '/' | '/add/' id: '__root__' | '/' | '/edit/$announcementId' | '/add/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
EditAnnouncementIdRoute: typeof EditAnnouncementIdRoute
AddIndexRoute: typeof AddIndexRoute AddIndexRoute: typeof AddIndexRoute
} }
@@ -65,11 +75,19 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AddIndexRouteImport preLoaderRoute: typeof AddIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/edit/$announcementId': {
id: '/edit/$announcementId'
path: '/edit/$announcementId'
fullPath: '/edit/$announcementId'
preLoaderRoute: typeof EditAnnouncementIdRouteImport
parentRoute: typeof rootRouteImport
}
} }
} }
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
EditAnnouncementIdRoute: EditAnnouncementIdRoute,
AddIndexRoute: AddIndexRoute, AddIndexRoute: AddIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
@@ -0,0 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import z from "zod";
import { AddOrEditAnnouncement } from "../../components/AddOrEditAnnouncement";
import { getAnnouncement } from "../../services";
const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(),
});
export const Route = createFileRoute("/edit/$announcementId")({
component: RouteComponent,
loader: async ({ params }) => {
const { announcementId } = AnnouncementIdInput.parse(params);
const announcement = await getAnnouncement(announcementId);
return announcement;
},
});
function RouteComponent() {
const announcement = Route.useLoaderData();
return <AddOrEditAnnouncement announcement={announcement} />;
}
+12 -5
View File
@@ -1,16 +1,16 @@
import z from "zod"; import z from "zod";
const Announcements = z.array( const Announcement = z.object({
z.object({
id: z.number(), id: z.number(),
title: z.string(), title: z.string(),
text: z.string(), text: z.string(),
publication_date: z.iso.datetime().transform((arg) => new Date(arg)), publication_date: z.iso.datetime().transform((arg) => new Date(arg)),
image: z.string(), image: z.string(),
}), });
);
export type Announcement = z.infer<typeof Announcements>[0]; const Announcements = z.array(Announcement);
export type Announcement = z.infer<typeof Announcement>;
export async function getAnnouncements() { export async function getAnnouncements() {
const response = await fetch("/api/announcements"); const response = await fetch("/api/announcements");
@@ -25,3 +25,10 @@ export async function deleteAnnouncement(announcementId: number) {
}); });
await response.text(); await response.text();
} }
export async function getAnnouncement(announcementId: number) {
const response = await fetch(`/api/announcements/${announcementId}`);
const data = await response.json();
const announcement = Announcement.parse(data);
return announcement;
}
+1
View File
@@ -8,4 +8,5 @@ const dialect = new SqliteDialect({
export const db = new Kysely<Database>({ export const db = new Kysely<Database>({
dialect, dialect,
log: ["query", "error"],
}); });
+22 -18
View File
@@ -41,25 +41,38 @@ const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(), announcementId: z.coerce.number().positive(),
}); });
app.get("/hello", (_, res) => { app.get("/api/announcements", async (_, res) => {
res.send("Hello Vite + React + TypeScript!"); const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
app.get("/api/announcements/:announcementId", async (req, res) => {
const { announcementId } = AnnouncementIdInput.parse(req.params);
const announcement = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
res.json(announcement);
}); });
app.post("/api/announcements", upload.single("image"), async (req, res) => { app.post("/api/announcements", upload.single("image"), async (req, res) => {
const postInput = PostInput.parse(req); const postInput = PostInput.parse(req);
await db const announcement = {
.insertInto("announcements")
.values({
title: postInput.body.title, title: postInput.body.title,
text: postInput.body.text, text: postInput.body.text,
publication_date: postInput.body.publication_date, publication_date: postInput.body.publication_date,
image: postInput.file.filename, image: postInput.file.filename,
}) };
.execute(); await db.insertInto("announcements").values(announcement).execute();
res.redirect("/"); res.redirect("/");
}); });
app.patch( app.post(
"/api/announcements/:announcementId", "/api/announcements/:announcementId",
upload.single("image"), upload.single("image"),
async (req, res) => { async (req, res) => {
@@ -75,20 +88,11 @@ app.patch(
}) })
.where("id", "=", announcementId) .where("id", "=", announcementId)
.execute(); .execute();
res.redirect("/"); res.redirect("/");
}, },
); );
app.get("/api/announcements", async (_, res) => {
// await new Promise((resolve) => setTimeout(resolve, 1000));
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
app.delete("/api/announcements/:announcementId", async (req, res) => { app.delete("/api/announcements/:announcementId", async (req, res) => {
const { announcementId } = AnnouncementIdInput.parse(req.params); const { announcementId } = AnnouncementIdInput.parse(req.params);