Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions navbar-and-home.patch
Original file line number Diff line number Diff line change
@@ -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<NavbarProps> = ({
>
Kiểm tra tốt nghiệp
</button>
+ <button
+ className={`tab-button ${activeTab === "subject_catalog" ? "active" : ""}`}
+ onClick={() => setActiveTab("subject_catalog")}
+ style={{
+ whiteSpace: "nowrap",
+ padding: isMobile ? "6px 8px" : "10px 20px",
+ fontSize: isMobile ? "12px" : "15px",
+ fontWeight: 600,
+ }}
+ >
+ Danh mục môn học
+ </button>
</div>
);

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<TabType>("grades");
@@ -1143,6 +1147,18 @@ export default function Home() {
</Suspense>
)}

+ {activeTab === "subject_catalog" && (
+ <Suspense
+ fallback={
+ <div style={{ padding: "20px", textAlign: "center" }}>
+ Loading subject catalog...
+ </div>
+ }
+ >
+ <SubjectCatalog />
+ </Suspense>
+ )}
+
{modalOpen && editing && (
<EditModal
editing={editing}
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
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.
5 changes: 4 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
packages:
- .
- .
allowBuilds:
sharp: true
unrs-resolver: true
12 changes: 12 additions & 0 deletions src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ const Navbar: React.FC<NavbarProps> = ({
>
Kiểm tra tốt nghiệp
</button>
<button
className={`tab-button ${activeTab === "subject_catalog" ? "active" : ""}`}
onClick={() => setActiveTab("subject_catalog")}
style={{
whiteSpace: "nowrap",
padding: isMobile ? "6px 8px" : "10px 20px",
fontSize: isMobile ? "12px" : "15px",
fontWeight: 600,
}}
>
Danh mục môn học
</button>
</div>
);

Expand Down
192 changes: 192 additions & 0 deletions src/components/SubjectCatalog/SubjectCatalog.tsx
Original file line number Diff line number Diff line change
@@ -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<Set<string>>(
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 (
<div className="instructions-container">
<h1>Danh mục môn học</h1>
<div className="instruction-item">
<p style={{ margin: 0 }}>
Đâ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 <em>&quot;Thêm môn&quot;</em>. Xin chân thành cảm ơn và chúc bạn có một trải nghiệm tốt ở Quamon.
</p>
</div>
<div className="dropdown-search-container" style={{ marginBottom: 20 }}>
<div className="search-input-wrapper">
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
className="search-icon"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="text"
placeholder="Tìm theo mã môn hoặc tên môn..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="dropdown-input"
aria-label="Tìm kiếm môn học"
/>
</div>
</div>

{groups.map(({ category, subjects, totalInCategory }) => {
const isExpanded = expandedCategories.has(category) || !!searchTerm.trim();
return (
<div key={category} className="instruction-item" style={{ padding: 0, overflow: "hidden" }}>
<button
type="button"
onClick={() => toggleCategory(category)}
className="category-header"
aria-expanded={isExpanded}
style={{
width: "100%",
textAlign: "left",
background: "transparent",
border: "none",
borderRadius: 0,
cursor: "pointer",
fontFamily: "inherit",
}}
>
<span className="category-title">
{category} ({totalInCategory} môn)
</span>
<span className="category-arrow">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
style={{
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
transition: "transform 0.2s ease",
}}
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</button>

{isExpanded && (
<div className="table-wrapper" style={{ padding: "0 16px 16px" }}>
{subjects.length === 0 ? (
<p style={{ padding: "12px 0", color: "var(--text-color)", opacity: 0.7 }}>
Không tìm thấy môn nào phù hợp trong nhóm này.
</p>
) : (
<table className="grade-table" style={{ width: "100%" }}>
<thead>
<tr>
<th>Mã môn</th>
<th>Tên môn (VI)</th>
<th>Tên môn (EN)</th>
<th>Tín chỉ</th>
<th>Quá trình</th>
<th>Giữa kỳ</th>
<th>Thực hành</th>
<th>Cuối kỳ</th>
</tr>
</thead>
<tbody>
{subjects.map((c) => (
<tr key={c.courseCode}>
<td>
<span className="subject-code">{c.courseCode}</span>
</td>
<td>{c.courseNameVi}</td>
<td>{c.courseNameEn}</td>
<td style={{ textAlign: "center" }}>{c.credits}</td>
<td style={{ textAlign: "center" }}>
{formatWeight(c.defaultWeights.progressWeight)}
</td>
<td style={{ textAlign: "center" }}>
{formatWeight(c.defaultWeights.midtermWeight)}
</td>
<td style={{ textAlign: "center" }}>
{formatWeight(c.defaultWeights.practiceWeight)}
</td>
<td style={{ textAlign: "center" }}>
{formatWeight(c.defaultWeights.finalTermWeight)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
);
})}
</div>
);
};

export default SubjectCatalog;
20 changes: 18 additions & 2 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<TabType>("grades");
Expand Down Expand Up @@ -1143,6 +1147,18 @@ export default function Home() {
</Suspense>
)}

{activeTab === "subject_catalog" && (
<Suspense
fallback={
<div style={{ padding: "20px", textAlign: "center" }}>
Loading subject catalog...
</div>
}
>
<SubjectCatalog />
</Suspense>
)}

{modalOpen && editing && (
<EditModal
editing={editing}
Expand All @@ -1163,4 +1179,4 @@ export default function Home() {
</div>
</>
);
}
}
Loading