From 0dd783f7511bebbe6a86bc6ec525f16881249005 Mon Sep 17 00:00:00 2001 From: redglow Date: Sun, 21 Jun 2026 16:17:22 +0200 Subject: [PATCH] feat: edit --- .../components/AddOrEditAnnouncement.tsx | 16 +++++-- .../components/AnnouncementTableRow.tsx | 11 ++++- src/client/components/RouterIconButton.tsx | 12 +++++ src/client/routeTree.gen.ts | 24 ++++++++-- src/client/routes/edit/$announcementId.tsx | 23 +++++++++ src/client/services.ts | 27 +++++++---- src/server/db/database.ts | 1 + src/server/main.ts | 48 ++++++++++--------- 8 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 src/client/components/RouterIconButton.tsx create mode 100644 src/client/routes/edit/$announcementId.tsx diff --git a/src/client/components/AddOrEditAnnouncement.tsx b/src/client/components/AddOrEditAnnouncement.tsx index cf78268..f9125e6 100644 --- a/src/client/components/AddOrEditAnnouncement.tsx +++ b/src/client/components/AddOrEditAnnouncement.tsx @@ -8,7 +8,7 @@ import { } from "@mui/material"; import { Box, styled } from "@mui/system"; 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"; const VisuallyHiddenInput = styled("input")({ @@ -44,12 +44,22 @@ export function AddOrEditAnnouncement({ undefined, ); + useEffect(() => { + if (announcement) { + setCurrentImageUrl(`/images/${announcement.image}`); + } + }, [announcement]); + return ( @@ -38,8 +41,14 @@ export function AnnouncementTableRow({ {announcement.publication_date.toString()} + + + openDeleteConfirmation(announcement)}> - + diff --git a/src/client/components/RouterIconButton.tsx b/src/client/components/RouterIconButton.tsx new file mode 100644 index 0000000..be9cf07 --- /dev/null +++ b/src/client/components/RouterIconButton.tsx @@ -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((props, ref) => { + return ; + }), +); diff --git a/src/client/routeTree.gen.ts b/src/client/routeTree.gen.ts index 2929de1..197a218 100644 --- a/src/client/routeTree.gen.ts +++ b/src/client/routeTree.gen.ts @@ -11,6 +11,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as IndexRouteImport } from './routes/index' import { Route as AddIndexRouteImport } from './routes/add/index' +import { Route as EditAnnouncementIdRouteImport } from './routes/edit/$announcementId' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -22,30 +23,39 @@ const AddIndexRoute = AddIndexRouteImport.update({ path: '/add/', getParentRoute: () => rootRouteImport, } as any) +const EditAnnouncementIdRoute = EditAnnouncementIdRouteImport.update({ + id: '/edit/$announcementId', + path: '/edit/$announcementId', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/edit/$announcementId': typeof EditAnnouncementIdRoute '/add/': typeof AddIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/edit/$announcementId': typeof EditAnnouncementIdRoute '/add': typeof AddIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/edit/$announcementId': typeof EditAnnouncementIdRoute '/add/': typeof AddIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/add/' + fullPaths: '/' | '/edit/$announcementId' | '/add/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/add' - id: '__root__' | '/' | '/add/' + to: '/' | '/edit/$announcementId' | '/add' + id: '__root__' | '/' | '/edit/$announcementId' | '/add/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + EditAnnouncementIdRoute: typeof EditAnnouncementIdRoute AddIndexRoute: typeof AddIndexRoute } @@ -65,11 +75,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AddIndexRouteImport parentRoute: typeof rootRouteImport } + '/edit/$announcementId': { + id: '/edit/$announcementId' + path: '/edit/$announcementId' + fullPath: '/edit/$announcementId' + preLoaderRoute: typeof EditAnnouncementIdRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + EditAnnouncementIdRoute: EditAnnouncementIdRoute, AddIndexRoute: AddIndexRoute, } export const routeTree = rootRouteImport diff --git a/src/client/routes/edit/$announcementId.tsx b/src/client/routes/edit/$announcementId.tsx new file mode 100644 index 0000000..b24ccb8 --- /dev/null +++ b/src/client/routes/edit/$announcementId.tsx @@ -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 ; +} diff --git a/src/client/services.ts b/src/client/services.ts index 00e731a..d12b7bc 100644 --- a/src/client/services.ts +++ b/src/client/services.ts @@ -1,16 +1,16 @@ import z from "zod"; -const Announcements = z.array( - z.object({ - id: z.number(), - title: z.string(), - text: z.string(), - publication_date: z.iso.datetime().transform((arg) => new Date(arg)), - image: z.string(), - }), -); +const Announcement = z.object({ + id: z.number(), + title: z.string(), + text: z.string(), + publication_date: z.iso.datetime().transform((arg) => new Date(arg)), + image: z.string(), +}); -export type Announcement = z.infer[0]; +const Announcements = z.array(Announcement); + +export type Announcement = z.infer; export async function getAnnouncements() { const response = await fetch("/api/announcements"); @@ -25,3 +25,10 @@ export async function deleteAnnouncement(announcementId: number) { }); 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; +} diff --git a/src/server/db/database.ts b/src/server/db/database.ts index 19caad5..ab63d36 100644 --- a/src/server/db/database.ts +++ b/src/server/db/database.ts @@ -8,4 +8,5 @@ const dialect = new SqliteDialect({ export const db = new Kysely({ dialect, + log: ["query", "error"], }); diff --git a/src/server/main.ts b/src/server/main.ts index a18d7a5..bcdab3f 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -41,25 +41,38 @@ const AnnouncementIdInput = z.object({ announcementId: z.coerce.number().positive(), }); -app.get("/hello", (_, res) => { - res.send("Hello Vite + React + TypeScript!"); +app.get("/api/announcements", async (_, res) => { + 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) => { const postInput = PostInput.parse(req); - await db - .insertInto("announcements") - .values({ - title: postInput.body.title, - text: postInput.body.text, - publication_date: postInput.body.publication_date, - image: postInput.file.filename, - }) - .execute(); + const announcement = { + title: postInput.body.title, + text: postInput.body.text, + publication_date: postInput.body.publication_date, + image: postInput.file.filename, + }; + await db.insertInto("announcements").values(announcement).execute(); res.redirect("/"); }); -app.patch( +app.post( "/api/announcements/:announcementId", upload.single("image"), async (req, res) => { @@ -75,20 +88,11 @@ app.patch( }) .where("id", "=", announcementId) .execute(); + 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) => { const { announcementId } = AnnouncementIdInput.parse(req.params);