Skip to content

Commit 4067812

Browse files
authored
Merge pull request #97 from hammercode-dev/feat/edit-events
[FEAT] - edit events features
2 parents 1044167 + 633a2b1 commit 4067812

File tree

13 files changed

+275
-41
lines changed

13 files changed

+275
-41
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { AdminEventUpdatePage } from "@/features/events/pages";
2+
3+
const EditEventPage = async (props: { params: Promise<{ eventId: string }> }) => {
4+
const params = await props.params;
5+
6+
return <AdminEventUpdatePage eventId={params.eventId} />;
7+
};
8+
export default EditEventPage;

src/app/[locale]/admin/events/[eventId]/page.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

src/domains/Events.ts

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,55 @@ export const userEventSchema = z.object({
8080
export type UserEventType = z.infer<typeof userEventSchema>;
8181

8282
export const createEventFormSchema = (t: (key: string) => string) =>
83-
z.object({
84-
title: z.string().min(1, t("validation.title-required")),
85-
description: z.string().min(1, t("validation.description-required")),
86-
// file_name: z.string().optional(),
87-
slug: z.string().min(1, t("validation.slug-required")),
88-
date: z.string().min(1, t("validation.date-required")),
89-
type: z.string().min(1, t("validation.type-required")),
90-
session_type: z.string().min(1, t("validation.session-type-required")),
91-
location: z.string().min(1, t("validation.location-required")),
92-
duration: z.string().min(1, t("validation.duration-required")),
93-
status: z.string().min(1, t("validation.status-required")),
94-
capacity: z.number().min(1, t("validation.capacity-min")),
95-
price: z.number().min(0, t("validation.price-min")),
96-
registration_link: z.string().url(t("validation.registration-url")),
97-
tags: z.array(z.string()),
98-
speakers: z.array(z.string()),
99-
reservation_start_date: z.string().min(1, t("validation.reservation-start-required")),
100-
reservation_end_date: z.string().min(1, t("validation.reservation-end-required")),
101-
image: z.union([
102-
z.instanceof(File).refine((file) => ["image/png", "image/jpeg", "image/jpg", "image/webp"].includes(file.type), {
103-
message: t("validation.image-type"),
104-
}),
105-
z.string().min(1, t("validation.image-required")),
106-
]),
107-
});
83+
z
84+
.object({
85+
title: z.string().min(1, t("validation.title-required")),
86+
description: z.string().min(1, t("validation.description-required")),
87+
file_name: z.string().optional(),
88+
slug: z.string().min(1, t("validation.slug-required")),
89+
date: z.string().min(1, t("validation.date-required")),
90+
type: z.string().min(1, t("validation.type-required")),
91+
session_type: z.string().min(1, t("validation.session-type-required")),
92+
location: z.string().min(1, t("validation.location-required")),
93+
duration: z.string().min(1, t("validation.duration-required")),
94+
status: z.string().min(1, t("validation.status-required")),
95+
capacity: z.number().min(1, t("validation.capacity-min")),
96+
price: z.number().min(0, t("validation.price-min")),
97+
registration_link: z.string().url(t("validation.registration-url")),
98+
tags: z.array(z.string()),
99+
speakers: z.array(z.string()),
100+
reservation_start_date: z.string().min(1, t("validation.reservation-start-required")),
101+
reservation_end_date: z.string().min(1, t("validation.reservation-end-required")),
102+
image: z
103+
.union([
104+
z
105+
.instanceof(File)
106+
.refine((file) => ["image/png", "image/jpeg", "image/jpg", "image/webp"].includes(file.type), {
107+
message: t("validation.image-type"),
108+
}),
109+
z.string().optional(),
110+
])
111+
.optional(),
112+
})
113+
.refine(
114+
(data) => {
115+
if (data.file_name) {
116+
return true;
117+
}
118+
return data.image instanceof File || (typeof data.image === "string" && data.image.length > 0);
119+
},
120+
{
121+
message: t("validation.image-required"),
122+
path: ["image"],
123+
}
124+
);
108125

109126
export type EventFormType = z.infer<ReturnType<typeof createEventFormSchema>>;
110127

111-
export type CreateEventPayload = Omit<EventFormType, "image"> & { file_name: string };
128+
export type CreateEventPayload = Omit<EventFormType, "image"> & { file_name?: string };
129+
130+
export type AdminEventResponseType = Omit<EventFormType, "image"> & {
131+
id: number;
132+
author: string;
133+
file_name: string;
134+
};

src/features/events/components/ColumnsEventListAdmin.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { formatDateEvent } from "@/lib/format";
66
import { ColumnDef } from "@tanstack/react-table";
77
import { Edit2, SquareChartGantt } from "lucide-react";
88

9+
import { Link } from "@/lib/navigation";
10+
911
export const columnsEventListAdmin: ColumnDef<EventType>[] = [
1012
{
1113
accessorKey: "title",
@@ -38,9 +40,11 @@ export const columnsEventListAdmin: ColumnDef<EventType>[] = [
3840
size="icon"
3941
variant="outline"
4042
className="cursor-poiner h-8 w-8 cursor-pointer border-blue-500 bg-blue-500 text-white hover:bg-blue-600 hover:text-white"
41-
onClick={() => console.log("Edit event:", row.original.id)}
43+
asChild
4244
>
43-
<Edit2 className="h-4 w-4" />
45+
<Link href={`/admin/events/${row.original.id}/edit`}>
46+
<Edit2 className="h-4 w-4" />
47+
</Link>
4448
</Button>
4549
<Button
4650
size="icon"

src/features/events/components/EventForm.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { eventTypes, eventStatuses, sessionTypes } from "../constants";
1919
import React, { useState, useEffect } from "react";
2020
import Badge from "@/components/ui/Badge";
2121
import { useRouter } from "@/lib/navigation";
22+
import Image from "next/image";
2223

2324
interface EventFormProps {
2425
onSubmit: (data: EventFormType) => void;
@@ -159,6 +160,25 @@ const EventForm = ({ onSubmit, isLoading = false, initialData, mode = "create" }
159160
)}
160161
/>
161162

163+
{initialData?.file_name && (
164+
<div className="relative h-82 w-full overflow-hidden rounded-md border">
165+
<Image
166+
src={initialData.file_name}
167+
alt="Background blur"
168+
width={400}
169+
height={160}
170+
className="absolute inset-0 h-82 w-full object-cover blur-sm"
171+
/>
172+
<Image
173+
src={initialData.file_name}
174+
alt="Current event image"
175+
width={200}
176+
height={200}
177+
className="relative z-10 mx-auto max-h-82 w-full object-contain"
178+
/>
179+
</div>
180+
)}
181+
162182
<FormField
163183
control={form.control}
164184
name="image"

src/features/events/hooks/useEvent.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"use client";
22

33
import React from "react";
4-
import { useQuery, useMutation } from "@tanstack/react-query";
4+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
55
import { eventsService } from "@/services/events";
66
import { uploadsService } from "@/services/uploads";
77
import { EventFormType, CreateEventPayload } from "@/domains/Events";
88
import DialogSuccess from "@/components/common/DialogGlobal/DialogSuccess";
99
import { useRouter } from "@/lib/navigation";
1010
import { useDialog } from "@/contexts";
1111
import DialogError from "@/components/common/DialogGlobal/DialogError";
12+
import { getFileNameFromUrl } from "@/lib/image";
1213

1314
export const useEventById = (eventId: string) => {
1415
return useQuery({
@@ -54,11 +55,14 @@ export const useEventsAdmin = (page: number, limit: number, search?: string) =>
5455
export const useCreateEvent = (t: (key: string) => string) => {
5556
const router = useRouter();
5657
const { openDialog, closeDialog } = useDialog();
58+
const queryClient = useQueryClient();
5759

5860
const submitMutation = useMutation({
5961
mutationKey: ["createEvent"],
6062
mutationFn: (payload: CreateEventPayload) => eventsService.createEventAdmin(payload),
6163
onSuccess: () => {
64+
queryClient.invalidateQueries({ queryKey: ["eventsAdmin"] });
65+
6266
openDialog({
6367
content: React.createElement(DialogSuccess, {
6468
title: t("EventForm.create-success-title"),
@@ -116,3 +120,97 @@ export const useCreateEvent = (t: (key: string) => string) => {
116120
isLoading,
117121
};
118122
};
123+
124+
export const useGetDetailEventAdmin = (id: string) => {
125+
return useQuery({
126+
queryKey: ["getDetailEventAdmin", id],
127+
queryFn: async () => eventsService.getDetailEventAdmin(id),
128+
});
129+
};
130+
131+
export const useUpdateEvent = (t: (key: string) => string, id: string) => {
132+
const router = useRouter();
133+
const { openDialog, closeDialog } = useDialog();
134+
const queryClient = useQueryClient();
135+
136+
const { mutate: mutateUpdateEvent, isPending: loadingCreateEvent } = useMutation({
137+
mutationKey: ["updateEvent"],
138+
mutationFn: (payload: CreateEventPayload) => eventsService.updateEventAdmin(id, payload),
139+
onSuccess: () => {
140+
queryClient.invalidateQueries({ queryKey: ["getDetailEventAdmin", id] });
141+
queryClient.invalidateQueries({ queryKey: ["eventsAdmin"] });
142+
143+
openDialog({
144+
content: React.createElement(DialogSuccess, {
145+
title: t("EventForm.update-success-title"),
146+
description: t("EventForm.update-success-description"),
147+
}),
148+
confirmText: t("EventForm.back-to-list"),
149+
onConfirm: () => {
150+
router.push("/admin/events");
151+
closeDialog();
152+
},
153+
classAction: "sm:justify-center",
154+
});
155+
},
156+
onError: () => {
157+
openDialog({
158+
content: React.createElement(DialogError, {
159+
title: t("EventForm.update-error-title"),
160+
description: t("EventForm.update-error-description"),
161+
}),
162+
cancelText: "OK",
163+
classAction: "sm:justify-center",
164+
});
165+
},
166+
});
167+
168+
const { mutate: mutateUpdateImage, isPending: loadingCreateImage } = useMutation({
169+
mutationFn: (payload: EventFormType) =>
170+
uploadsService.updateImageAdmin(payload.image, "events", getFileNameFromUrl(payload?.file_name as string)),
171+
onSuccess: (data, variables) => {
172+
const filename = data.data.file_name;
173+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
174+
const { image, ...rest } = variables;
175+
mutateUpdateEvent({
176+
...rest,
177+
file_name: filename,
178+
});
179+
},
180+
onError: () => {
181+
openDialog({
182+
content: React.createElement(DialogError, {
183+
title: t("EventForm.upload-error-title"),
184+
description: t("EventForm.upload-error-description"),
185+
}),
186+
onConfirm: () => {
187+
closeDialog();
188+
},
189+
confirmText: "OK",
190+
classAction: "sm:justify-center",
191+
});
192+
},
193+
});
194+
195+
const updateEvent = (payload: EventFormType) => {
196+
if (payload.image instanceof File) {
197+
return mutateUpdateImage(payload);
198+
} else if (payload.file_name) {
199+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
200+
const { image, ...rest } = payload;
201+
return mutateUpdateEvent({
202+
...rest,
203+
file_name: getFileNameFromUrl(payload.file_name),
204+
});
205+
} else {
206+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
207+
const { image, file_name, ...rest } = payload;
208+
return mutateUpdateEvent(rest);
209+
}
210+
};
211+
212+
return {
213+
updateEvent,
214+
isLoading: loadingCreateEvent || loadingCreateImage,
215+
};
216+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { EventFormType } from "@/domains/Events";
4+
import EventForm from "../components/EventForm";
5+
import { useGetDetailEventAdmin, useUpdateEvent } from "../hooks/useEvent";
6+
import Loader from "@/components/common/Loader";
7+
import { useTranslations } from "next-intl";
8+
9+
const AdminEventUpdatePage = ({ eventId }: { eventId: string }) => {
10+
console.log("eventsss id", eventId);
11+
const t = useTranslations();
12+
const { data, isLoading } = useGetDetailEventAdmin(eventId);
13+
const { updateEvent, isLoading: loadingUpdate } = useUpdateEvent(t, eventId);
14+
15+
const handleSubmit = (data: EventFormType) => {
16+
updateEvent(data);
17+
console.log(data);
18+
};
19+
20+
return (
21+
<section>
22+
<div className="mb-6">
23+
<h1 className="text-2xl font-bold">Edit Event</h1>
24+
<p className="text-muted-foreground">Edit event details quickly and easily.</p>
25+
</div>
26+
27+
{isLoading ? (
28+
<Loader />
29+
) : (
30+
<EventForm onSubmit={handleSubmit} mode="edit" isLoading={loadingUpdate} initialData={data?.data} />
31+
)}
32+
</section>
33+
);
34+
};
35+
export default AdminEventUpdatePage;

src/features/events/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { default as AdminEventsListPage } from "./AdminEventsListPage";
22
export { default as AdminEventsCreatePage } from "./AdminEventsCreatePage";
3+
export { default as AdminEventUpdatePage } from "./AdminEventUpdatePage";
34
export { default as PublicEventListPage } from "./PublicEventListPage";
45
export { default as PublicEventDetailPage } from "./PublicEventDetailPage";
56
export { default as UserEventPage } from "./UserEventPage";

src/lib/image.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const getFileNameFromUrl = (url: string): string => {
2+
return url.substring(url.lastIndexOf("/") + 1);
3+
};

src/locales/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,13 @@
309309
"create-success": "Event created successfully!",
310310
"create-success-title": "Success!",
311311
"create-success-description": "You can now view it in your event list or continue managing other events",
312+
"update-success": "Event updated successfully!",
313+
"update-success-title": "Success!",
314+
"update-success-description": "Your event has been updated successfully",
312315
"create-error-title": "Error",
313316
"create-error-description": "Failed to create event. Please try again.",
317+
"update-error-title": "Error",
318+
"update-error-description": "Failed to update event. Please try again.",
314319
"upload-error-title": "Upload Error",
315320
"upload-error-description": "Failed to upload image. Please try again.",
316321
"back-to-list": "Back to List",

0 commit comments

Comments
 (0)