- Timestamp:
- 12/21/25 18:28:25 (9 months ago)
- Branches:
- master
- Children:
- 90f7842
- Parents:
- 82c9c53
- Location:
- src/app
- Files:
-
- 4 edited
-
page.tsx (modified) (5 diffs)
-
students/profile/page.tsx (modified) (2 diffs)
-
students/semesters/page.tsx (modified) (2 diffs)
-
students/subjects/page.tsx (modified) (3 diffs)
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
Note:
See TracChangeset
for help on using the changeset viewer.
