Skip to content

Commit e3d762a

Browse files
committed
feat: enhance submissions page with form, history, and notification components
1 parent 743fa38 commit e3d762a

9 files changed

Lines changed: 415 additions & 26 deletions

File tree

frontend/src/components/Header/Candidate.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1+
import Helper from "~/utils/helper";
12
import { NavLink } from "./NavLink";
2-
import { ServerCrash } from "lucide-react";
3+
import { Presentation, Send } from "lucide-react";
4+
import { useLocation } from "react-router";
35

46
const CandidateHeader = () => {
5-
// const location = useLocation();
7+
const location = useLocation();
68
return (
79
<>
8-
<li>
9-
<NavLink url="https://discord.gg/WvudrJaYD" name="Hỗ trợ" Icon={ServerCrash} target="_blank" />
10+
<li id="submissions">
11+
<NavLink
12+
url="/submissions"
13+
name="Đăng ký thuyết trình"
14+
Icon={Presentation}
15+
active={Helper.isActive(location.pathname, "/submissions")}
16+
/>
17+
</li>
18+
<li id="submissions">
19+
<NavLink
20+
url="/submissions"
21+
name="Nộp sản phẩm"
22+
Icon={Send}
23+
active={Helper.isActive(location.pathname, "/submissions")}
24+
/>
1025
</li>
1126
</>
1227
);

frontend/src/components/Header/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BadgeQuestionMark, ChevronDown, House, Menu, Users, X } from "lucide-react";
1+
import { BadgeQuestionMark, ChevronDown, House, Menu, ServerCrash, Users, X } from "lucide-react";
22
import { Link, useLocation } from "react-router";
33
import useAuth from "~/hooks/useAuth";
44
import { useState, useRef, useEffect } from "react";
@@ -61,15 +61,24 @@ const Header = () => {
6161
active={Helper.isActive(location.pathname, "/")}
6262
/>
6363
</li>
64-
<li id="submissions">
64+
{isLogin && user.role === USER_ROLE.CANDIDATE && <CandidateHeader />}
65+
<li id="teams">
6566
<NavLink
6667
url="/teams"
6768
name="Danh sách nhóm"
6869
Icon={Users}
6970
active={Helper.isActive(location.pathname, "/teams")}
7071
/>
7172
</li>
72-
{isLogin && user.role === USER_ROLE.CANDIDATE && <CandidateHeader />}
73+
{/* <li>
74+
<NavLink
75+
url="https://discord.gg/WvudrJaYD"
76+
name="Hỗ trợ"
77+
Icon={ServerCrash}
78+
target="_blank"
79+
/>
80+
</li> */}
81+
7382
{isLogin && user.role === USER_ROLE.ADMIN && <AdminHeader />}
7483
</ul>
7584
</nav>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from "react";
2+
import { FileText, Github, Link2, Send } from "lucide-react";
3+
import { Button } from "~/components/ui/button";
4+
import { Input } from "~/components/ui/input";
5+
import { Textarea } from "~/components/ui/textarea";
6+
7+
const FormSubmit = () => {
8+
const handleSubmit = async (e: React.FormEvent) => {
9+
e.preventDefault();
10+
};
11+
12+
return (
13+
<section className="mb-6 overflow-hidden rounded-md border bg-white">
14+
<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">
15+
<h2 className="text-base font-semibold tracking-tight text-gray-900 sm:text-lg">Nộp bài dự thi</h2>
16+
<p className="mt-1.5 text-xs leading-relaxed text-gray-500 sm:text-sm">
17+
Vui lòng điền đầy đủ thông tin dưới đây để nộp bài tham dự thuyết trình.
18+
</p>
19+
</div>
20+
21+
<form onSubmit={handleSubmit} className="space-y-6 p-5 sm:p-6">
22+
{/* Product Link */}
23+
<div className="space-y-2">
24+
<label htmlFor="productLink" className="flex items-center gap-2 text-sm font-medium text-gray-700">
25+
<Link2 className="h-4 w-4 text-gray-400" />
26+
Link sản phẩm (Bao gồm slide .pptx, Sheet phân công .xlsx)
27+
<span className="text-red-500">*</span>
28+
</label>
29+
<Input
30+
id="productLink"
31+
type="url"
32+
placeholder="https://drive.google.com/..."
33+
className="focus:ring-primary/20 transition-all focus:ring-2"
34+
required
35+
/>
36+
<p className="flex items-center gap-1.5 text-xs text-gray-500">
37+
<span className="mt-0.5 h-1 w-1 flex-shrink-0 rounded-full bg-gray-400"></span>
38+
Yêu cầu để ở chế độ công khai để Ban Giám khảo có thể truy cập
39+
</p>
40+
</div>
41+
42+
{/* Code Link */}
43+
<div className="space-y-2">
44+
<label htmlFor="codeLink" className="flex items-center gap-2 text-sm font-medium text-gray-700">
45+
<Github className="h-4 w-4 text-gray-400" />
46+
Link source/Figma (Nếu đề tài yêu cầu sản phẩm)
47+
</label>
48+
<Input
49+
id="codeLink"
50+
type="url"
51+
placeholder="GitHub, GitLab, Figma..."
52+
className="focus:ring-primary/20 transition-all focus:ring-2"
53+
/>
54+
</div>
55+
56+
{/* Description */}
57+
<div className="space-y-2">
58+
<label htmlFor="description" className="flex items-center gap-2 text-sm font-medium text-gray-700">
59+
<FileText className="h-4 w-4 text-gray-400" />
60+
Mô tả (Tùy chọn)
61+
</label>
62+
<Textarea
63+
id="description"
64+
placeholder="Mô tả ngắn về sản phẩm, tính năng nổi bật, công nghệ sử dụng..."
65+
className="focus:ring-primary/20 min-h-[120px] resize-none transition-all focus:ring-2"
66+
// rows={5}
67+
/>
68+
<p className="flex items-center gap-1.5 text-xs text-gray-500">
69+
<span className="mt-0.5 h-1 w-1 flex-shrink-0 rounded-full bg-gray-400"></span>
70+
Chia sẻ thêm về sản phẩm đã làm được những gì hay để giảm khảo tập trung khai thác (tối đa 500
71+
ký tự)
72+
</p>
73+
</div>
74+
75+
{/* Submit Button */}
76+
<div className="flex items-center gap-3 border-t border-gray-200/70 pt-5">
77+
<Button type="submit" className="group flex items-center gap-2 transition-all hover:shadow-md">
78+
<Send className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
79+
Nộp bài
80+
</Button>
81+
<p className="text-xs text-gray-500">
82+
<span className="font-semibold text-red-600">Lưu ý:</span> Kiểm tra kỹ thông tin trước khi nộp
83+
</p>
84+
</div>
85+
</form>
86+
</section>
87+
);
88+
};
89+
90+
export default FormSubmit;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { ExternalLink, History, Clock, CheckCircle2, AlertCircle, XCircle } from "lucide-react";
2+
import { useState } from "react";
3+
interface Submission {
4+
id: number;
5+
phoneNumber: string;
6+
productLink: string;
7+
codeLink: string;
8+
submittedAt: string;
9+
status: "pending" | "approved" | "rejected";
10+
}
11+
12+
const HistorySubmit = () => {
13+
const [submissions] = useState<Submission[]>([
14+
{
15+
id: 1,
16+
phoneNumber: "0123456789",
17+
productLink: "https://demo.example.com",
18+
codeLink: "https://github.com/user/project",
19+
submittedAt: "2025-12-19 14:30:00",
20+
status: "approved",
21+
},
22+
{
23+
id: 2,
24+
phoneNumber: "0123456789",
25+
productLink: "https://demo-v2.example.com",
26+
codeLink: "https://github.com/user/project-v2",
27+
28+
submittedAt: "2025-12-19 16:45:00",
29+
status: "pending",
30+
},
31+
]);
32+
33+
const getStatusBadge = (status: Submission["status"]) => {
34+
switch (status) {
35+
case "approved":
36+
return (
37+
<span className="inline-flex items-center gap-1.5 rounded-md border border-green-200/50 bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">
38+
<CheckCircle2 className="h-3.5 w-3.5" />
39+
Đã ghi nhận
40+
</span>
41+
);
42+
case "rejected":
43+
return (
44+
<span className="inline-flex items-center gap-1.5 rounded-md border border-red-200/50 bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700">
45+
<XCircle className="h-3.5 w-3.5" />
46+
Không hợp lệ
47+
</span>
48+
);
49+
case "pending":
50+
default:
51+
return (
52+
<span className="inline-flex items-center gap-1.5 rounded-md border border-yellow-200/50 bg-yellow-50 px-2.5 py-1 text-xs font-medium text-yellow-700">
53+
<AlertCircle className="h-3.5 w-3.5" />
54+
Chờ xác nhận
55+
</span>
56+
);
57+
}
58+
};
59+
60+
return (
61+
<div className="overflow-hidden rounded-md border border-gray-200/70 bg-white">
62+
<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">
63+
<h2 className="text-base font-semibold tracking-tight text-gray-900 sm:text-lg">Lịch sử nộp bài</h2>
64+
<p className="mt-1.5 text-xs leading-relaxed text-gray-500 sm:text-sm">
65+
Danh sách các lần nộp bài của nhóm.
66+
</p>
67+
</div>
68+
69+
<div className="overflow-x-auto">
70+
{submissions.length === 0 ? (
71+
<div className="py-16 text-center">
72+
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-gray-100">
73+
<History className="h-8 w-8 text-gray-400" />
74+
</div>
75+
<p className="mt-4 text-sm font-medium text-gray-900">Chưa có lịch sử nộp bài</p>
76+
<p className="mt-1 text-xs text-gray-500">Hãy nộp bài dự thi của bạn ở form phía trên</p>
77+
</div>
78+
) : (
79+
<table className="w-full">
80+
<thead className="bg-gray-50/50">
81+
<tr>
82+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
83+
Lần nộp
84+
</th>
85+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
86+
Thời gian
87+
</th>
88+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
89+
Link sản phẩm
90+
</th>
91+
92+
<th className="px-4 py-3 text-left text-xs font-semibold tracking-wide text-gray-600 uppercase sm:px-6 sm:py-3.5">
93+
Trạng thái
94+
</th>
95+
</tr>
96+
</thead>
97+
<tbody className="divide-y divide-gray-200/60 bg-white">
98+
{submissions
99+
.slice()
100+
.reverse()
101+
.map((submission, index) => {
102+
const isLatest = index === 0;
103+
return (
104+
<tr key={submission.id} className="transition-colors hover:bg-gray-50/50">
105+
<td className="px-4 py-3.5 text-sm font-medium whitespace-nowrap text-gray-900 sm:px-6 sm:py-4">
106+
<div className="flex items-center gap-2">
107+
<span>#{submissions.length - index}</span>
108+
{isLatest && (
109+
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
110+
Mới nhất
111+
</span>
112+
)}
113+
</div>
114+
</td>
115+
<td className="px-4 py-3.5 text-sm whitespace-nowrap text-gray-600 sm:px-6 sm:py-4">
116+
<div className="flex items-center gap-1.5">
117+
<Clock className="h-3.5 w-3.5 text-gray-400" />
118+
<span>
119+
{new Date(submission.submittedAt).toLocaleString("vi-VN")}
120+
</span>
121+
</div>
122+
</td>
123+
<td className="px-4 py-3.5 text-sm sm:px-6 sm:py-4">
124+
<div className="flex flex-col gap-2">
125+
<a
126+
href={submission.productLink}
127+
target="_blank"
128+
rel="noopener noreferrer"
129+
className="text-primary hover:text-primary/80 flex items-center gap-1.5 font-medium transition-colors"
130+
>
131+
<ExternalLink className="h-3.5 w-3.5" />
132+
<span>Xem sản phẩm</span>
133+
</a>
134+
{submission.codeLink && (
135+
<a
136+
href={submission.codeLink}
137+
target="_blank"
138+
rel="noopener noreferrer"
139+
className="flex items-center gap-1.5 text-gray-600 transition-colors hover:text-gray-900"
140+
>
141+
<ExternalLink className="h-3.5 w-3.5" />
142+
<span>Mã nguồn</span>
143+
</a>
144+
)}
145+
</div>
146+
</td>
147+
148+
<td className="px-4 py-3.5 text-sm whitespace-nowrap sm:px-6 sm:py-4">
149+
{getStatusBadge(submission.status)}
150+
</td>
151+
</tr>
152+
);
153+
})}
154+
</tbody>
155+
</table>
156+
)}
157+
</div>
158+
</div>
159+
);
160+
};
161+
162+
export default HistorySubmit;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const Notification = () => {
2+
return (
3+
<div
4+
className="mb-6 rounded-lg border border-yellow-300/60 bg-linear-to-r from-yellow-50 to-amber-50 px-5 py-4 shadow-xs"
5+
role="alert"
6+
>
7+
<div className="flex items-center gap-3">
8+
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-yellow-500 text-white">
9+
<span className="text-sm font-bold">!</span>
10+
</div>
11+
<div className="flex-1">
12+
<p className="text-sm font-semibold text-red-600">Lưu ý quan trọng</p>
13+
<ul className="mt-2.5 space-y-2">
14+
<li className="flex items-center gap-2 text-sm text-gray-700">
15+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
16+
<span>
17+
<span className="font-semibold">Chỉ có leader</span> mới được phép đăng ký thời gian
18+
thuyết trình thử
19+
</span>
20+
</li>
21+
22+
<li className="flex items-center gap-2 text-sm text-gray-700">
23+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
24+
<span>
25+
Thời gian nhận đăng ký:{" "}
26+
<span className="font-semibold text-red-600">
27+
từ ngày 12/01/2026 - 23h59p 15/01/2026
28+
</span>
29+
</span>
30+
</li>
31+
32+
<li className="flex items-center gap-2 text-sm text-gray-700">
33+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
34+
<span>
35+
<span className="font-semibold">Lưu ý kiểm tra kỹ thông tin</span> trước khi đăng ký
36+
present thử
37+
</span>
38+
</li>
39+
40+
<li className="flex items-center gap-2 text-sm text-gray-700">
41+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
42+
<span>Tất cả các liên kết đến sản phẩm đều phải được mở công khai</span>
43+
</li>
44+
45+
<li className="flex items-center gap-2 text-sm text-gray-700">
46+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
47+
<span>
48+
<span className="font-semibold">Không cần nộp sản phẩm</span> (Nếu đề tài không yêu cầu)
49+
</span>
50+
</li>
51+
<li className="flex items-center gap-2 text-sm text-gray-700">
52+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500"></span>
53+
<span>
54+
Thời gian thuyết trình thử diễn ra từ:{" "}
55+
<span className="font-semibold">ngày 17/01 - 21/01</span> (Online qua Google Meet)
56+
</span>
57+
</li>
58+
</ul>
59+
</div>
60+
</div>
61+
</div>
62+
);
63+
};
64+
65+
export default Notification;

0 commit comments

Comments
 (0)