Skip to content

Commit 455415d

Browse files
committed
feat: implement Present page with registration form, history, and notification components
1 parent e3d762a commit 455415d

10 files changed

Lines changed: 503 additions & 5 deletions

File tree

frontend/package-lock.json

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

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
},
1212
"dependencies": {
1313
"@radix-ui/react-alert-dialog": "^1.1.15",
14+
"@radix-ui/react-checkbox": "^1.3.3",
1415
"@radix-ui/react-dialog": "^1.1.15",
16+
"@radix-ui/react-label": "^2.1.8",
1517
"@radix-ui/react-radio-group": "^1.3.8",
1618
"@radix-ui/react-select": "^2.2.6",
1719
"@radix-ui/react-slot": "^1.2.4",

frontend/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import AdminPage from "./pages/Admin";
1616
import ReportsPage from "./pages/Admin/Reports";
1717
import CandidatePages from "./pages/Admin/Candidates";
1818
import TeamPage from "./pages/Teams";
19+
import PresentPage from "./pages/Present";
1920
const App = () => {
2021
return (
2122
<BrowserRouter>
@@ -26,6 +27,7 @@ const App = () => {
2627
<Route path="teams" element={<TeamPage />} />
2728
<Route path="active/token/:token" element={<ActivePage />} />
2829
<Route path="submissions" element={<SubmissionsPage />} />
30+
<Route path="presents" element={<PresentPage />} />
2931

3032
{/* Role Judge */}
3133
<Route path="judge" element={<ProtectedRoute roleAccess={[USER_ROLE.JUDGE]} />}>

frontend/src/components/Header/Candidate.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,22 @@ const CandidateHeader = () => {
77
const location = useLocation();
88
return (
99
<>
10-
<li id="submissions">
10+
<li id="presents">
1111
<NavLink
12-
url="/submissions"
12+
url="/presents"
1313
name="Đăng ký thuyết trình"
1414
Icon={Presentation}
15-
active={Helper.isActive(location.pathname, "/submissions")}
15+
active={Helper.isActive(location.pathname, "/presents")}
1616
/>
1717
</li>
18-
<li id="submissions">
18+
{/* <li id="submissions">
1919
<NavLink
2020
url="/submissions"
2121
name="Nộp sản phẩm"
2222
Icon={Send}
2323
active={Helper.isActive(location.pathname, "/submissions")}
2424
/>
25-
</li>
25+
</li> */}
2626
</>
2727
);
2828
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react"
2+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3+
import { CheckIcon } from "lucide-react"
4+
5+
import { cn } from "~/lib/utils"
6+
7+
function Checkbox({
8+
className,
9+
...props
10+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11+
return (
12+
<CheckboxPrimitive.Root
13+
data-slot="checkbox"
14+
className={cn(
15+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16+
className
17+
)}
18+
{...props}
19+
>
20+
<CheckboxPrimitive.Indicator
21+
data-slot="checkbox-indicator"
22+
className="grid place-content-center text-current transition-none"
23+
>
24+
<CheckIcon className="size-3.5" />
25+
</CheckboxPrimitive.Indicator>
26+
</CheckboxPrimitive.Root>
27+
)
28+
}
29+
30+
export { Checkbox }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as React from "react";
2+
import * as LabelPrimitive from "@radix-ui/react-label";
3+
import { cva, type VariantProps } from "class-variance-authority";
4+
5+
import { cn } from "~/lib/utils";
6+
7+
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70");
8+
9+
const Label = React.forwardRef<
10+
React.ElementRef<typeof LabelPrimitive.Root>,
11+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
12+
>(({ className, ...props }, ref) => (
13+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
14+
));
15+
Label.displayName = LabelPrimitive.Root.displayName;
16+
17+
export { Label };
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import React, { useState } from "react";
2+
import { Calendar, Clock, Send } from "lucide-react";
3+
import { Button } from "~/components/ui/button";
4+
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
5+
import { Checkbox } from "~/components/ui/checkbox";
6+
import { Label } from "~/components/ui/label";
7+
8+
interface TimeSlots {
9+
[date: string]: string[];
10+
}
11+
12+
// Thời gian thuyết trình thử - mỗi ngày 4 slot
13+
const trialTimeSlots: TimeSlots = {
14+
"17/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
15+
"18/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
16+
"19/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
17+
"20/01/2026": ["7:00 - 7:45", "8:00 - 8:45", "9:00 - 9:45", "10:00 - 10:45"],
18+
};
19+
20+
// Thời gian thuyết trình chính thức - mỗi ngày 2 slot
21+
const officialTimeSlots: TimeSlots = {
22+
"17/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
23+
"18/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
24+
"19/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
25+
"20/01/2026": ["13:00 - 13:45", "14:00 - 14:45"],
26+
};
27+
28+
const FormRegisterPresent = () => {
29+
const [trialSlot, setTrialSlot] = useState<string>("");
30+
const [officialSlots, setOfficialSlots] = useState<string[]>([]);
31+
32+
const handleOfficialSlotChange = (slot: string) => {
33+
setOfficialSlots((prev) => {
34+
if (prev.includes(slot)) {
35+
return prev.filter((s) => s !== slot);
36+
}
37+
return [...prev, slot];
38+
});
39+
};
40+
41+
const handleSubmit = async (e: React.FormEvent) => {
42+
e.preventDefault();
43+
console.log({
44+
trialSlot,
45+
officialSlots,
46+
});
47+
// TODO: Implement API call
48+
};
49+
50+
return (
51+
<section className="mb-6 overflow-hidden rounded-md border bg-white">
52+
<div className="border-b border-gray-200/70 bg-gradient-to-r from-gray-50/80 to-white px-5 py-4 sm:px-6">
53+
<h2 className="text-base font-semibold tracking-tight text-gray-900 sm:text-lg">
54+
Đăng ký thời gian thuyết trình
55+
</h2>
56+
<p className="mt-1.5 text-xs leading-relaxed text-gray-500 sm:text-sm">
57+
Vui lòng chọn thời gian phù hợp cho buổi thuyết trình thử và thuyết trình chính thức.
58+
</p>
59+
</div>
60+
61+
<form onSubmit={handleSubmit} className="space-y-6 p-5 sm:p-6">
62+
{/* Trial Presentation */}
63+
<div className="space-y-4">
64+
<div className="flex items-center gap-2">
65+
<Clock className="h-5 w-5 text-blue-500" />
66+
<h3 className="text-base font-semibold text-gray-900">
67+
Thời gian thuyết trình thử
68+
<span className="ml-2 text-xs font-normal text-gray-500">(Không bắt buộc)</span>
69+
</h3>
70+
</div>
71+
<p className="text-sm text-gray-600">
72+
Chọn <span className="font-semibold">MỘT</span> khung giờ để thuyết trình thử nghiệm và nhận
73+
phản hồi
74+
</p>
75+
76+
<RadioGroup value={trialSlot} onValueChange={setTrialSlot}>
77+
{Object.entries(trialTimeSlots).map(([date, slots]) => (
78+
<div key={`trial-${date}`} className="mb-5 space-y-3">
79+
<div className="flex items-center gap-2 border-b border-gray-200 pb-2">
80+
<Calendar className="h-4 w-4 text-gray-400" />
81+
<span className="font-medium text-gray-700">{date}</span>
82+
</div>
83+
<div className="grid gap-2 pl-6 sm:grid-cols-2 lg:grid-cols-4">
84+
{slots.map((slot) => (
85+
<div key={`trial-${date}-${slot}`} className="flex items-center space-x-2">
86+
<RadioGroupItem
87+
value={`${date}|${slot}`}
88+
id={`trial-${date}-${slot}`}
89+
className="text-blue-600"
90+
/>
91+
<Label
92+
htmlFor={`trial-${date}-${slot}`}
93+
className="cursor-pointer text-sm font-normal text-gray-700 hover:text-gray-900"
94+
>
95+
{slot}
96+
</Label>
97+
</div>
98+
))}
99+
</div>
100+
</div>
101+
))}
102+
</RadioGroup>
103+
</div>
104+
105+
<div className="border-t border-gray-200"></div>
106+
107+
{/* Official Presentation */}
108+
<div className="space-y-4">
109+
<div className="flex items-center gap-2">
110+
<Clock className="h-5 w-5 text-green-600" />
111+
<h3 className="text-base font-semibold text-green-700">
112+
Thời gian thuyết trình chính thức
113+
<span className="text-red-500">*</span>
114+
</h3>
115+
</div>
116+
<p className="text-sm text-gray-600">
117+
Chọn <span className="font-semibold text-green-700">NHIỀU</span> khung giờ bạn có thể tham gia
118+
thuyết trình chính thức
119+
</p>
120+
121+
<div className="space-y-4">
122+
{Object.entries(officialTimeSlots).map(([date, slots]) => (
123+
<div key={`official-${date}`} className="space-y-3">
124+
<div className="flex items-center gap-2 border-b border-gray-200 pb-2">
125+
<Calendar className="h-4 w-4 text-gray-400" />
126+
<span className="font-medium text-gray-700">{date}</span>
127+
</div>
128+
<div className="grid gap-3 pl-6 sm:grid-cols-2 lg:grid-cols-4">
129+
{slots.map((slot) => (
130+
<div key={`official-${date}-${slot}`} className="flex items-center space-x-2">
131+
<Checkbox
132+
id={`official-${date}-${slot}`}
133+
checked={officialSlots.includes(`${date}|${slot}`)}
134+
onCheckedChange={() => handleOfficialSlotChange(`${date}|${slot}`)}
135+
/>
136+
<Label
137+
htmlFor={`official-${date}-${slot}`}
138+
className="cursor-pointer text-sm font-normal text-gray-700 hover:text-gray-900"
139+
>
140+
{slot}
141+
</Label>
142+
</div>
143+
))}
144+
</div>
145+
</div>
146+
))}
147+
</div>
148+
</div>
149+
150+
{/* Submit Button */}
151+
<div className="flex items-center gap-3 border-t border-gray-200/70 pt-5">
152+
<Button
153+
type="submit"
154+
className="group flex items-center gap-2 transition-all hover:shadow-md"
155+
disabled={officialSlots.length === 0}
156+
>
157+
<Send className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
158+
Đăng ký
159+
</Button>
160+
<p className="text-xs text-gray-500">
161+
<span className="font-semibold text-red-600">Lưu ý:</span> Bạn phải chọn ít nhất 1 khung giờ cho
162+
thuyết trình chính thức
163+
</p>
164+
</div>
165+
</form>
166+
</section>
167+
);
168+
};
169+
170+
export default FormRegisterPresent;

0 commit comments

Comments
 (0)