source: frontend/src/app/students/profile/page.tsx

Last change on this file was b8093a0, checked in by imbrsk <boris696boris@…>, 4 days ago

Merge iknow-remaster into frontend/

Bring the Next.js frontend into this repository as a monorepo subdirectory,
preserving its full commit history via a subtree merge.

  • Property mode set to 100644
File size: 11.0 KB
RevLine 
[636f86c]1"use client"
2
3import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4import {
5 faUser,
6 faIdCard,
7 faGraduationCap,
8 faAddressCard,
9 faSchool,
10 faCalendarAlt,
11 faFlag,
12 faVenus,
13 faMars,
14 faEnvelope,
15 faPhone,
16 faMapMarkerAlt,
17 faPassport
18} from '@fortawesome/free-solid-svg-icons';
[b67f274]19import { useEffect, useState } from 'react';
20import { getAccessToken } from '@/lib/auth';
[82c9c53]21import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
[50f2fb4]22import { useTranslation } from 'react-i18next';
[8496f3c]23import { apiUrl } from '@/lib/api';
[82c9c53]24
[b67f274]25type PersonalInfo = {
26 firstName: string;
27 middleName: string;
28 lastName: string;
29 maidenName: string;
30 dateOfBirth: string;
31 gender: string;
32 nationality: string;
33 citizenship: string;
34 scholarship: string;
35 currentPlan: string;
36 registryNumber: string;
37 studyGroup: string;
38 notes?: string;
39 index: string;
40 embg: string;
41};
42
43type BirthInfo = {
44 placeOfBirth: string;
45 municipalityOfBirth: string;
46 country: string;
47};
48
49type PreviousEducation = {
50 type: string;
51 profession: string;
52 average: string | number;
53 language: string;
54 country: string;
55 previousUniversity: string;
56 previousFaculty: string;
57 previousStudyMode: string;
58};
59
60type EnrollmentInfo = {
61 enrollmentYear: string | number;
62 status: string;
63 cycle: string;
64 program: string;
65 quota: string;
66 secondaryEducationNumber: string;
67 previousEducationCredits: string | number;
68};
69
70type Contact = {
71 placeOfResidence: string;
72 municipalityOfResidence: string;
73 country: string;
74 address: string;
75 temporaryAddress: string;
76 phone: string;
77 mobilePhone: string;
78 passportNumber: string;
79 passportExpiryDate: string;
80 email: string;
81 microsoftEmail: string;
82};
83
84type StudentProfile = {
85 personalInfo: PersonalInfo;
86 birthInfo: BirthInfo;
87 previousEducation: PreviousEducation;
88 enrollmentInfo: EnrollmentInfo;
89 contact: Contact;
90};
91
[636f86c]92interface InfoRowProps {
93 label: string;
94 value: string | number;
[82c9c53]95 icon?: IconDefinition;
[636f86c]96}
97
[50f2fb4]98const InfoRow = ({ label, value, icon }: InfoRowProps) => {
99 const { t } = useTranslation();
100 return (
[ba52069]101 <div className="flex justify-between items-center py-3 border-b border-border last:border-b-0">
102 <div className="flex items-center gap-2 text-muted-foreground font-medium">
[50f2fb4]103 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />}
104 <span>{t(label)}:</span>
105 </div>
[ba52069]106 <div className="text-card-foreground font-semibold text-right max-w-xs break-words">
[50f2fb4]107 {value || t('n_a')}
108 </div>
[636f86c]109 </div>
[50f2fb4]110 );
111};
[636f86c]112
113interface SectionProps {
114 title: string;
[82c9c53]115 icon: IconDefinition;
[636f86c]116 children: React.ReactNode;
117}
118
[50f2fb4]119const Section = ({ title, icon, children }: SectionProps) => {
120 const { t } = useTranslation();
121 return (
[ba52069]122 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
[50f2fb4]123 <div className="bg-primary text-white px-6 py-4">
124 <div className="flex items-center gap-3">
125 <FontAwesomeIcon icon={icon} className="text-xl" />
126 <h2 className="text-xl font-bold">{t(title)}</h2>
127 </div>
128 </div>
129 <div className="p-6">
130 {children}
[636f86c]131 </div>
132 </div>
[50f2fb4]133 );
134};
[636f86c]135
136export default function ProfilePage() {
[b67f274]137 const [studentData, setStudentData] = useState<StudentProfile | null>(null);
138 const [isLoading, setIsLoading] = useState(true);
139 const [errorMessage, setErrorMessage] = useState<string | null>(null);
[50f2fb4]140 const { t } = useTranslation();
[b67f274]141
142 useEffect(() => {
143 let cancelled = false;
144
145 async function load() {
146 setIsLoading(true);
147 setErrorMessage(null);
148
149 const token = getAccessToken();
150 if (!token) {
151 setErrorMessage('Not authenticated. Please login again.');
152 setIsLoading(false);
153 return;
154 }
155
156 try {
[8496f3c]157 const response = await fetch(apiUrl('/api/user/getUser'), {
[b67f274]158 method: 'GET',
159 headers: {
160 Authorization: `Bearer ${token}`,
161 },
162 });
163
164 if (!response.ok) {
165 const text = await response.text().catch(() => '');
166 throw new Error(text || `Failed to load profile (${response.status})`);
167 }
168
169 const data = (await response.json()) as StudentProfile;
170 if (!cancelled) setStudentData(data);
171 } catch (err) {
172 if (!cancelled) {
173 setErrorMessage(err instanceof Error ? err.message : 'Failed to load profile.');
174 }
175 } finally {
176 if (!cancelled) setIsLoading(false);
177 }
178 }
179
180 void load();
181
182 return () => {
183 cancelled = true;
184 };
185 }, []);
186
187 if (isLoading) {
188 return (
189 <div className="min-h-screen pb-8">
[ba52069]190 <div className="bg-card rounded-xl shadow-sm border border-border p-6">
[50f2fb4]191 {t('loading')}
[b67f274]192 </div>
193 </div>
194 );
195 }
196
197 if (errorMessage || !studentData) {
198 return (
199 <div className="min-h-screen pb-8">
200 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
[50f2fb4]201 {errorMessage ?? t('failed_to_load_profile')}
[b67f274]202 </div>
203 </div>
204 );
205 }
206
[636f86c]207 const { personalInfo, birthInfo, previousEducation, enrollmentInfo, contact } = studentData;
208
209 return (
210 <div className="min-h-screen pb-8">
211 {/* Header */}
212 <div className="bg-primary text-white rounded-xl p-8 mb-8">
213 <div className="flex items-center gap-6">
214 <div className="relative">
[ba52069]215 <div className="w-20 h-20 bg-white rounded-full flex items-center justify-center border-2 border-white shadow-lg">
216 <FontAwesomeIcon icon={faUser} className="text-3xl text-[#0272D1]" />
[636f86c]217 </div>
218 <div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
219 </div>
220 <div>
221 <h1 className="text-3xl font-bold mb-2">
222 {personalInfo.firstName} {personalInfo.middleName} {personalInfo.lastName}
223 </h1>
224 <div className="text-lg opacity-90">
[50f2fb4]225 {t('index')}: {personalInfo.index} | {t('embg')}: {personalInfo.embg}
[636f86c]226 </div>
227 <div className="text-base opacity-80 mt-1">
228 {enrollmentInfo.program}
229 </div>
230 </div>
231 </div>
232 </div>
233
234 {/* Profile Sections */}
235 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
236
237 {/* Personal Information */}
[50f2fb4]238 <Section title="personal_info" icon={faIdCard}>
239 <InfoRow label="first_name" value={personalInfo.firstName} />
240 <InfoRow label="middle_name" value={personalInfo.middleName} />
241 <InfoRow label="last_name" value={personalInfo.lastName} />
242 <InfoRow label="maiden_name" value={personalInfo.maidenName} />
[636f86c]243 <InfoRow
[50f2fb4]244 label="date_of_birth"
[636f86c]245 value={personalInfo.dateOfBirth}
246 icon={faCalendarAlt}
247 />
248 <InfoRow
[50f2fb4]249 label="gender"
[636f86c]250 value={personalInfo.gender}
[50f2fb4]251 icon={personalInfo.gender === t('male') ? faMars : faVenus}
[636f86c]252 />
253 <InfoRow
[50f2fb4]254 label="nationality"
[636f86c]255 value={personalInfo.nationality}
256 icon={faFlag}
257 />
[50f2fb4]258 <InfoRow label="citizenship" value={personalInfo.citizenship} />
259 <InfoRow label="scholarship" value={personalInfo.scholarship} />
260 <InfoRow label="current_plan" value={personalInfo.currentPlan} />
261 <InfoRow label="registry_number" value={personalInfo.registryNumber} />
262 <InfoRow label="study_group" value={personalInfo.studyGroup} />
[636f86c]263 {personalInfo.notes && (
264 <div className="mt-4 p-4 bg-blue-50 rounded-lg">
[50f2fb4]265 <div className="text-sm font-medium text-blue-800 mb-1">{t('note')}:</div>
[636f86c]266 <div className="text-sm text-blue-700">{personalInfo.notes}</div>
267 </div>
268 )}
269 </Section>
270
271 {/* Birth Information */}
[50f2fb4]272 <Section title="birth_info" icon={faMapMarkerAlt}>
273 <InfoRow label="place_of_birth" value={birthInfo.placeOfBirth} />
274 <InfoRow label="municipality_of_birth" value={birthInfo.municipalityOfBirth} />
275 <InfoRow label="country" value={birthInfo.country} />
[636f86c]276 </Section>
277
278 {/* Previous Education */}
[50f2fb4]279 <Section title="previous_education" icon={faSchool}>
280 <InfoRow label="type" value={previousEducation.type} />
281 <InfoRow label="profession" value={previousEducation.profession} />
282 <InfoRow label="average" value={previousEducation.average} />
283 <InfoRow label="language" value={previousEducation.language} />
284 <InfoRow label="country" value={previousEducation.country} />
285 <InfoRow label="previous_university" value={previousEducation.previousUniversity} />
286 <InfoRow label="previous_faculty" value={previousEducation.previousFaculty} />
287 <InfoRow label="previous_study_mode" value={previousEducation.previousStudyMode} />
[636f86c]288 </Section>
289
290 {/* Enrollment Information */}
[50f2fb4]291 <Section title="enrollment_info" icon={faGraduationCap}>
292 <InfoRow label="enrollment_year" value={enrollmentInfo.enrollmentYear} />
293 <InfoRow label="status" value={enrollmentInfo.status} />
294 <InfoRow label="cycle" value={enrollmentInfo.cycle} />
295 <InfoRow label="program" value={enrollmentInfo.program} />
296 <InfoRow label="quota" value={enrollmentInfo.quota} />
297 <InfoRow label="secondary_education_number" value={enrollmentInfo.secondaryEducationNumber} />
298 <InfoRow label="previous_education_credits" value={enrollmentInfo.previousEducationCredits} />
[636f86c]299 </Section>
300
301 {/* Contact Information */}
302 <div className="lg:col-span-2">
[50f2fb4]303 <Section title="contact" icon={faAddressCard}>
[636f86c]304 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
305 <div>
306 <InfoRow
[50f2fb4]307 label="place_of_residence"
[636f86c]308 value={contact.placeOfResidence}
309 icon={faMapMarkerAlt}
310 />
[50f2fb4]311 <InfoRow label="municipality_of_residence" value={contact.municipalityOfResidence} />
312 <InfoRow label="country" value={contact.country} />
313 <InfoRow label="address" value={contact.address} />
314 <InfoRow label="temporary_address" value={contact.temporaryAddress} />
[636f86c]315 </div>
316 <div>
[50f2fb4]317 <InfoRow label="phone" value={contact.phone} icon={faPhone} />
318 <InfoRow label="mobile_phone" value={contact.mobilePhone} icon={faPhone} />
319 <InfoRow label="passport_number" value={contact.passportNumber} icon={faPassport} />
320 <InfoRow label="passport_expiry_date" value={contact.passportExpiryDate} />
[636f86c]321 <InfoRow
[50f2fb4]322 label="email"
[636f86c]323 value={contact.email}
324 icon={faEnvelope}
325 />
326 <InfoRow
[50f2fb4]327 label="microsoft_email"
[636f86c]328 value={contact.microsoftEmail}
329 icon={faEnvelope}
330 />
331 </div>
332 </div>
333 </Section>
334 </div>
335 </div>
336 </div>
337 );
338}
Note: See TracBrowser for help on using the repository browser.