source: src/app/professor/profile/page.tsx@ 50f2fb4

Last change on this file since 50f2fb4 was 50f2fb4, checked in by Stefan-Saveski <stefansaveski19@…>, 8 months ago

Added dual language feature.

  • 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';
21
22type PersonalInfo = {
23 firstName: string;
24 middleName: string;
25 lastName: string;
26 maidenName: string;
27 dateOfBirth: string;
28 gender: string;
29 nationality: string;
30 citizenship: string;
31 scholarship: string;
32 currentPlan: string;
33 registryNumber: string;
34 studyGroup: string;
35 notes?: string;
36 index: string;
37 embg: string;
38};
39
40type BirthInfo = {
41 placeOfBirth: string;
42 municipalityOfBirth: string;
43 country: string;
44};
45
46// Present in the API response, but intentionally not shown on professor profile.
47type 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// Present in the API response, but intentionally not shown on professor profile.
59type EnrollmentInfo = {
60 enrollmentYear: string | number;
61 status: string;
62 cycle: string;
63 program: string;
64 quota: string;
65 secondaryEducationNumber: string;
66 previousEducationCredits: string | number;
67};
68
69type Contact = {
70 placeOfResidence: string;
71 municipalityOfResidence: string;
72 country: string;
73 address: string;
74 temporaryAddress: string;
75 phone: string;
76 mobilePhone: string;
77 passportNumber: string;
78 passportExpiryDate: string;
79 email: string;
80 microsoftEmail: string;
81};
82
83type StudentProfile = {
84 personalInfo: PersonalInfo;
85 birthInfo: BirthInfo;
86 previousEducation: PreviousEducation;
87 enrollmentInfo: EnrollmentInfo;
88 contact: Contact;
89};
90
91interface InfoRowProps {
92 label: string;
93 value: string | number;
94 icon?: IconDefinition;
95}
96
97const InfoRow = ({ label, value, icon }: InfoRowProps) => {
98 const { t } = useTranslation();
99 return (
100 <div className="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0">
101 <div className="flex items-center gap-2 text-gray-600 font-medium">
102 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />}
103 <span>{t(label)}:</span>
104 </div>
105 <div className="text-gray-900 font-semibold text-right max-w-xs break-words">
106 {value || t('n_a')}
107 </div>
108 </div>
109 );
110};
111
112interface SectionProps {
113 title: string;
114 icon: IconDefinition;
115 children: React.ReactNode;
116}
117
118const Section = ({ title, icon, children }: SectionProps) => {
119 const { t } = useTranslation();
120 return (
121 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
122 <div className="bg-primary text-white px-6 py-4">
123 <div className="flex items-center gap-3">
124 <FontAwesomeIcon icon={icon} className="text-xl" />
125 <h2 className="text-xl font-bold">{t(title)}</h2>
126 </div>
127 </div>
128 <div className="p-6">{children}</div>
129 </div>
130 );
131};
132
133export default function ProfessorProfilePage() {
134 const [profileData, setProfileData] = useState<StudentProfile | null>(null);
135 const [isLoading, setIsLoading] = useState(true);
136 const [errorMessage, setErrorMessage] = useState<string | null>(null);
137 const { t } = useTranslation();
138
139 useEffect(() => {
140 let cancelled = false;
141
142 async function load() {
143 setIsLoading(true);
144 setErrorMessage(null);
145
146 const token = getAccessToken();
147 if (!token) {
148 setErrorMessage("Not authenticated. Please login again.");
149 setIsLoading(false);
150 return;
151 }
152
153 try {
154 const response = await fetch("https://iknow-api.onrender.com/api/user/getUser", {
155 method: "GET",
156 headers: {
157 Authorization: `Bearer ${token}`,
158 },
159 });
160
161 if (!response.ok) {
162 const text = await response.text().catch(() => "");
163 throw new Error(text || `Failed to load profile (${response.status})`);
164 }
165
166 const data = (await response.json()) as StudentProfile;
167 if (!cancelled) setProfileData(data);
168 } catch (err) {
169 if (!cancelled) {
170 setErrorMessage(err instanceof Error ? err.message : "Failed to load profile.");
171 }
172 } finally {
173 if (!cancelled) setIsLoading(false);
174 }
175 }
176
177 void load();
178
179 return () => {
180 cancelled = true;
181 };
182 }, []);
183
184 if (isLoading) {
185 return (
186 <div className="min-h-screen pb-8">
187 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">{t('loading')}</div>
188 </div>
189 );
190 }
191
192 if (errorMessage || !profileData) {
193 return (
194 <div className="min-h-screen pb-8">
195 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
196 {errorMessage ?? t('failed_to_load_profile')}
197 </div>
198 </div>
199 );
200 }
201
202 const { personalInfo, birthInfo, contact } = profileData;
203
204 return (
205 <div className="min-h-screen pb-8">
206 {/* Header */}
207 <div className="bg-primary text-white rounded-xl p-8 mb-8">
208 <div className="flex items-center gap-6">
209 <div className="relative">
210 <div className="w-20 h-20 bg-white bg-opacity-20 rounded-full flex items-center justify-center border-2 border-white border-opacity-30">
211 <FontAwesomeIcon icon={faUser} className="text-3xl text-primary" />
212 </div>
213 <div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
214 </div>
215 <div>
216 <h1 className="text-3xl font-bold mb-2">
217 {personalInfo.firstName} {personalInfo.middleName} {personalInfo.lastName}
218 </h1>
219 <div className="text-lg opacity-90">
220 {t('index')}: {personalInfo.index} | {t('embg')}: {personalInfo.embg}
221 </div>
222 </div>
223 </div>
224 </div>
225
226 {/* Profile Sections */}
227 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
228 {/* Personal Information */}
229 <Section title="personal_info" icon={faIdCard}>
230 <InfoRow label="first_name" value={personalInfo.firstName} />
231 <InfoRow label="middle_name" value={personalInfo.middleName} />
232 <InfoRow label="last_name" value={personalInfo.lastName} />
233 <InfoRow label="maiden_name" value={personalInfo.maidenName} />
234 <InfoRow label="date_of_birth" value={personalInfo.dateOfBirth} icon={faCalendarAlt} />
235 <InfoRow
236 label="gender"
237 value={personalInfo.gender}
238 icon={personalInfo.gender === t('male') ? faMars : faVenus}
239 />
240 <InfoRow label="nationality" value={personalInfo.nationality} icon={faFlag} />
241 <InfoRow label="citizenship" value={personalInfo.citizenship} />
242 <InfoRow label="scholarship" value={personalInfo.scholarship} />
243 <InfoRow label="current_plan" value={personalInfo.currentPlan} />
244 <InfoRow label="registry_number" value={personalInfo.registryNumber} />
245 <InfoRow label="study_group" value={personalInfo.studyGroup} />
246 {personalInfo.notes && (
247 <div className="mt-4 p-4 bg-blue-50 rounded-lg">
248 <div className="text-sm font-medium text-blue-800 mb-1">{t('note')}:</div>
249 <div className="text-sm text-blue-700">{personalInfo.notes}</div>
250 </div>
251 )}
252 </Section>
253
254 {/* Birth Information */}
255 <Section title="birth_info" icon={faMapMarkerAlt}>
256 <InfoRow label="place_of_birth" value={birthInfo.placeOfBirth} />
257 <InfoRow label="municipality_of_birth" value={birthInfo.municipalityOfBirth} />
258 <InfoRow label="country" value={birthInfo.country} />
259 </Section>
260
261 {/* Contact Information */}
262 <div className="lg:col-span-2">
263 <Section title="contact" icon={faAddressCard}>
264 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
265 <div>
266 <InfoRow label="place_of_residence" value={contact.placeOfResidence} icon={faMapMarkerAlt} />
267 <InfoRow label="municipality_of_residence" value={contact.municipalityOfResidence} />
268 <InfoRow label="country" value={contact.country} />
269 <InfoRow label="address" value={contact.address} />
270 <InfoRow label="temporary_address" value={contact.temporaryAddress} />
271 </div>
272 <div>
273 <InfoRow label="phone" value={contact.phone} icon={faPhone} />
274 <InfoRow label="mobile_phone" value={contact.mobilePhone} icon={faPhone} />
275 <InfoRow label="passport_number" value={contact.passportNumber} icon={faPassport} />
276 <InfoRow label="passport_expiry_date" value={contact.passportExpiryDate} />
277 <InfoRow label="email" value={contact.email} icon={faEnvelope} />
278 <InfoRow label="microsoft_email" value={contact.microsoftEmail} icon={faEnvelope} />
279 </div>
280 </div>
281 </Section>
282 </div>
283 </div>
284 </div>
285 );
286}
Note: See TracBrowser for help on using the repository browser.