feat: edit
This commit is contained in:
@@ -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 (
|
||||
<Box
|
||||
component="form"
|
||||
encType="multipart/form-data"
|
||||
action="/api/announcements"
|
||||
method="POST"
|
||||
action={
|
||||
announcement
|
||||
? `/api/announcements/${announcement.id}`
|
||||
: "/api/announcements"
|
||||
}
|
||||
method={"POST"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { Box } from "@mui/system";
|
||||
import type { Announcement } from "../services";
|
||||
import { RouterIconButton } from "./RouterIconButton";
|
||||
import { useBoolean } from "./use-boolean";
|
||||
|
||||
export interface AnnouncementTableRowProps {
|
||||
@@ -22,6 +24,7 @@ export function AnnouncementTableRow({
|
||||
announcement,
|
||||
}: AnnouncementTableRowProps) {
|
||||
const [isOpen, { toggle }] = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
@@ -38,8 +41,14 @@ export function AnnouncementTableRow({
|
||||
</TableCell>
|
||||
<TableCell>{announcement.publication_date.toString()}</TableCell>
|
||||
<TableCell>
|
||||
<RouterIconButton
|
||||
to="/edit/$announcementId"
|
||||
params={{ announcementId: announcement.id.toString() }}
|
||||
>
|
||||
<EditIcon />
|
||||
</RouterIconButton>
|
||||
<IconButton onClick={() => openDeleteConfirmation(announcement)}>
|
||||
<DeleteIcon />
|
||||
<DeleteIcon color="error" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</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} />;
|
||||
}),
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
+17
-10
@@ -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<typeof Announcements>[0];
|
||||
const Announcements = z.array(Announcement);
|
||||
|
||||
export type Announcement = z.infer<typeof Announcement>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ const dialect = new SqliteDialect({
|
||||
|
||||
export const db = new Kysely<Database>({
|
||||
dialect,
|
||||
log: ["query", "error"],
|
||||
});
|
||||
|
||||
+26
-22
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user