Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/app/[locale]/admin/events/[eventId]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AdminEventUpdatePage } from "@/features/events/pages";

const EditEventPage = async (props: { params: Promise<{ eventId: string }> }) => {
const params = await props.params;

return <AdminEventUpdatePage eventId={params.eventId} />;
};
export default EditEventPage;
7 changes: 0 additions & 7 deletions src/app/[locale]/admin/events/[eventId]/page.tsx

This file was deleted.

75 changes: 49 additions & 26 deletions src/domains/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,32 +80,55 @@ export const userEventSchema = z.object({
export type UserEventType = z.infer<typeof userEventSchema>;

export const createEventFormSchema = (t: (key: string) => string) =>
z.object({
title: z.string().min(1, t("validation.title-required")),
description: z.string().min(1, t("validation.description-required")),
// file_name: z.string().optional(),
slug: z.string().min(1, t("validation.slug-required")),
date: z.string().min(1, t("validation.date-required")),
type: z.string().min(1, t("validation.type-required")),
session_type: z.string().min(1, t("validation.session-type-required")),
location: z.string().min(1, t("validation.location-required")),
duration: z.string().min(1, t("validation.duration-required")),
status: z.string().min(1, t("validation.status-required")),
capacity: z.number().min(1, t("validation.capacity-min")),
price: z.number().min(0, t("validation.price-min")),
registration_link: z.string().url(t("validation.registration-url")),
tags: z.array(z.string()),
speakers: z.array(z.string()),
reservation_start_date: z.string().min(1, t("validation.reservation-start-required")),
reservation_end_date: z.string().min(1, t("validation.reservation-end-required")),
image: z.union([
z.instanceof(File).refine((file) => ["image/png", "image/jpeg", "image/jpg", "image/webp"].includes(file.type), {
message: t("validation.image-type"),
}),
z.string().min(1, t("validation.image-required")),
]),
});
z
.object({
title: z.string().min(1, t("validation.title-required")),
description: z.string().min(1, t("validation.description-required")),
file_name: z.string().optional(),
slug: z.string().min(1, t("validation.slug-required")),
date: z.string().min(1, t("validation.date-required")),
type: z.string().min(1, t("validation.type-required")),
session_type: z.string().min(1, t("validation.session-type-required")),
location: z.string().min(1, t("validation.location-required")),
duration: z.string().min(1, t("validation.duration-required")),
status: z.string().min(1, t("validation.status-required")),
capacity: z.number().min(1, t("validation.capacity-min")),
price: z.number().min(0, t("validation.price-min")),
registration_link: z.string().url(t("validation.registration-url")),
tags: z.array(z.string()),
speakers: z.array(z.string()),
reservation_start_date: z.string().min(1, t("validation.reservation-start-required")),
reservation_end_date: z.string().min(1, t("validation.reservation-end-required")),
image: z
.union([
z
.instanceof(File)
.refine((file) => ["image/png", "image/jpeg", "image/jpg", "image/webp"].includes(file.type), {
message: t("validation.image-type"),
}),
z.string().optional(),
])
.optional(),
})
.refine(
(data) => {
if (data.file_name) {
return true;
}
return data.image instanceof File || (typeof data.image === "string" && data.image.length > 0);
},
{
message: t("validation.image-required"),
path: ["image"],
}
);

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

export type CreateEventPayload = Omit<EventFormType, "image"> & { file_name: string };
export type CreateEventPayload = Omit<EventFormType, "image"> & { file_name?: string };

export type AdminEventResponseType = Omit<EventFormType, "image"> & {
id: number;
author: string;
file_name: string;
};
8 changes: 6 additions & 2 deletions src/features/events/components/ColumnsEventListAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { formatDateEvent } from "@/lib/format";
import { ColumnDef } from "@tanstack/react-table";
import { Edit2, SquareChartGantt } from "lucide-react";

import { Link } from "@/lib/navigation";

export const columnsEventListAdmin: ColumnDef<EventType>[] = [
{
accessorKey: "title",
Expand Down Expand Up @@ -38,9 +40,11 @@ export const columnsEventListAdmin: ColumnDef<EventType>[] = [
size="icon"
variant="outline"
className="cursor-poiner h-8 w-8 cursor-pointer border-blue-500 bg-blue-500 text-white hover:bg-blue-600 hover:text-white"
onClick={() => console.log("Edit event:", row.original.id)}
asChild
>
<Edit2 className="h-4 w-4" />
<Link href={`/admin/events/${row.original.id}/edit`}>
<Edit2 className="h-4 w-4" />
</Link>
</Button>
<Button
size="icon"
Expand Down
20 changes: 20 additions & 0 deletions src/features/events/components/EventForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { eventTypes, eventStatuses, sessionTypes } from "../constants";
import React, { useState, useEffect } from "react";
import Badge from "@/components/ui/Badge";
import { useRouter } from "@/lib/navigation";
import Image from "next/image";

interface EventFormProps {
onSubmit: (data: EventFormType) => void;
Expand Down Expand Up @@ -159,6 +160,25 @@ const EventForm = ({ onSubmit, isLoading = false, initialData, mode = "create" }
)}
/>

{initialData?.file_name && (
<div className="relative h-82 w-full overflow-hidden rounded-md border">
<Image
src={initialData.file_name}
alt="Background blur"
width={400}
height={160}
className="absolute inset-0 h-82 w-full object-cover blur-sm"
/>
<Image
src={initialData.file_name}
alt="Current event image"
width={200}
height={200}
className="relative z-10 mx-auto max-h-82 w-full object-contain"
/>
</div>
)}

<FormField
control={form.control}
name="image"
Expand Down
100 changes: 99 additions & 1 deletion src/features/events/hooks/useEvent.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"use client";

import React from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { eventsService } from "@/services/events";
import { uploadsService } from "@/services/uploads";
import { EventFormType, CreateEventPayload } from "@/domains/Events";
import DialogSuccess from "@/components/common/DialogGlobal/DialogSuccess";
import { useRouter } from "@/lib/navigation";
import { useDialog } from "@/contexts";
import DialogError from "@/components/common/DialogGlobal/DialogError";
import { getFileNameFromUrl } from "@/lib/image";

export const useEventById = (eventId: string) => {
return useQuery({
Expand Down Expand Up @@ -54,11 +55,14 @@ export const useEventsAdmin = (page: number, limit: number, search?: string) =>
export const useCreateEvent = (t: (key: string) => string) => {
const router = useRouter();
const { openDialog, closeDialog } = useDialog();
const queryClient = useQueryClient();

const submitMutation = useMutation({
mutationKey: ["createEvent"],
mutationFn: (payload: CreateEventPayload) => eventsService.createEventAdmin(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["eventsAdmin"] });

openDialog({
content: React.createElement(DialogSuccess, {
title: t("EventForm.create-success-title"),
Expand Down Expand Up @@ -116,3 +120,97 @@ export const useCreateEvent = (t: (key: string) => string) => {
isLoading,
};
};

export const useGetDetailEventAdmin = (id: string) => {
return useQuery({
queryKey: ["getDetailEventAdmin", id],
queryFn: async () => eventsService.getDetailEventAdmin(id),
});
};

export const useUpdateEvent = (t: (key: string) => string, id: string) => {
const router = useRouter();
const { openDialog, closeDialog } = useDialog();
const queryClient = useQueryClient();

const { mutate: mutateUpdateEvent, isPending: loadingCreateEvent } = useMutation({
mutationKey: ["updateEvent"],
mutationFn: (payload: CreateEventPayload) => eventsService.updateEventAdmin(id, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["getDetailEventAdmin", id] });
queryClient.invalidateQueries({ queryKey: ["eventsAdmin"] });

openDialog({
content: React.createElement(DialogSuccess, {
title: t("EventForm.update-success-title"),
description: t("EventForm.update-success-description"),
}),
confirmText: t("EventForm.back-to-list"),
onConfirm: () => {
router.push("/admin/events");
closeDialog();
},
classAction: "sm:justify-center",
});
},
onError: () => {
openDialog({
content: React.createElement(DialogError, {
title: t("EventForm.update-error-title"),
description: t("EventForm.update-error-description"),
}),
cancelText: "OK",
classAction: "sm:justify-center",
});
},
});

const { mutate: mutateUpdateImage, isPending: loadingCreateImage } = useMutation({
mutationFn: (payload: EventFormType) =>
uploadsService.updateImageAdmin(payload.image, "events", getFileNameFromUrl(payload?.file_name as string)),
onSuccess: (data, variables) => {
const filename = data.data.file_name;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { image, ...rest } = variables;
mutateUpdateEvent({
...rest,
file_name: filename,
});
},
onError: () => {
openDialog({
content: React.createElement(DialogError, {
title: t("EventForm.upload-error-title"),
description: t("EventForm.upload-error-description"),
}),
onConfirm: () => {
closeDialog();
},
confirmText: "OK",
classAction: "sm:justify-center",
});
},
});

const updateEvent = (payload: EventFormType) => {
if (payload.image instanceof File) {
return mutateUpdateImage(payload);
} else if (payload.file_name) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { image, ...rest } = payload;
return mutateUpdateEvent({
...rest,
file_name: getFileNameFromUrl(payload.file_name),
});
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { image, file_name, ...rest } = payload;
return mutateUpdateEvent(rest);
}
};

return {
updateEvent,
isLoading: loadingCreateEvent || loadingCreateImage,
};
};
35 changes: 35 additions & 0 deletions src/features/events/pages/AdminEventUpdatePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import { EventFormType } from "@/domains/Events";
import EventForm from "../components/EventForm";
import { useGetDetailEventAdmin, useUpdateEvent } from "../hooks/useEvent";
import Loader from "@/components/common/Loader";
import { useTranslations } from "next-intl";

const AdminEventUpdatePage = ({ eventId }: { eventId: string }) => {
console.log("eventsss id", eventId);
const t = useTranslations();
const { data, isLoading } = useGetDetailEventAdmin(eventId);
const { updateEvent, isLoading: loadingUpdate } = useUpdateEvent(t, eventId);

const handleSubmit = (data: EventFormType) => {
updateEvent(data);
console.log(data);
};

return (
<section>
<div className="mb-6">
<h1 className="text-2xl font-bold">Edit Event</h1>
<p className="text-muted-foreground">Edit event details quickly and easily.</p>
</div>

{isLoading ? (
<Loader />
) : (
<EventForm onSubmit={handleSubmit} mode="edit" isLoading={loadingUpdate} initialData={data?.data} />
)}
</section>
);
};
export default AdminEventUpdatePage;
1 change: 1 addition & 0 deletions src/features/events/pages/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as AdminEventsListPage } from "./AdminEventsListPage";
export { default as AdminEventsCreatePage } from "./AdminEventsCreatePage";
export { default as AdminEventUpdatePage } from "./AdminEventUpdatePage";
export { default as PublicEventListPage } from "./PublicEventListPage";
export { default as PublicEventDetailPage } from "./PublicEventDetailPage";
export { default as UserEventPage } from "./UserEventPage";
3 changes: 3 additions & 0 deletions src/lib/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getFileNameFromUrl = (url: string): string => {
return url.substring(url.lastIndexOf("/") + 1);
};
5 changes: 5 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,13 @@
"create-success": "Event created successfully!",
"create-success-title": "Success!",
"create-success-description": "You can now view it in your event list or continue managing other events",
"update-success": "Event updated successfully!",
"update-success-title": "Success!",
"update-success-description": "Your event has been updated successfully",
"create-error-title": "Error",
"create-error-description": "Failed to create event. Please try again.",
"update-error-title": "Error",
"update-error-description": "Failed to update event. Please try again.",
"upload-error-title": "Upload Error",
"upload-error-description": "Failed to upload image. Please try again.",
"back-to-list": "Back to List",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,13 @@
"create-success": "Event berhasil dibuat!",
"create-success-title": "Berhasil!",
"create-success-description": "Anda sekarang dapat melihatnya di daftar event atau melanjutkan mengelola event lainnya",
"update-success": "Event berhasil diperbarui!",
"update-success-title": "Berhasil!",
"update-success-description": "Event Anda telah berhasil diperbarui",
"create-error-title": "Error",
"create-error-description": "Gagal membuat event. Silakan coba lagi.",
"update-error-title": "Error",
"update-error-description": "Gagal memperbarui event. Silakan coba lagi.",
"upload-error-title": "Error Upload",
"upload-error-description": "Gagal mengupload gambar. Silakan coba lagi.",
"back-to-list": "Kembali ke Daftar",
Expand Down
25 changes: 24 additions & 1 deletion src/services/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { HttpResponse } from "@/types/http";
import { fetcher } from "../instance";
import { CreateEventPayload, EventType, RegistrationForm, UserEventType } from "@/domains/Events";
import {
AdminEventResponseType,
CreateEventPayload,
EventType,
RegistrationForm,
UserEventType,
} from "@/domains/Events";

export const eventsService = {
/**
Expand Down Expand Up @@ -56,7 +62,24 @@ export const eventsService = {
});
},

/**
* API to create event for admin.
*/
async createEventAdmin(payload: CreateEventPayload): Promise<HttpResponse<null>> {
return fetcher.post(`/admin/events`, payload);
},

/**
* API to get detail event for admin.
*/
async getDetailEventAdmin(id: string): Promise<HttpResponse<AdminEventResponseType>> {
return fetcher.get(`/admin/events/${id}`);
},

/**
* API to update event for admin.
*/
async updateEventAdmin(id: string, payload: CreateEventPayload): Promise<HttpResponse<null>> {
return fetcher.put(`/admin/events/${id}`, payload);
},
};
Loading