Skip to content

Commit 3631551

Browse files
committed
feat: wiring regist event
1 parent 28bddf2 commit 3631551

File tree

18 files changed

+651
-69
lines changed

18 files changed

+651
-69
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@radix-ui/react-label": "^2.1.0",
2929
"@radix-ui/react-select": "^2.1.1",
3030
"@radix-ui/react-slot": "^1.1.0",
31+
"@radix-ui/react-toast": "^1.2.2",
3132
"@radix-ui/react-tooltip": "^1.1.3",
3233
"@vitest/coverage-v8": "^2.1.3",
3334
"axios": "^1.7.7",
12.5 KB
Binary file not shown.

src/app/[locale]/layout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "./globals.css";
66
import WrapperLayout from "@/components/layout/WrapperLayout";
77
import { locales } from "@/lib/locales";
88
import { notFound } from "next/navigation";
9+
import { Toaster } from "@/components/ui/Toaster";
910
const sora = Sora({ subsets: ["latin"] });
1011

1112
type Props = {
@@ -43,6 +44,7 @@ export default async function LocaleRootLayout({ children, params: { locale } }:
4344
<body className={`${sora.className} pt-8`}>
4445
<NextIntlClientProvider messages={messages}>
4546
<WrapperLayout>{children}</WrapperLayout>
47+
<Toaster />
4648
</NextIntlClientProvider>
4749
</body>
4850
</html>
+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
5+
import type { ToastActionElement, ToastProps } from "@/components/ui/Toast";
6+
7+
const TOAST_LIMIT = 1;
8+
const TOAST_REMOVE_DELAY = 1000000;
9+
10+
type ToasterToast = ToastProps & {
11+
id: string;
12+
title?: React.ReactNode;
13+
description?: React.ReactNode;
14+
action?: ToastActionElement;
15+
};
16+
17+
let count = 0;
18+
19+
function genId() {
20+
count = (count + 1) % Number.MAX_SAFE_INTEGER;
21+
return count.toString();
22+
}
23+
24+
type ActionType = {
25+
ADD_TOAST: "ADD_TOAST";
26+
UPDATE_TOAST: "UPDATE_TOAST";
27+
DISMISS_TOAST: "DISMISS_TOAST";
28+
REMOVE_TOAST: "REMOVE_TOAST";
29+
};
30+
31+
type Action =
32+
| {
33+
type: ActionType["ADD_TOAST"];
34+
toast: ToasterToast;
35+
}
36+
| {
37+
type: ActionType["UPDATE_TOAST"];
38+
toast: Partial<ToasterToast>;
39+
}
40+
| {
41+
type: ActionType["DISMISS_TOAST"];
42+
toastId?: ToasterToast["id"];
43+
}
44+
| {
45+
type: ActionType["REMOVE_TOAST"];
46+
toastId?: ToasterToast["id"];
47+
};
48+
49+
interface State {
50+
toasts: ToasterToast[];
51+
}
52+
53+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
54+
55+
const addToRemoveQueue = (toastId: string) => {
56+
if (toastTimeouts.has(toastId)) {
57+
return;
58+
}
59+
60+
const timeout = setTimeout(() => {
61+
toastTimeouts.delete(toastId);
62+
dispatch({
63+
type: "REMOVE_TOAST",
64+
toastId: toastId,
65+
});
66+
}, TOAST_REMOVE_DELAY);
67+
68+
toastTimeouts.set(toastId, timeout);
69+
};
70+
71+
export const reducer = (state: State, action: Action): State => {
72+
switch (action.type) {
73+
case "ADD_TOAST":
74+
return {
75+
...state,
76+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
77+
};
78+
79+
case "UPDATE_TOAST":
80+
return {
81+
...state,
82+
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
83+
};
84+
85+
case "DISMISS_TOAST": {
86+
const { toastId } = action;
87+
88+
// ! Side effects ! - This could be extracted into a dismissToast() action,
89+
// but I'll keep it here for simplicity
90+
if (toastId) {
91+
addToRemoveQueue(toastId);
92+
} else {
93+
state.toasts.forEach((toast) => {
94+
addToRemoveQueue(toast.id);
95+
});
96+
}
97+
98+
return {
99+
...state,
100+
toasts: state.toasts.map((t) =>
101+
t.id === toastId || toastId === undefined
102+
? {
103+
...t,
104+
open: false,
105+
}
106+
: t
107+
),
108+
};
109+
}
110+
case "REMOVE_TOAST":
111+
if (action.toastId === undefined) {
112+
return {
113+
...state,
114+
toasts: [],
115+
};
116+
}
117+
return {
118+
...state,
119+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
120+
};
121+
}
122+
};
123+
124+
const listeners: Array<(state: State) => void> = [];
125+
126+
let memoryState: State = { toasts: [] };
127+
128+
function dispatch(action: Action) {
129+
memoryState = reducer(memoryState, action);
130+
listeners.forEach((listener) => {
131+
listener(memoryState);
132+
});
133+
}
134+
135+
type Toast = Omit<ToasterToast, "id">;
136+
137+
function toast({ ...props }: Toast) {
138+
const id = genId();
139+
140+
const update = (props: ToasterToast) =>
141+
dispatch({
142+
type: "UPDATE_TOAST",
143+
toast: { ...props, id },
144+
});
145+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
146+
147+
dispatch({
148+
type: "ADD_TOAST",
149+
toast: {
150+
...props,
151+
id,
152+
open: true,
153+
onOpenChange: (open) => {
154+
if (!open) dismiss();
155+
},
156+
},
157+
});
158+
159+
return {
160+
id: id,
161+
dismiss,
162+
update,
163+
};
164+
}
165+
166+
function useToast() {
167+
const [state, setState] = React.useState<State>(memoryState);
168+
169+
React.useEffect(() => {
170+
listeners.push(setState);
171+
return () => {
172+
const index = listeners.indexOf(setState);
173+
if (index > -1) {
174+
listeners.splice(index, 1);
175+
}
176+
};
177+
}, [state]);
178+
179+
return {
180+
...state,
181+
toast,
182+
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
183+
};
184+
}
185+
186+
export { useToast, toast };
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as React from "react";
2+
import { Slot } from "@radix-ui/react-slot";
3+
import { cva, type VariantProps } from "class-variance-authority";
4+
import { cn } from "@/lib/utils";
5+
import { Loader2 } from "lucide-react";
6+
7+
const buttonVariants = cva(
8+
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-hmc-primary text-primary-foreground hover:opacity-90 active:bg-hmc-primary",
13+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16+
tertiary: "bg-tertiary text-tertiary-foreground hover:opacity-90 active:bg-tertiary",
17+
ghost: "hover:bg-accent hover:text-accent-foreground",
18+
link: "text-primary underline-offset-4 hover:underline",
19+
},
20+
size: {
21+
default: "lg:h-10 h-9 lg:px-4 px-4 lg:py-2",
22+
sm: "h-9 rounded-md px-3",
23+
lg: "lg:h-12 h-10 rounded-md lg:px-6 px-4",
24+
icon: "h-10 w-10",
25+
},
26+
},
27+
defaultVariants: {
28+
variant: "default",
29+
size: "default",
30+
},
31+
}
32+
);
33+
34+
export interface ButtonProps
35+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
36+
VariantProps<typeof buttonVariants> {
37+
asChild?: boolean;
38+
loading?: boolean;
39+
}
40+
41+
const SubmitButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
42+
({ className, variant, size, asChild = false, loading, children, ...props }, ref) => {
43+
if (asChild) {
44+
return (
45+
<Slot ref={ref} {...props}>
46+
<>
47+
{React.Children.map(children as React.ReactElement, (child: React.ReactElement) => {
48+
return React.cloneElement(child, {
49+
className: cn(buttonVariants({ variant, size }), className),
50+
children: (
51+
<>
52+
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
53+
{child.props.children}
54+
</>
55+
),
56+
});
57+
})}
58+
</>
59+
</Slot>
60+
);
61+
}
62+
63+
return (
64+
<button className={cn(buttonVariants({ variant, size, className }))} disabled={loading} ref={ref} {...props}>
65+
<>
66+
{loading && <Loader2 className={cn("h-4 w-4 animate-spin", children && "mr-2")} />}
67+
{children}
68+
</>
69+
</button>
70+
);
71+
}
72+
);
73+
SubmitButton.displayName = "SubmitButton";
74+
75+
export { SubmitButton, buttonVariants };

0 commit comments

Comments
 (0)