Changeset b67f274 for src/components/exams.tsx
- Timestamp:
- 12/21/25 18:28:25 (9 months ago)
- Branches:
- master
- Children:
- 90f7842
- Parents:
- 82c9c53
- File:
-
- 1 edited
-
src/components/exams.tsx (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/components/exams.tsx
r82c9c53 rb67f274 1 1 "use client" 2 import { use State } from 'react';2 import { useEffect, useMemo, useState } from 'react'; 3 3 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 4 import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'; 5 import { getAccessToken } from '@/lib/auth'; 6 7 type PassedSubject = { 8 id: number; 9 subjectId: number; 10 code: string; 11 subject: string; 12 credits: number; 13 grade: number; 14 gradeText: string; 15 date: string; // DD.MM.YYYY 16 semester: string; 17 professor: string; 18 }; 19 20 type PassedSubjectsResponse = { 21 passedSubjects: PassedSubject[]; 22 }; 23 24 type ExamRow = { 25 id: number; 26 code: string; 27 subject: string; 28 date: string; 29 semester: string; 30 credits: number; 31 grade: number; 32 }; 5 33 6 34 const Exams = () => { 7 35 const [sortField, setSortField] = useState<'grade' | 'date' | null>(null); 8 36 const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); 9 // Real exam data 10 const stats = { 11 average: 8.95, 12 credits: { current: 108, total: 240 }, 13 passed: 19, 14 remaining: 22 15 }; 16 17 const exams = [ 18 { 19 id: 1, 20 code: "F23L1W005", 21 subject: "Бизнис и менаџмент", 22 session: "2024 (Зимска) Прва испитна сесија", 23 date: "21.02.2024", 24 semester: 1, 25 credits: 6.00, 26 type: "З", 27 grade: 7 28 }, 29 { 30 id: 2, 31 code: "F23L1W007", 32 subject: "Вовед во компјутерските науки", 33 session: "2024 (Зимска) Прва испитна сесија", 34 date: "06.02.2024", 35 semester: 1, 36 credits: 6.00, 37 type: "З", 38 grade: 10 39 }, 40 { 41 id: 3, 42 code: "F23L2W003", 43 subject: "Избрани теми од математика", 44 session: "2024 (Зимска) Прва испитна сесија", 45 date: "12.02.2024", 46 semester: 1, 47 credits: 6.00, 48 type: "З", 49 grade: 9 50 }, 51 { 52 id: 4, 53 code: "F23L1W018", 54 subject: "Професионални вештини", 55 session: "2024 (Зимска) Прва испитна сесија", 56 date: "01.02.2024", 57 semester: 1, 58 credits: 4.00, 59 type: "З", 60 grade: 9 61 }, 62 { 63 id: 5, 64 code: "F23L1W004", 65 subject: "Спорт и здравје", 66 session: "2024 (Летна) Втора испитна сесија", 67 date: "15.07.2024", 68 semester: 1, 69 credits: 2.00, 70 type: "З", 71 grade: "Реализиран" 72 }, 73 { 74 id: 6, 75 code: "F23L1W020", 76 subject: "Структурно програмирање", 77 session: "2024 (Зимска) Прва испитна сесија", 78 date: "12.02.2024", 79 semester: 1, 80 credits: 6.00, 81 type: "З", 82 grade: 10 83 }, 84 { 85 id: 7, 86 code: "F23L1S003", 87 subject: "Архитектура и организација на компјутери", 88 session: "2024 (Летна) Втора испитна сесија", 89 date: "03.07.2024", 90 semester: 2, 91 credits: 6.00, 92 type: "З", 93 grade: 9 94 }, 95 { 96 id: 8, 97 code: "F23L1S023", 98 subject: "Бизнис статистика", 99 session: "2024 (Летна) Втора испитна сесија", 100 date: "02.07.2024", 101 semester: 2, 102 credits: 6.00, 103 type: "З", 104 grade: 8 105 }, 106 { 107 id: 9, 108 code: "F23L1S120", 109 subject: "Креативни вештини за решавање проблеми", 110 session: "2024 (Летна) Втора испитна сесија", 111 date: "25.06.2024", 112 semester: 2, 113 credits: 6.00, 114 type: "И", 115 grade: 10 116 }, 117 { 118 id: 10, 119 code: "F23L1S016", 120 subject: "Објектно-ориентирано програмирање", 121 session: "2024 (Летна) Втора испитна сесија", 122 date: "13.07.2024", 123 semester: 2, 124 credits: 6.00, 125 type: "З", 126 grade: 10 127 }, 128 { 129 id: 11, 130 code: "F23L1S146", 131 subject: "Основи на Веб дизајн", 132 session: "2024 (Летна) Втора испитна сесија", 133 date: "24.06.2024", 134 semester: 2, 135 credits: 6.00, 136 type: "З", 137 grade: 10 138 }, 139 { 140 id: 12, 141 code: "F23L2W100", 142 subject: "Економија за ИКТ инженери", 143 session: "2025 (Зимска) Прва испитна сесија", 144 date: "03.02.2025", 145 semester: 3, 146 credits: 6.00, 147 type: "З", 148 grade: 8 149 }, 150 { 151 id: 13, 152 code: "F23L2W109", 153 subject: "Интернет програмирање на клиентска страна", 154 session: "2025 (Зимска) Прва испитна сесија", 155 date: "30.01.2025", 156 semester: 3, 157 credits: 6.00, 158 type: "И", 159 grade: 10 160 }, 161 { 162 id: 14, 163 code: "F23L2W014", 164 subject: "Компјутерски мрежи и безбедност", 165 session: "2025 (Зимска) Прва испитна сесија", 166 date: "10.02.2025", 167 semester: 3, 168 credits: 6.00, 169 type: "З", 170 grade: 7 171 }, 172 { 173 id: 15, 174 code: "F23L2W201", 175 subject: "Примена на алгоритми и податочни структури", 176 session: "2025 (Зимска) Прва испитна сесија", 177 date: "13.02.2025", 178 semester: 3, 179 credits: 6.00, 180 type: "З", 181 grade: 10 182 }, 183 { 184 id: 16, 185 code: "F23L2W167", 186 subject: "Шаблони за дизајн на кориснички интерфејси", 187 session: "2025 (Зимска) Прва испитна сесија", 188 date: "06.02.2025", 189 semester: 3, 190 credits: 6.00, 191 type: "И", 192 grade: 10 193 }, 194 { 195 id: 17, 196 code: "F23L2S026", 197 subject: "Маркетинг", 198 session: "2025 (Летна) Втора испитна сесија", 199 date: "12.06.2025", 200 semester: 4, 201 credits: 6.00, 202 type: "З", 203 grade: 8 204 }, 205 { 206 id: 18, 207 code: "F23L2S017", 208 subject: "Оперативни системи", 209 session: "2025 (Летна) Втора испитна сесија", 210 date: "19.06.2025", 211 semester: 4, 212 credits: 6.00, 213 type: "З", 214 grade: 7 215 }, 216 { 217 id: 19, 218 code: "F23L2S029", 219 subject: "Софтверско инженерство", 220 session: "2025 (Летна) Втора испитна сесија", 221 date: "23.06.2025", 222 semester: 4, 223 credits: 6.00, 224 type: "З", 225 grade: 8 37 const [exams, setExams] = useState<ExamRow[]>([]); 38 const [isLoading, setIsLoading] = useState(true); 39 const [errorMessage, setErrorMessage] = useState<string | null>(null); 40 41 useEffect(() => { 42 let cancelled = false; 43 44 async function load() { 45 setIsLoading(true); 46 setErrorMessage(null); 47 48 const token = getAccessToken(); 49 if (!token) { 50 setErrorMessage('Not authenticated. Please login again.'); 51 setIsLoading(false); 52 return; 53 } 54 55 try { 56 const response = await fetch('http://localhost:5147/api/user/getPassedSubjects', { 57 method: 'GET', 58 headers: { 59 Authorization: `Bearer ${token}`, 60 }, 61 }); 62 63 if (!response.ok) { 64 const text = await response.text().catch(() => ''); 65 throw new Error(text || `Failed to load passed subjects (${response.status})`); 66 } 67 68 const data = (await response.json()) as PassedSubjectsResponse; 69 const rows: ExamRow[] = (data?.passedSubjects ?? []).map((s) => ({ 70 id: s.id, 71 code: s.code, 72 subject: s.subject, 73 date: s.date, 74 semester: s.semester, 75 credits: s.credits, 76 grade: s.grade, 77 })); 78 79 if (!cancelled) setExams(rows); 80 } catch (err) { 81 if (!cancelled) { 82 setErrorMessage(err instanceof Error ? err.message : 'Failed to load passed subjects.'); 83 } 84 } finally { 85 if (!cancelled) setIsLoading(false); 86 } 226 87 } 227 ]; 228 229 const getGradeColor = (grade: number | string) => { 230 if (grade === "Реализиран") return "bg-blue-100 text-blue-800 border-blue-200"; 231 if (typeof grade === "number") { 232 if (grade >= 9) return "bg-green-100 text-green-800 border-green-200"; 233 if (grade >= 8) return "bg-yellow-100 text-yellow-800 border-yellow-200"; 234 if (grade >= 6) return "bg-orange-100 text-orange-800 border-orange-200"; 235 return "bg-red-100 text-red-800 border-red-200"; 236 } 237 return "bg-gray-100 text-gray-800 border-gray-200"; 88 89 void load(); 90 91 return () => { 92 cancelled = true; 93 }; 94 }, []); 95 96 const stats = useMemo(() => { 97 const passed = exams.length; 98 const creditsCurrent = exams.reduce((sum, s) => sum + (typeof s.credits === 'number' ? s.credits : 0), 0); 99 const numericGrades = exams.map((e) => (typeof e.grade === 'number' ? e.grade : null)).filter((g): g is number => g !== null); 100 const average = 101 numericGrades.length > 0 102 ? numericGrades.reduce((sum, g) => sum + g, 0) / numericGrades.length 103 : 0; 104 105 const totalCredits = 240; 106 const totalExams = 40; 107 return { 108 average: Number.isFinite(average) ? Number(average.toFixed(2)) : 0, 109 credits: { current: creditsCurrent, total: totalCredits }, 110 passed, 111 remaining: Math.max(0, totalExams - passed), 112 }; 113 }, [exams]); 114 115 const getGradeColor = (grade: number) => { 116 if (grade >= 9) return "bg-green-100 text-green-800 border-green-200"; 117 if (grade >= 8) return "bg-yellow-100 text-yellow-800 border-yellow-200"; 118 if (grade >= 6) return "bg-orange-100 text-orange-800 border-orange-200"; 119 return "bg-red-100 text-red-800 border-red-200"; 238 120 }; 239 121 … … 275 157 return [...exams].sort((a, b) => { 276 158 if (sortField === 'grade') { 277 // Handle "Реализиран" grade separately 278 if (a.grade === "Реализиран" && b.grade === "Реализиран") return 0; 279 if (a.grade === "Реализиран") return sortOrder === 'asc' ? -1 : 1; 280 if (b.grade === "Реализиран") return sortOrder === 'asc' ? 1 : -1; 281 282 // Handle numeric grades 283 const gradeA = typeof a.grade === 'number' ? a.grade : 0; 284 const gradeB = typeof b.grade === 'number' ? b.grade : 0; 285 286 return sortOrder === 'asc' ? gradeA - gradeB : gradeB - gradeA; 159 return sortOrder === 'asc' ? a.grade - b.grade : b.grade - a.grade; 287 160 } 288 161 … … 307 180 }; 308 181 309 const creditsPercentage = (stats.credits.current / stats.credits.total) * 100; 182 const creditsPercentage = stats.credits.total > 0 ? (stats.credits.current / stats.credits.total) * 100 : 0; 183 184 if (isLoading) { 185 return ( 186 <div className="bg-white rounded-lg shadow-sm p-6"> 187 Loading... 188 </div> 189 ); 190 } 191 192 if (errorMessage) { 193 return ( 194 <div className="bg-white rounded-lg shadow-sm p-6"> 195 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 196 {errorMessage} 197 </div> 198 </div> 199 ); 200 } 310 201 311 202 return (
Note:
See TracChangeset
for help on using the changeset viewer.
