- Timestamp:
- 12/21/25 18:28:25 (9 months ago)
- Branches:
- master
- Children:
- 90f7842
- Parents:
- 82c9c53
- Location:
- src
- Files:
-
- 1 added
- 2 deleted
- 5 edited
-
app/page.tsx (modified) (5 diffs)
-
app/students/profile/page.tsx (modified) (2 diffs)
-
app/students/semesters/page.tsx (modified) (2 diffs)
-
app/students/subjects/page.tsx (modified) (3 diffs)
-
components/exams.tsx (modified) (3 diffs)
-
data/semesters.json (deleted)
-
data/subjects.json (deleted)
-
lib/auth.ts (added)
Legend:
- Unmodified
- Added
- Removed
-
src/app/page.tsx
r82c9c53 rb67f274 11 11 } from '@fortawesome/free-solid-svg-icons'; 12 12 import { useState } from 'react'; 13 import { login } from '@/lib/auth'; 13 14 14 15 export default function LoginPage() { 15 16 const [showPassword, setShowPassword] = useState(false); 17 const [isSubmitting, setIsSubmitting] = useState(false); 18 const [errorMessage, setErrorMessage] = useState<string | null>(null); 16 19 const [formData, setFormData] = useState({ 17 username: '',20 email: '', 18 21 password: '' 19 22 }); … … 27 30 }; 28 31 29 const handleSubmit = (e: React.FormEvent) => {32 const handleSubmit = async (e: React.FormEvent) => { 30 33 e.preventDefault(); 31 // Handle login logic here 32 console.log('Login attempt:', formData); 34 setErrorMessage(null); 35 setIsSubmitting(true); 36 try { 37 await login({ email: formData.email, password: formData.password }); 38 window.location.href = '/students/profile'; 39 } catch (err) { 40 setErrorMessage(err instanceof Error ? err.message : 'Login failed.'); 41 } finally { 42 setIsSubmitting(false); 43 } 33 44 }; 34 45 … … 57 68 <div className="p-8"> 58 69 <form onSubmit={handleSubmit} className="space-y-6"> 59 {/* UsernameField */}70 {/* Email Field */} 60 71 <div> 61 <label htmlFor=" username" className="block text-sm font-medium text-gray-700 mb-2">62 Корисничко име72 <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> 73 Внесете е-пошта 63 74 </label> 64 75 <div className="relative"> … … 67 78 </div> 68 79 <input 69 id=" username"70 name=" username"71 type=" text"80 id="email" 81 name="email" 82 type="email" 72 83 required 73 value={formData. username}84 value={formData.email} 74 85 onChange={handleInputChange} 75 86 className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors" 76 placeholder="Внесете корисничко име"87 placeholder="Внесете е-пошта" 77 88 /> 78 89 </div> … … 132 143 </div> 133 144 145 {errorMessage && ( 146 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 147 {errorMessage} 148 </div> 149 )} 150 134 151 {/* Submit Button */} 135 152 <button 136 153 type="submit" 137 className="w-full bg-primary hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl" 154 disabled={isSubmitting} 155 className="w-full bg-primary hover:bg-blue-700 disabled:opacity-60 disabled:hover:bg-primary text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl" 138 156 > 139 157 <FontAwesomeIcon icon={faSignInAlt} className="h-4 w-4" /> 140 Најави се158 {isSubmitting ? 'Најави се...' : 'Најави се'} 141 159 </button> 142 160 </form> -
src/app/students/profile/page.tsx
r82c9c53 rb67f274 17 17 faPassport 18 18 } from '@fortawesome/free-solid-svg-icons'; 19 import studentData from '@/data/student-profile.json';20 19 import { useEffect, useState } from 'react'; 20 import { getAccessToken } from '@/lib/auth'; 21 21 import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; 22 23 type PersonalInfo = { 24 firstName: string; 25 middleName: string; 26 lastName: string; 27 maidenName: string; 28 dateOfBirth: string; 29 gender: string; 30 nationality: string; 31 citizenship: string; 32 scholarship: string; 33 currentPlan: string; 34 registryNumber: string; 35 studyGroup: string; 36 notes?: string; 37 index: string; 38 embg: string; 39 }; 40 41 type BirthInfo = { 42 placeOfBirth: string; 43 municipalityOfBirth: string; 44 country: string; 45 }; 46 47 type PreviousEducation = { 48 type: string; 49 profession: string; 50 average: string | number; 51 language: string; 52 country: string; 53 previousUniversity: string; 54 previousFaculty: string; 55 previousStudyMode: string; 56 }; 57 58 type EnrollmentInfo = { 59 enrollmentYear: string | number; 60 status: string; 61 cycle: string; 62 program: string; 63 quota: string; 64 secondaryEducationNumber: string; 65 previousEducationCredits: string | number; 66 }; 67 68 type Contact = { 69 placeOfResidence: string; 70 municipalityOfResidence: string; 71 country: string; 72 address: string; 73 temporaryAddress: string; 74 phone: string; 75 mobilePhone: string; 76 passportNumber: string; 77 passportExpiryDate: string; 78 email: string; 79 microsoftEmail: string; 80 }; 81 82 type StudentProfile = { 83 personalInfo: PersonalInfo; 84 birthInfo: BirthInfo; 85 previousEducation: PreviousEducation; 86 enrollmentInfo: EnrollmentInfo; 87 contact: Contact; 88 }; 22 89 23 90 interface InfoRowProps { … … 60 127 61 128 export default function ProfilePage() { 129 const [studentData, setStudentData] = useState<StudentProfile | null>(null); 130 const [isLoading, setIsLoading] = useState(true); 131 const [errorMessage, setErrorMessage] = useState<string | null>(null); 132 133 useEffect(() => { 134 let cancelled = false; 135 136 async function load() { 137 setIsLoading(true); 138 setErrorMessage(null); 139 140 const token = getAccessToken(); 141 if (!token) { 142 setErrorMessage('Not authenticated. Please login again.'); 143 setIsLoading(false); 144 return; 145 } 146 147 try { 148 const response = await fetch('http://localhost:5147/api/user/getUser', { 149 method: 'GET', 150 headers: { 151 Authorization: `Bearer ${token}`, 152 }, 153 }); 154 155 if (!response.ok) { 156 const text = await response.text().catch(() => ''); 157 throw new Error(text || `Failed to load profile (${response.status})`); 158 } 159 160 const data = (await response.json()) as StudentProfile; 161 if (!cancelled) setStudentData(data); 162 } catch (err) { 163 if (!cancelled) { 164 setErrorMessage(err instanceof Error ? err.message : 'Failed to load profile.'); 165 } 166 } finally { 167 if (!cancelled) setIsLoading(false); 168 } 169 } 170 171 void load(); 172 173 return () => { 174 cancelled = true; 175 }; 176 }, []); 177 178 if (isLoading) { 179 return ( 180 <div className="min-h-screen pb-8"> 181 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 182 Loading... 183 </div> 184 </div> 185 ); 186 } 187 188 if (errorMessage || !studentData) { 189 return ( 190 <div className="min-h-screen pb-8"> 191 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 192 {errorMessage ?? 'Failed to load profile.'} 193 </div> 194 </div> 195 ); 196 } 197 62 198 const { personalInfo, birthInfo, previousEducation, enrollmentInfo, contact } = studentData; 63 199 -
src/app/students/semesters/page.tsx
r82c9c53 rb67f274 12 12 faTimesCircle 13 13 } from '@fortawesome/free-solid-svg-icons'; 14 import semestersData from '@/data/semesters.json'; 14 import { useEffect, useState } from 'react'; 15 import { getAccessToken } from '@/lib/auth'; 16 17 type Semester = { 18 id: number | string; 19 semester: string; 20 direction: string; 21 quota: string; 22 note: string; 23 studentCom: string; 24 sum: string; 25 paid: string; 26 ukim: string; 27 createdOn: string; 28 dateChanged: string; 29 credits: string; 30 type: string; 31 doc: string; 32 doc1: string; 33 verified: string; 34 taxes: string; 35 signatures: string; 36 status: string; 37 completed: string; 38 }; 39 40 type SemestersResponse = { 41 semesters: Semester[]; 42 }; 15 43 16 44 interface TableCellProps { … … 81 109 82 110 export default function SemestersPage() { 83 const { semesters } = semestersData; 111 const [semesters, setSemesters] = useState<Semester[]>([]); 112 const [isLoading, setIsLoading] = useState(true); 113 const [errorMessage, setErrorMessage] = useState<string | null>(null); 114 115 useEffect(() => { 116 let cancelled = false; 117 118 async function load() { 119 setIsLoading(true); 120 setErrorMessage(null); 121 122 const token = getAccessToken(); 123 if (!token) { 124 setErrorMessage('Not authenticated. Please login again.'); 125 setIsLoading(false); 126 return; 127 } 128 129 try { 130 const response = await fetch('http://localhost:5147/api/user/getSemesters', { 131 method: 'GET', 132 headers: { 133 Authorization: `Bearer ${token}`, 134 }, 135 }); 136 137 if (!response.ok) { 138 const text = await response.text().catch(() => ''); 139 throw new Error(text || `Failed to load semesters (${response.status})`); 140 } 141 142 const data = (await response.json()) as SemestersResponse; 143 if (!cancelled) setSemesters(Array.isArray(data?.semesters) ? data.semesters : []); 144 } catch (err) { 145 if (!cancelled) { 146 setErrorMessage(err instanceof Error ? err.message : 'Failed to load semesters.'); 147 } 148 } finally { 149 if (!cancelled) setIsLoading(false); 150 } 151 } 152 153 void load(); 154 155 return () => { 156 cancelled = true; 157 }; 158 }, []); 159 160 if (isLoading) { 161 return ( 162 <div className="min-h-screen pb-8"> 163 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 164 Loading... 165 </div> 166 </div> 167 ); 168 } 169 170 if (errorMessage) { 171 return ( 172 <div className="min-h-screen pb-8"> 173 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 174 {errorMessage} 175 </div> 176 </div> 177 ); 178 } 84 179 85 180 return ( -
src/app/students/subjects/page.tsx
r82c9c53 rb67f274 7 7 faFileInvoice 8 8 } from '@fortawesome/free-solid-svg-icons'; 9 import { use State } from 'react';10 import subjectsData from '@/data/subjects.json';9 import { useEffect, useState } from 'react'; 10 import { getAccessToken } from '@/lib/auth'; 11 11 12 12 interface Subject { … … 21 21 group: string; 22 22 professor: string; 23 } 24 25 type SemesterInfo = { 26 id: number; 27 name: string; 28 status: string; 29 serviceNumber: string | number; 30 }; 31 32 type FinancialInfo = { 33 sum: string | number; 34 paid: string; 35 due: string; 36 materialCosts: string; 37 credits: string; 38 MKSA: string; 39 electronicRegistration: string; 40 eUKIM: string; 41 bankProvision: string; 42 total: string; 43 }; 44 45 type CurrentSemester = { 46 id: string; 47 name: string; 48 status: string; 49 serviceNumber: string | number; 50 ticketNumber: string; 51 debt: string; 52 financialInfo: FinancialInfo; 53 }; 54 55 type SubjectsResponse = { 56 currentSemester: CurrentSemester; 57 semesters: SemesterInfo[]; 58 subjectsBySemester: Record<string, Subject[]>; 59 semesterKeyById: Record<number, string>; 60 }; 61 62 type ApiCurrentSemester = { 63 id: string; 64 name: string; 65 status: string; 66 serviceNumber: string | number; 67 ticketNumber: string; 68 debt: string; 69 financialInfo: { 70 sum: string | number; 71 paid: string; 72 due: string; 73 materialCosts: string; 74 credits: string; 75 totalCredits?: string; 76 mksa?: string; 77 MKSA?: string; 78 electronicRegistration: string; 79 eUKIM: string; 80 bankProvision: string; 81 total: string; 82 }; 83 }; 84 85 type ApiSubjectsResponse = { 86 semesters: SemesterInfo[]; 87 currentSemester?: ApiCurrentSemester; 88 currentSemestar?: ApiCurrentSemester; 89 subjectsBySemester: Record<string, Subject[]>; 90 }; 91 92 function normalizeKey(input: string) { 93 return input.toLowerCase().replace(/\s|\(|\)|\.|,/g, ''); 94 } 95 96 function detectSeasonFromName(name: string): 'summer' | 'winter' | null { 97 const n = name.toLowerCase(); 98 if (n.includes('летен')) return 'summer'; 99 if (n.includes('зимски')) return 'winter'; 100 return null; 101 } 102 103 function buildSemesterKeyById(semesters: SemesterInfo[], keys: string[]) { 104 const keyBySeason: Partial<Record<'summer' | 'winter', string>> = {}; 105 for (const key of keys) { 106 const k = key.toLowerCase(); 107 if (k.includes('summer')) keyBySeason.summer = key; 108 if (k.includes('winter')) keyBySeason.winter = key; 109 } 110 111 const mapping: Record<number, string> = {}; 112 for (const s of semesters) { 113 const season = detectSeasonFromName(s.name); 114 const mapped = season ? keyBySeason[season] : undefined; 115 if (mapped) mapping[s.id] = mapped; 116 } 117 118 // Fallback: if we couldn't infer seasons, try matching by normalized names. 119 if (Object.keys(mapping).length === 0) { 120 for (const s of semesters) { 121 const ns = normalizeKey(s.name); 122 const match = keys.find((k) => normalizeKey(k).includes(ns) || ns.includes(normalizeKey(k))); 123 if (match) mapping[s.id] = match; 124 } 125 } 126 127 // Last resort: map in order. 128 if (Object.keys(mapping).length === 0) { 129 semesters.forEach((s, idx) => { 130 if (keys[idx]) mapping[s.id] = keys[idx]; 131 }); 132 } 133 134 return mapping; 23 135 } 24 136 … … 59 171 60 172 export default function SubjectsPage() { 61 const [selectedSemester, setSelectedSemester] = useState(subjectsData.currentSemester.id); 173 const [subjectsData, setSubjectsData] = useState<SubjectsResponse | null>(null); 174 const [selectedSemester, setSelectedSemester] = useState<number | null>(null); 62 175 const [isDropdownOpen, setIsDropdownOpen] = useState(false); 176 const [isLoading, setIsLoading] = useState(true); 177 const [errorMessage, setErrorMessage] = useState<string | null>(null); 178 179 useEffect(() => { 180 let cancelled = false; 181 182 async function load() { 183 setIsLoading(true); 184 setErrorMessage(null); 185 186 const token = getAccessToken(); 187 if (!token) { 188 setErrorMessage('Not authenticated. Please login again.'); 189 setIsLoading(false); 190 return; 191 } 192 193 try { 194 const response = await fetch('http://localhost:5147/api/user/getSubjects', { 195 method: 'GET', 196 headers: { 197 Authorization: `Bearer ${token}`, 198 }, 199 }); 200 201 if (!response.ok) { 202 const text = await response.text().catch(() => ''); 203 throw new Error(text || `Failed to load subjects (${response.status})`); 204 } 205 206 const apiData = (await response.json()) as ApiSubjectsResponse; 207 if (cancelled) return; 208 209 const current = apiData.currentSemester ?? apiData.currentSemestar; 210 if (!current) { 211 throw new Error('Response missing currentSemestar/currentSemester'); 212 } 213 214 const keys = Object.keys(apiData.subjectsBySemester ?? {}); 215 const semesterKeyById = buildSemesterKeyById(apiData.semesters ?? [], keys); 216 217 const normalized: SubjectsResponse = { 218 semesters: apiData.semesters ?? [], 219 subjectsBySemester: apiData.subjectsBySemester ?? {}, 220 semesterKeyById, 221 currentSemester: { 222 id: current.id, 223 name: current.name, 224 status: current.status, 225 serviceNumber: current.serviceNumber, 226 ticketNumber: current.ticketNumber, 227 debt: current.debt, 228 financialInfo: { 229 sum: current.financialInfo.sum, 230 paid: current.financialInfo.paid, 231 due: current.financialInfo.due, 232 materialCosts: current.financialInfo.materialCosts, 233 credits: current.financialInfo.credits, 234 MKSA: current.financialInfo.MKSA ?? current.financialInfo.mksa ?? '', 235 electronicRegistration: current.financialInfo.electronicRegistration, 236 eUKIM: current.financialInfo.eUKIM, 237 bankProvision: current.financialInfo.bankProvision, 238 total: current.financialInfo.total, 239 }, 240 }, 241 }; 242 243 setSubjectsData(normalized); 244 245 setSelectedSemester((prev) => { 246 if (prev !== null) return prev; 247 const season = detectSeasonFromName(current.name); 248 if (season) { 249 const key = keys.find((k) => k.toLowerCase().includes(season)); 250 if (key) { 251 const matchId = normalized.semesters.find((s) => normalized.semesterKeyById[s.id] === key)?.id; 252 if (typeof matchId === 'number') return matchId; 253 } 254 } 255 return normalized.semesters[0]?.id ?? null; 256 }); 257 } catch (err) { 258 if (!cancelled) { 259 setErrorMessage(err instanceof Error ? err.message : 'Failed to load subjects.'); 260 } 261 } finally { 262 if (!cancelled) setIsLoading(false); 263 } 264 } 265 266 void load(); 267 268 return () => { 269 cancelled = true; 270 }; 271 }, []); 272 273 if (isLoading) { 274 return ( 275 <div className="min-h-screen pb-8"> 276 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 277 Loading... 278 </div> 279 </div> 280 ); 281 } 282 283 if (errorMessage || !subjectsData || selectedSemester === null) { 284 return ( 285 <div className="min-h-screen pb-8"> 286 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 287 {errorMessage ?? 'Failed to load subjects.'} 288 </div> 289 </div> 290 ); 291 } 63 292 64 const currentSemesterData = subjectsData.semesters.find(s => s.id === selectedSemester) || subjectsData.currentSemester; 65 const currentSubjects: Subject[] = subjectsData.subjectsBySemester[selectedSemester as keyof typeof subjectsData.subjectsBySemester] || []; 293 const currentSemesterData = 294 subjectsData.semesters.find((s) => s.id === selectedSemester) ?? subjectsData.semesters[0]; 295 const semesterKey = 296 subjectsData.semesterKeyById[selectedSemester] ?? 297 Object.keys(subjectsData.subjectsBySemester)[0]; 298 const currentSubjects: Subject[] = semesterKey ? subjectsData.subjectsBySemester[semesterKey] || [] : []; 66 299 const { currentSemester } = subjectsData; 67 300 -
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.
