diff --git a/navbar-and-home.patch b/navbar-and-home.patch new file mode 100644 index 0000000..eafadf4 --- /dev/null +++ b/navbar-and-home.patch @@ -0,0 +1,66 @@ +diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx +index 232bc4d..fd5253c 100644 +--- a/src/components/Navbar/Navbar.tsx ++++ b/src/components/Navbar/Navbar.tsx +@@ -103,6 +103,18 @@ const Navbar: React.FC = ({ + > + Kiểm tra tốt nghiệp + ++ + + ); + +diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx +index f83e9fe..64f7eb5 100644 +--- a/src/pages/Home.tsx ++++ b/src/pages/Home.tsx +@@ -13,6 +13,9 @@ const Instructions = lazy( + const AddSubjectForm = lazy( + () => import("../components/AddSubject/AddSubjectForm"), + ); ++const SubjectCatalog = lazy( ++ () => import("../components/SubjectCatalog/SubjectCatalog"), ++); + import { useGradeApp } from "../hooks/useGradeApp"; + import { uploadPdf } from "../config/appwrite"; + import { +@@ -29,7 +32,8 @@ export type TabType = + | "grades" + | "instructions" + | "add_subject" +- | "graduation_check"; ++ | "graduation_check" ++ | "subject_catalog"; + + export default function Home() { + const [activeTab, setActiveTab] = useState("grades"); +@@ -1143,6 +1147,18 @@ export default function Home() { + + )} + ++ {activeTab === "subject_catalog" && ( ++ ++ Loading subject catalog... ++ ++ } ++ > ++ ++ ++ )} ++ + {modalOpen && editing && ( + /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 33dd6e3..7ef83df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,5 @@ packages: - - . \ No newline at end of file + - . +allowBuilds: + sharp: true + unrs-resolver: true diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index 232bc4d..fd5253c 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -103,6 +103,18 @@ const Navbar: React.FC = ({ > Kiểm tra tốt nghiệp + ); diff --git a/src/components/SubjectCatalog/SubjectCatalog.tsx b/src/components/SubjectCatalog/SubjectCatalog.tsx new file mode 100644 index 0000000..86ee0c7 --- /dev/null +++ b/src/components/SubjectCatalog/SubjectCatalog.tsx @@ -0,0 +1,192 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +import { SUBJECTS_DATA } from "../../constants"; +import type { Course } from "../../types"; + +const CATEGORY_ORDER = [ + "Đại cương", + "Cơ sở ngành (CSN)", + "Chuyên ngành (CN/CNTC)", + "Khác (Tự chọn/CĐTN/TN)", +]; + +const normalize = (str: string) => + str + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/đ/g, "d") + .trim(); + +const formatWeight = (w: number) => `${Math.round(w * 100)}%`; + +const SubjectCatalog: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(""); + const [expandedCategories, setExpandedCategories] = useState>( + new Set(CATEGORY_ORDER), + ); + + const groups = useMemo(() => { + const term = normalize(searchTerm); + return CATEGORY_ORDER.map((category) => { + const all = (SUBJECTS_DATA[category] || []) as Course[]; + const filtered = term + ? all.filter( + (c) => + normalize(c.courseCode).includes(term) || + normalize(c.courseNameVi).includes(term) || + normalize(c.courseNameEn).includes(term), + ) + : all; + return { category, subjects: filtered, totalInCategory: all.length }; + }).filter((g) => g.subjects.length > 0 || !searchTerm.trim()); + }, [searchTerm]); + + const toggleCategory = (category: string) => { + setExpandedCategories((prev) => { + const next = new Set(prev); + if (next.has(category)) next.delete(category); + else next.add(category); + return next; + }); + }; + + return ( +
+

Danh mục môn học

+
+

+ Đây là danh sách các môn học hiện có trong hệ thống, kèm theo + trọng số tính điểm (Quá trình / Giữa kỳ / Thực hành / Cuối kỳ) của + từng môn. Nếu môn bạn cần không có trong danh sách này, bạn có thể + tự thêm vào ở tab "Thêm môn". Xin chân thành cảm ơn và chúc bạn có một trải nghiệm tốt ở Quamon. +

+
+
+
+ + setSearchTerm(e.target.value)} + className="dropdown-input" + aria-label="Tìm kiếm môn học" + /> +
+
+ + {groups.map(({ category, subjects, totalInCategory }) => { + const isExpanded = expandedCategories.has(category) || !!searchTerm.trim(); + return ( +
+ + + {isExpanded && ( +
+ {subjects.length === 0 ? ( +

+ Không tìm thấy môn nào phù hợp trong nhóm này. +

+ ) : ( + + + + + + + + + + + + + + + {subjects.map((c) => ( + + + + + + + + + + + ))} + +
Mã mônTên môn (VI)Tên môn (EN)Tín chỉQuá trìnhGiữa kỳThực hànhCuối kỳ
+ {c.courseCode} + {c.courseNameVi}{c.courseNameEn}{c.credits} + {formatWeight(c.defaultWeights.progressWeight)} + + {formatWeight(c.defaultWeights.midtermWeight)} + + {formatWeight(c.defaultWeights.practiceWeight)} + + {formatWeight(c.defaultWeights.finalTermWeight)} +
+ )} +
+ )} +
+ ); + })} +
+ ); +}; + +export default SubjectCatalog; \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index f83e9fe..40abf34 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -13,6 +13,9 @@ const Instructions = lazy( const AddSubjectForm = lazy( () => import("../components/AddSubject/AddSubjectForm"), ); +const SubjectCatalog = lazy( + () => import("../components/SubjectCatalog/SubjectCatalog"), +); import { useGradeApp } from "../hooks/useGradeApp"; import { uploadPdf } from "../config/appwrite"; import { @@ -29,7 +32,8 @@ export type TabType = | "grades" | "instructions" | "add_subject" - | "graduation_check"; + | "graduation_check" + | "subject_catalog"; export default function Home() { const [activeTab, setActiveTab] = useState("grades"); @@ -1143,6 +1147,18 @@ export default function Home() { )} + {activeTab === "subject_catalog" && ( + + Loading subject catalog... + + } + > + + + )} + {modalOpen && editing && ( ); -} +} \ No newline at end of file