Skip to content

Commit fb62ddb

Browse files
committed
feat: slicing form registration
1 parent 65728a1 commit fb62ddb

File tree

12 files changed

+431
-25
lines changed

12 files changed

+431
-25
lines changed

next.config.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const nextConfig = {
1010
protocol: "https",
1111
hostname: "**",
1212
},
13+
{
14+
protocol: "https",
15+
hostname: "lms-be-development.hammercode.org",
16+
},
1317
],
1418
},
1519
trailingSlash: true,

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
}
2121
},
2222
"dependencies": {
23+
"@hookform/resolvers": "^3.9.0",
2324
"@radix-ui/react-alert-dialog": "^1.1.1",
2425
"@radix-ui/react-avatar": "^1.1.1",
2526
"@radix-ui/react-dialog": "^1.1.1",
2627
"@radix-ui/react-dropdown-menu": "^2.1.1",
28+
"@radix-ui/react-label": "^2.1.0",
2729
"@radix-ui/react-select": "^2.1.1",
2830
"@radix-ui/react-slot": "^1.1.0",
2931
"@radix-ui/react-tooltip": "^1.1.3",
@@ -39,8 +41,10 @@
3941
"next-themes": "^0.3.0",
4042
"react": "^18",
4143
"react-dom": "^18",
44+
"react-hook-form": "^7.53.1",
4245
"tailwind-merge": "^2.5.3",
43-
"tailwindcss-animate": "^1.0.7"
46+
"tailwindcss-animate": "^1.0.7",
47+
"zod": "^3.23.8"
4448
},
4549
"devDependencies": {
4650
"@testing-library/dom": "^10.4.0",

src/components/ui/Form/index.tsx

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as LabelPrimitive from "@radix-ui/react-label";
5+
import { Slot } from "@radix-ui/react-slot";
6+
import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form";
7+
8+
import { cn } from "@/lib/utils";
9+
import { Label } from "@/components/ui/Label";
10+
11+
const Form = FormProvider;
12+
13+
type FormFieldContextValue<
14+
TFieldValues extends FieldValues = FieldValues,
15+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
16+
> = {
17+
name: TName;
18+
};
19+
20+
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
21+
22+
const FormField = <
23+
TFieldValues extends FieldValues = FieldValues,
24+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
25+
>({
26+
...props
27+
}: ControllerProps<TFieldValues, TName>) => {
28+
return (
29+
<FormFieldContext.Provider value={{ name: props.name }}>
30+
<Controller {...props} />
31+
</FormFieldContext.Provider>
32+
);
33+
};
34+
35+
const useFormField = () => {
36+
const fieldContext = React.useContext(FormFieldContext);
37+
const itemContext = React.useContext(FormItemContext);
38+
const { getFieldState, formState } = useFormContext();
39+
40+
const fieldState = getFieldState(fieldContext.name, formState);
41+
42+
if (!fieldContext) {
43+
throw new Error("useFormField should be used within <FormField>");
44+
}
45+
46+
const { id } = itemContext;
47+
48+
return {
49+
id,
50+
name: fieldContext.name,
51+
formItemId: `${id}-form-item`,
52+
formDescriptionId: `${id}-form-item-description`,
53+
formMessageId: `${id}-form-item-message`,
54+
...fieldState,
55+
};
56+
};
57+
58+
type FormItemContextValue = {
59+
id: string;
60+
};
61+
62+
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
63+
64+
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
65+
({ className, ...props }, ref) => {
66+
const id = React.useId();
67+
68+
return (
69+
<FormItemContext.Provider value={{ id }}>
70+
<div ref={ref} className={cn("space-y-2", className)} {...props} />
71+
</FormItemContext.Provider>
72+
);
73+
}
74+
);
75+
FormItem.displayName = "FormItem";
76+
77+
const FormLabel = React.forwardRef<
78+
React.ElementRef<typeof LabelPrimitive.Root>,
79+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
80+
>(({ className, ...props }, ref) => {
81+
const { error, formItemId } = useFormField();
82+
83+
return <Label ref={ref} className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...props} />;
84+
});
85+
FormLabel.displayName = "FormLabel";
86+
87+
const FormControl = React.forwardRef<React.ElementRef<typeof Slot>, React.ComponentPropsWithoutRef<typeof Slot>>(
88+
({ ...props }, ref) => {
89+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
90+
91+
return (
92+
<Slot
93+
ref={ref}
94+
id={formItemId}
95+
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
96+
aria-invalid={!!error}
97+
{...props}
98+
/>
99+
);
100+
}
101+
);
102+
FormControl.displayName = "FormControl";
103+
104+
const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
105+
({ className, ...props }, ref) => {
106+
const { formDescriptionId } = useFormField();
107+
108+
return <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />;
109+
}
110+
);
111+
FormDescription.displayName = "FormDescription";
112+
113+
const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
114+
({ className, children, ...props }, ref) => {
115+
const { error, formMessageId } = useFormField();
116+
const body = error ? String(error?.message) : children;
117+
118+
if (!body) {
119+
return null;
120+
}
121+
122+
return (
123+
<p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
124+
{body}
125+
</p>
126+
);
127+
}
128+
);
129+
FormMessage.displayName = "FormMessage";
130+
131+
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };

src/components/ui/Input/index.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
6+
7+
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
8+
return (
9+
<input
10+
type={type}
11+
className={cn(
12+
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
13+
className
14+
)}
15+
ref={ref}
16+
{...props}
17+
/>
18+
);
19+
});
20+
Input.displayName = "Input";
21+
22+
export { Input };

src/components/ui/Label/index.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as LabelPrimitive from "@radix-ui/react-label";
5+
import { cva, type VariantProps } from "class-variance-authority";
6+
7+
import { cn } from "@/lib/utils";
8+
9+
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
10+
11+
const Label = React.forwardRef<
12+
React.ElementRef<typeof LabelPrimitive.Root>,
13+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
14+
>(({ className, ...props }, ref) => (
15+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
16+
));
17+
Label.displayName = LabelPrimitive.Root.displayName;
18+
19+
export { Label };

src/domains/Events.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from "zod";
2+
3+
export const EventSchema = z
4+
.object({
5+
id: z.number(),
6+
title: z.string().optional(),
7+
description: z.string().optional(),
8+
author: z.string().optional(),
9+
image_event: z.string().optional(),
10+
date_event: z.string().optional(),
11+
type: z.string().optional(),
12+
location: z.string().optional(),
13+
duration: z.string().optional(),
14+
capacity: z.number().optional(),
15+
status: z.string().optional(),
16+
Tags: z.null().optional(),
17+
Speakers: z.null().optional(),
18+
registration_link: z.string().optional(),
19+
price: z.number().optional(),
20+
created_by_user_id: z.number().optional(),
21+
updated_by_user_id: z.number().optional(),
22+
deleted_by_user_id: z.number().optional(),
23+
booking_start: z.string().optional(),
24+
booking_end: z.string().optional(),
25+
created_at: z.string().optional(),
26+
updated_at: z.string().optional(),
27+
deleted_at: z.null().optional(),
28+
})
29+
.optional();
30+
31+
export type EventType = z.infer<typeof EventSchema>;
32+
33+
export const registrationSchema = z.object({
34+
name: z.string().min(1, "Name is required"),
35+
email: z.string().email("Invalid email address"),
36+
phone_number: z.string().min(10, "Phone number is required"),
37+
image_proof_payment: z.string().min(1, "Proof of payment is required"),
38+
net_amount: z.number().min(1, "Net amount must be at least 1"),
39+
});
40+
41+
export type RegistrationForm = z.infer<typeof registrationSchema>;

src/features/events/EventDetailPage.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@
22
import { FC, useEffect, useState } from "react";
33
import Image from "next/image";
44
import { useTranslations } from "next-intl";
5-
import { TechEvent } from "@/features/events/types";
65
import { eventsService } from "@/services/events";
76
import { useFormatPrice } from "@/lib/utils";
87
import EventInfo from "./components/EventInfo";
9-
import { Button } from "@/components/ui/Button";
108
import Skeleton from "@/components/ui/Skeleton";
119
import TitleContainer from "@/components/ui/TitleContainer";
1210
import EventBreadcrumbs from "./components/EventBreadcrumb";
11+
import EventFormRegistration from "./components/EventFormRegistration";
12+
import { EventType } from "@/domains/Events";
1313

1414
interface EventDetailPageProp {
1515
eventId: string;
1616
}
1717

1818
const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
1919
const t = useTranslations("EventsPage");
20-
const [event, setEvent] = useState<TechEvent>();
20+
const [event, setEvent] = useState<EventType>();
2121

2222
useEffect(() => {
2323
const handleGetEvent = async () => {
@@ -63,17 +63,17 @@ const EventDetailPage: FC<EventDetailPageProp> = ({ eventId }) => {
6363
</div>
6464
</div>
6565
<div className="fixed bottom-0 left-0 right-0 flex items-center self-start justify-between w-full gap-4 rounded-lg bg-white dark:bg-slate-950 md:bg-transparent lg:flex-col lg:justify-start lg:sticky lg:top-24 lg:px-4">
66-
<div className="hidden w-full p-4 space-y-6 border rounded-lg lg:block border-slate-600 dark:border-slate-400">
66+
<div className="hidden w-full p-4 space-y-6 border rounded-lg lg:block">
6767
{event ? <EventInfo event={event} /> : <Skeleton className="w-full h-4 rounded-lg" />}
6868
</div>
69-
<div className="flex flex-col w-full gap-4 px-6 py-4 border-t rounded-lg sm:border border-slate-600 dark:border-slate-400">
69+
<div className="flex flex-col w-full gap-4 px-6 py-4 border-t rounded-lg sm:border">
7070
<div className="flex items-center justify-between w-full">
7171
<span className="font-semibold text-xs sm:text-sm dark:text-slate-200">
7272
{t("EventDetail.price-title")}
7373
</span>
74-
<p className="text-sm font-bold dark:text-slate-200">{useFormatPrice(1000)}</p>
74+
<p className="text-sm font-bold dark:text-slate-200">{useFormatPrice(event?.price)}</p>
7575
</div>
76-
<Button className="w-full font-bold dark:bg-slate-200">{t("EventDetail.register-button")}</Button>
76+
<EventFormRegistration data={event} />
7777
</div>
7878
</div>
7979
</div>

0 commit comments

Comments
 (0)