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

Last change on this file was b8093a0, checked in by imbrsk <boris696boris@…>, 3 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: 9.2 KB
Line 
1"use client";
2
3import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4import {
5 faUser,
6 faIdCard,
7 faAddressCard,
8 faCalendarAlt,
9 faFlag,
10 faVenus,
11 faMars,
12 faEnvelope,
13 faPhone,
14 faMapMarkerAlt,
15 faPassport,
16} from "@fortawesome/free-solid-svg-icons";
17import { useEffect, useState } from "react";
18import { getAccessToken } from "@/lib/auth";
19import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
20import { useTranslation } from 'react-i18next';
21import { apiUrl } from '@/lib/api';
22
23type 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
41type BirthInfo = {
42 placeOfBirth: string;
43 municipalityOfBirth: string;
44 country: string;
45};
46
47// Present in the API response, but intentionally not shown on professor profile.
48type PreviousEducation = {
49 type: string;
50 profession: string;
51 average: string | number;
52 language: string;
53 country: string;
54 previousUniversity: string;
55 previousFaculty: string;
56 previousStudyMode: string;
57};
58
59// Present in the API response, but intentionally not shown on professor profile.
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
92interface InfoRowProps {
93 label: string;
94 value: string | number;
95 icon?: IconDefinition;
96}
97
98const InfoRow = ({ label, value, icon }: InfoRowProps) => {
99 const { t } = useTranslation();
100 return (
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">
103 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />}
104 <span>{t(label)}:</span>
105 </div>
106 <div className="text-card-foreground font-semibold text-right max-w-xs break-words">
107 {value || t('n_a')}
108 </div>
109 </div>
110 );
111};
112
113interface SectionProps {
114 title: string;
115 icon: IconDefinition;
116 children: React.ReactNode;
117}
118
119const Section = ({ title, icon, children }: SectionProps) => {
120 const { t } = useTranslation();
121 return (
122 <div className="bg-card rounded-xl shadow-sm border border-border overflow-hidden">
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">{children}</div>
130 </div>
131 );
132};
133
134export default function ProfessorProfilePage() {
135 const [profileData, setProfileData] = useState<StudentProfile | null>(null);
136 const [isLoading, setIsLoading] = useState(true);
137 const [errorMessage, setErrorMessage] = useState<string | null>(null);
138 const { t } = useTranslation();
139
140 useEffect(() => {
141 let cancelled = false;
142
143 async function load() {
144 setIsLoading(true);
145 setErrorMessage(null);
146
147 const token = getAccessToken();
148 if (!token) {
149 setErrorMessage("Not authenticated. Please login again.");
150 setIsLoading(false);
151 return;
152 }
153
154 try {
155 const response = await fetch(apiUrl("/api/user/getUser"), {
156 method: "GET",
157 headers: {
158 Authorization: `Bearer ${token}`,
159 },
160 });
161
162 if (!response.ok) {
163 const text = await response.text().catch(() => "");
164 throw new Error(text || `Failed to load profile (${response.status})`);
165 }
166
167 const data = (await response.json()) as StudentProfile;
168 if (!cancelled) setProfileData(data);
169 } catch (err) {
170 if (!cancelled) {
171 setErrorMessage(err instanceof Error ? err.message : "Failed to load profile.");
172 }
173 } finally {
174 if (!cancelled) setIsLoading(false);
175 }
176 }
177
178 void load();
179
180 return () => {
181 cancelled = true;
182 };
183 }, []);
184
185 if (isLoading) {
186 return (
187 <div className="min-h-screen pb-8">
188 <div className="bg-card rounded-xl shadow-sm border border-border p-6">{t('loading')}</div>
189 </div>
190 );
191 }
192
193 if (errorMessage || !profileData) {
194 return (
195 <div className="min-h-screen pb-8">
196 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
197 {errorMessage ?? t('failed_to_load_profile')}
198 </div>
199 </div>
200 );
201 }
202
203 const { personalInfo, birthInfo, contact } = profileData;
204
205 return (
206 <div className="min-h-screen pb-8">
207 {/* Header */}
208 <div className="bg-primary text-white rounded-xl p-8 mb-8">
209 <div className="flex items-center gap-6">
210 <div className="relative">
211 <div className="w-20 h-20 bg-white rounded-full flex items-center justify-center border-2 border-white shadow-lg">
212 <FontAwesomeIcon icon={faUser} className="text-3xl text-[#0272D1]" />
213 </div>
214 <div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
215 </div>
216 <div>
217 <h1 className="text-3xl font-bold mb-2">
218 {personalInfo.firstName} {personalInfo.middleName} {personalInfo.lastName}
219 </h1>
220 <div className="text-lg opacity-90">
221 {t('index')}: {personalInfo.index} | {t('embg')}: {personalInfo.embg}
222 </div>
223 </div>
224 </div>
225 </div>
226
227 {/* Profile Sections */}
228 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
229 {/* Personal Information */}
230 <Section title="personal_info" icon={faIdCard}>
231 <InfoRow label="first_name" value={personalInfo.firstName} />
232 <InfoRow label="middle_name" value={personalInfo.middleName} />
233 <InfoRow label="last_name" value={personalInfo.lastName} />
234 <InfoRow label="maiden_name" value={personalInfo.maidenName} />
235 <InfoRow label="date_of_birth" value={personalInfo.dateOfBirth} icon={faCalendarAlt} />
236 <InfoRow
237 label="gender"
238 value={personalInfo.gender}
239 icon={personalInfo.gender === t('male') ? faMars : faVenus}
240 />
241 <InfoRow label="nationality" value={personalInfo.nationality} icon={faFlag} />
242 <InfoRow label="citizenship" value={personalInfo.citizenship} />
243 <InfoRow label="scholarship" value={personalInfo.scholarship} />
244 <InfoRow label="current_plan" value={personalInfo.currentPlan} />
245 <InfoRow label="registry_number" value={personalInfo.registryNumber} />
246 <InfoRow label="study_group" value={personalInfo.studyGroup} />
247 {personalInfo.notes && (
248 <div className="mt-4 p-4 bg-blue-50 rounded-lg">
249 <div className="text-sm font-medium text-blue-800 mb-1">{t('note')}:</div>
250 <div className="text-sm text-blue-700">{personalInfo.notes}</div>
251 </div>
252 )}
253 </Section>
254
255 {/* Birth Information */}
256 <Section title="birth_info" icon={faMapMarkerAlt}>
257 <InfoRow label="place_of_birth" value={birthInfo.placeOfBirth} />
258 <InfoRow label="municipality_of_birth" value={birthInfo.municipalityOfBirth} />
259 <InfoRow label="country" value={birthInfo.country} />
260 </Section>
261
262 {/* Contact Information */}
263 <div className="lg:col-span-2">
264 <Section title="contact" icon={faAddressCard}>
265 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
266 <div>
267 <InfoRow label="place_of_residence" value={contact.placeOfResidence} icon={faMapMarkerAlt} />
268 <InfoRow label="municipality_of_residence" value={contact.municipalityOfResidence} />
269 <InfoRow label="country" value={contact.country} />
270 <InfoRow label="address" value={contact.address} />
271 <InfoRow label="temporary_address" value={contact.temporaryAddress} />
272 </div>
273 <div>
274 <InfoRow label="phone" value={contact.phone} icon={faPhone} />
275 <InfoRow label="mobile_phone" value={contact.mobilePhone} icon={faPhone} />
276 <InfoRow label="passport_number" value={contact.passportNumber} icon={faPassport} />
277 <InfoRow label="passport_expiry_date" value={contact.passportExpiryDate} />
278 <InfoRow label="email" value={contact.email} icon={faEnvelope} />
279 <InfoRow label="microsoft_email" value={contact.microsoftEmail} icon={faEnvelope} />
280 </div>
281 </div>
282 </Section>
283 </div>
284 </div>
285 </div>
286 );
287}
Note: See TracBrowser for help on using the repository browser.