feat: edit
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user