Skip to content

Commit b2abeaa

Browse files
committed
feat(dialog): create custom dialog, and finishing create event
1 parent 7b850ef commit b2abeaa

File tree

12 files changed

+262
-13
lines changed

12 files changed

+262
-13
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@radix-ui/react-switch": "^1.2.6",
3434
"@radix-ui/react-tabs": "^1.1.13",
3535
"@radix-ui/react-tooltip": "^1.2.7",
36+
"@radix-ui/react-visually-hidden": "^1.2.3",
3637
"@tanstack/react-query": "^5.85.5",
3738
"@tanstack/react-table": "^8.21.3",
3839
"@tiptap/extension-code-block-lowlight": "^3.4.1",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/[locale]/layout.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { AuthJwtPayload } from "@/types";
1010
import { jwtDecode } from "jwt-decode";
1111
import { ThemeProvider } from "@/components/provider/ThemeProvider";
1212
import TanstackProvider from "@/components/provider/TanstackProvider";
13+
import { DialogProvider } from "@/contexts/dialogContext";
14+
import DialogGlobal from "@/components/common/DialogGlobal";
1315
const sora = Sora({ subsets: ["latin"] });
1416

1517
type Props = {
@@ -58,14 +60,17 @@ export default async function LocaleRootLayout(props: Readonly<Props>) {
5860
<html lang={locale} suppressHydrationWarning>
5961
<body className={`${sora.className}`}>
6062
<NextIntlClientProvider messages={messages}>
61-
<TanstackProvider>
62-
<AuthProvider payload={payload}>
63-
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
64-
{children}
65-
</ThemeProvider>
66-
</AuthProvider>
67-
</TanstackProvider>
68-
<Toaster />
63+
<DialogProvider>
64+
<TanstackProvider>
65+
<AuthProvider payload={payload}>
66+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
67+
{children}
68+
</ThemeProvider>
69+
</AuthProvider>
70+
</TanstackProvider>
71+
<Toaster />
72+
<DialogGlobal />
73+
</DialogProvider>
6974
</NextIntlClientProvider>
7075
</body>
7176
</html>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CircleX } from "lucide-react";
2+
3+
interface DialogErrorProps {
4+
title: string;
5+
description: string;
6+
}
7+
8+
const DialogError = ({ title, description }: DialogErrorProps) => {
9+
return (
10+
<section className="flex w-full flex-col items-center justify-center gap-2">
11+
<CircleX className="h-16 w-16 text-red-500" />
12+
<h1 className="text-xl font-semibold">{title}</h1>
13+
<p className="text-center text-gray-500">{description}</p>
14+
</section>
15+
);
16+
};
17+
export default DialogError;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CircleCheckBig } from "lucide-react";
2+
3+
interface DialogSuccessProps {
4+
title: string;
5+
description: string;
6+
}
7+
8+
const DialogSuccess = ({ title, description }: DialogSuccessProps) => {
9+
return (
10+
<section className="flex w-full flex-col items-center justify-center gap-2">
11+
<CircleCheckBig className="h-16 w-16 text-green-500" />
12+
<h1 className="text-xl font-semibold">{title}</h1>
13+
<p className="text-center text-gray-500">{description}</p>
14+
</section>
15+
);
16+
};
17+
export default DialogSuccess;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/Button";
4+
import {
5+
Dialog,
6+
DialogHeader,
7+
DialogContent,
8+
DialogTitle,
9+
DialogDescription,
10+
DialogFooter,
11+
} from "@/components/ui/Dialog";
12+
import { useDialog } from "@/contexts/dialogContext";
13+
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
14+
import { cn } from "@/lib/utils";
15+
16+
const sizeMap: Record<string, string> = {
17+
sm: "sm:max-w-[400px]",
18+
md: "sm:max-w-[500px]",
19+
lg: "sm:max-w-[700px]",
20+
xl: "sm:max-w-[900px]",
21+
"2xl": "sm:max-w-[1100px]",
22+
"80%": "sm:max-w-[80%]",
23+
"90%": "sm:max-w-[90%]",
24+
};
25+
26+
const DialogGlobal = () => {
27+
const { state, closeDialog } = useDialog();
28+
return (
29+
<Dialog open={state.isOpen} onOpenChange={closeDialog}>
30+
<DialogContent className={cn(sizeMap[state.size ?? "md"], state.className)}>
31+
<DialogHeader>
32+
{state.title ? (
33+
<DialogTitle>{state.title}</DialogTitle>
34+
) : (
35+
<VisuallyHidden.Root>
36+
<DialogTitle>Dialog</DialogTitle>
37+
</VisuallyHidden.Root>
38+
)}
39+
{state.description && <DialogDescription>{state.description}</DialogDescription>}
40+
</DialogHeader>
41+
42+
{state.content}
43+
44+
{(state.confirmText || state.cancelText) && (
45+
<DialogFooter className={state.classAction}>
46+
{state.cancelText && (
47+
<Button variant="outline" onClick={closeDialog}>
48+
{state.cancelText}
49+
</Button>
50+
)}
51+
{state.confirmText && (
52+
<Button
53+
variant="default"
54+
onClick={() => {
55+
state.onConfirm?.();
56+
}}
57+
className="bg-hmc-base-blue hover:bg-hmc-base-blue/90 text-white"
58+
>
59+
{state.confirmText}
60+
</Button>
61+
)}
62+
</DialogFooter>
63+
)}
64+
</DialogContent>
65+
</Dialog>
66+
);
67+
};
68+
export default DialogGlobal;

src/contexts/dialogContext.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"use client";
2+
3+
import { createContext, Dispatch, ReactNode, useContext, useReducer } from "react";
4+
5+
type DialogSize = "sm" | "md" | "lg" | "xl" | "2xl" | "80%" | "90%";
6+
7+
interface DialogState {
8+
isOpen: boolean;
9+
title?: string;
10+
description?: string;
11+
content?: ReactNode;
12+
size?: DialogSize;
13+
className?: string;
14+
classAction?: string;
15+
onClose?: () => void;
16+
onConfirm?: () => void;
17+
confirmText?: string;
18+
cancelText?: string;
19+
}
20+
21+
type DialogAction = { type: "OPEN"; payload: Omit<DialogState, "isOpen"> } | { type: "CLOSE" };
22+
23+
const initialState: DialogState = {
24+
isOpen: false,
25+
title: undefined,
26+
description: undefined,
27+
content: undefined,
28+
size: "md",
29+
className: "",
30+
classAction: "",
31+
onClose: undefined,
32+
onConfirm: undefined,
33+
confirmText: undefined,
34+
cancelText: undefined,
35+
};
36+
37+
const dialogReducer = (state: DialogState, action: DialogAction): DialogState => {
38+
switch (action.type) {
39+
case "OPEN":
40+
return { isOpen: true, ...action.payload };
41+
case "CLOSE":
42+
return { isOpen: false };
43+
default:
44+
return state;
45+
}
46+
};
47+
48+
const DialogContext = createContext<{
49+
state: DialogState;
50+
dispatch: Dispatch<DialogAction>;
51+
openDialog: (config: Omit<DialogState, "isOpen">) => void;
52+
closeDialog: () => void;
53+
}>({
54+
state: initialState,
55+
dispatch: () => {},
56+
openDialog: () => {},
57+
closeDialog: () => {},
58+
});
59+
60+
export const DialogProvider = ({ children }: { children: ReactNode }) => {
61+
const [state, dispatch] = useReducer(dialogReducer, initialState);
62+
63+
const openDialog = (config: Omit<DialogState, "isOpen">) => {
64+
dispatch({ type: "OPEN", payload: config });
65+
};
66+
67+
const closeDialog = () => {
68+
dispatch({ type: "CLOSE" });
69+
};
70+
71+
return (
72+
<DialogContext.Provider value={{ state, dispatch, openDialog, closeDialog }}>{children}</DialogContext.Provider>
73+
);
74+
};
75+
76+
export const useDialog = () => {
77+
return useContext(DialogContext);
78+
};

src/contexts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useDialog, DialogProvider } from "./dialogContext";

src/features/events/components/EventForm.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import TextEditor from "@/components/common/TextEditor/TextEditor";
1616
import { cn, generateSlug } from "@/lib/utils";
1717
import { createEventFormSchema, EventFormType } from "@/domains/Events";
1818
import { eventTypes, eventStatuses, sessionTypes } from "../constants";
19-
import { useState, useEffect } from "react";
19+
import React, { useState, useEffect } from "react";
2020
import Badge from "@/components/ui/Badge";
21+
import { useRouter } from "@/lib/navigation";
2122

2223
interface EventFormProps {
2324
onSubmit: (data: EventFormType) => void;
@@ -28,6 +29,7 @@ interface EventFormProps {
2829

2930
const EventForm = ({ onSubmit, isLoading = false, initialData, mode = "create" }: EventFormProps) => {
3031
const t = useTranslations("EventForm");
32+
const router = useRouter();
3133
const eventFormSchema = createEventFormSchema(t);
3234
const [tagInput, setTagInput] = useState("");
3335
const [speakerInput, setSpeakerInput] = useState("");
@@ -525,7 +527,13 @@ const EventForm = ({ onSubmit, isLoading = false, initialData, mode = "create" }
525527
: t("buttons.update")}
526528
</Button>
527529

528-
<Button type="button" variant="outline">
530+
<Button
531+
type="button"
532+
variant="outline"
533+
onClick={() => {
534+
router.back();
535+
}}
536+
>
529537
{t("buttons.cancel")}
530538
</Button>
531539
</div>

src/features/events/hooks/useEvent.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
"use client";
2+
3+
import React from "react";
14
import { useQuery, useMutation } from "@tanstack/react-query";
25
import { eventsService } from "@/services/events";
36
import { uploadsService } from "@/services/uploads";
47
import { EventFormType, CreateEventPayload } from "@/domains/Events";
5-
import { toast } from "sonner";
8+
import DialogSuccess from "@/components/common/DialogGlobal/DialogSuccess";
69
import { useRouter } from "@/lib/navigation";
10+
import { useDialog } from "@/contexts";
11+
import DialogError from "@/components/common/DialogGlobal/DialogError";
712

813
export const useEventById = (eventId: string) => {
914
return useQuery({
@@ -48,13 +53,32 @@ export const useEventsAdmin = (page: number, limit: number, search?: string) =>
4853

4954
export const useCreateEvent = (t: (key: string) => string) => {
5055
const router = useRouter();
56+
const { openDialog, closeDialog } = useDialog();
5157

5258
const submitMutation = useMutation({
5359
mutationKey: ["createEvent"],
5460
mutationFn: (payload: CreateEventPayload) => eventsService.createEventAdmin(payload),
5561
onSuccess: () => {
56-
toast.success(t("EventForm.create-success"));
57-
router.push("/admin/events");
62+
openDialog({
63+
content: React.createElement(DialogSuccess, {
64+
title: t("EventForm.create-success-title"),
65+
description: t("EventForm.create-success-description"),
66+
}),
67+
confirmText: t("EventForm.back-to-list"),
68+
onConfirm: () => {
69+
router.push("/admin/events");
70+
closeDialog();
71+
},
72+
classAction: "sm:justify-center",
73+
});
74+
},
75+
onError: () => {
76+
openDialog({
77+
title: t("EventForm.create-error-title"),
78+
description: t("EventForm.create-error-description"),
79+
confirmText: "OK",
80+
classAction: "sm:justify-center",
81+
});
5882
},
5983
});
6084

@@ -69,6 +93,19 @@ export const useCreateEvent = (t: (key: string) => string) => {
6993
file_name: filename,
7094
});
7195
},
96+
onError: () => {
97+
openDialog({
98+
content: React.createElement(DialogError, {
99+
title: t("EventForm.upload-error-title"),
100+
description: t("EventForm.upload-error-description"),
101+
}),
102+
onConfirm: () => {
103+
closeDialog();
104+
},
105+
confirmText: "OK",
106+
classAction: "sm:justify-center",
107+
});
108+
},
72109
});
73110

74111
const isLoading = createMutation.isPending || submitMutation.isPending;

0 commit comments

Comments
 (0)