source: src/app/professor/profile/page.tsx@ 298f9b3

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

feat: Implement professor portal with student management, grade handling, and profile display

  • Property mode set to 100644
File size: 9.3 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";
20
21type PersonalInfo = {
22 firstName: string;
23 middleName: string;
24 lastName: string;
25 maidenName: string;
26 dateOfBirth: string;
27 gender: string;
28 nationality: string;
29 citizenship: string;
30 scholarship: string;
31 currentPlan: string;
32 registryNumber: string;
33 studyGroup: string;
34 notes?: string;
35 index: string;
36 embg: string;
37};
38
39type BirthInfo = {
40 placeOfBirth: string;
41 municipalityOfBirth: string;
42 country: string;
43};
44
45// Present in the API response, but intentionally not shown on professor profile.
46type PreviousEducation = {
47 type: string;
48 profession: string;
49 average: string | number;
50 language: string;
51 country: string;
52 previousUniversity: string;
53 previousFaculty: string;
54 previousStudyMode: string;
55};
56
57// Present in the API response, but intentionally not shown on professor profile.
58type 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
68type 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
82type StudentProfile = {
83 personalInfo: PersonalInfo;
84 birthInfo: BirthInfo;
85 previousEducation: PreviousEducation;
86 enrollmentInfo: EnrollmentInfo;
87 contact: Contact;
88};
89
90interface InfoRowProps {
91 label: string;
92 value: string | number;
93 icon?: IconDefinition;
94}
95
96const InfoRow = ({ label, value, icon }: InfoRowProps) => (
97 <div className="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0">
98 <div className="flex items-center gap-2 text-gray-600 font-medium">
99 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />}
100 <span>{label}:</span>
101 </div>
102 <div className="text-gray-900 font-semibold text-right max-w-xs break-words">
103 {value || "N/A"}
104 </div>
105 </div>
106);
107
108interface SectionProps {
109 title: string;
110 icon: IconDefinition;
111 children: React.ReactNode;
112}
113
114const Section = ({ title, icon, children }: SectionProps) => (
115 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
116 <div className="bg-primary text-white px-6 py-4">
117 <div className="flex items-center gap-3">
118 <FontAwesomeIcon icon={icon} className="text-xl" />
119 <h2 className="text-xl font-bold">{title}</h2>
120 </div>
121 </div>
122 <div className="p-6">{children}</div>
123 </div>
124);
125
126export default function ProfessorProfilePage() {
127 const [profileData, setProfileData] = useState<StudentProfile | null>(null);
128 const [isLoading, setIsLoading] = useState(true);
129 const [errorMessage, setErrorMessage] = useState<string | null>(null);
130
131 useEffect(() => {
132 let cancelled = false;
133
134 async function load() {
135 setIsLoading(true);
136 setErrorMessage(null);
137
138 const token = getAccessToken();
139 if (!token) {
140 setErrorMessage("Not authenticated. Please login again.");
141 setIsLoading(false);
142 return;
143 }
144
145 try {
146 const response = await fetch("https://iknow-api.onrender.com/api/user/getUser", {
147 method: "GET",
148 headers: {
149 Authorization: `Bearer ${token}`,
150 },
151 });
152
153 if (!response.ok) {
154 const text = await response.text().catch(() => "");
155 throw new Error(text || `Failed to load profile (${response.status})`);
156 }
157
158 const data = (await response.json()) as StudentProfile;
159 if (!cancelled) setProfileData(data);
160 } catch (err) {
161 if (!cancelled) {
162 setErrorMessage(err instanceof Error ? err.message : "Failed to load profile.");
163 }
164 } finally {
165 if (!cancelled) setIsLoading(false);
166 }
167 }
168
169 void load();
170
171 return () => {
172 cancelled = true;
173 };
174 }, []);
175
176 if (isLoading) {
177 return (
178 <div className="min-h-screen pb-8">
179 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">Loading...</div>
180 </div>
181 );
182 }
183
184 if (errorMessage || !profileData) {
185 return (
186 <div className="min-h-screen pb-8">
187 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
188 {errorMessage ?? "Failed to load profile."}
189 </div>
190 </div>
191 );
192 }
193
194 const { personalInfo, birthInfo, contact } = profileData;
195
196 return (
197 <div className="min-h-screen pb-8">
198 {/* Header */}
199 <div className="bg-primary text-white rounded-xl p-8 mb-8">
200 <div className="flex items-center gap-6">
201 <div className="relative">
202 <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">
203 <FontAwesomeIcon icon={faUser} className="text-3xl text-primary" />
204 </div>
205 <div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
206 </div>
207 <div>
208 <h1 className="text-3xl font-bold mb-2">
209 {personalInfo.firstName} {personalInfo.middleName} {personalInfo.lastName}
210 </h1>
211 <div className="text-lg opacity-90">
212 Индекс: {personalInfo.index} | ЕМБГ: {personalInfo.embg}
213 </div>
214 </div>
215 </div>
216 </div>
217
218 {/* Profile Sections */}
219 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
220 {/* Personal Information */}
221 <Section title="Лични податоци" icon={faIdCard}>
222 <InfoRow label="Име" value={personalInfo.firstName} />
223 <InfoRow label="Средно име" value={personalInfo.middleName} />
224 <InfoRow label="Презиме" value={personalInfo.lastName} />
225 <InfoRow label="Моминско презиме" value={personalInfo.maidenName} />
226 <InfoRow label="Датум на раѓање" value={personalInfo.dateOfBirth} icon={faCalendarAlt} />
227 <InfoRow
228 label="Пол"
229 value={personalInfo.gender}
230 icon={personalInfo.gender === "машки" ? faMars : faVenus}
231 />
232 <InfoRow label="Националност" value={personalInfo.nationality} icon={faFlag} />
233 <InfoRow label="Државјанство" value={personalInfo.citizenship} />
234 <InfoRow label="Стипендија" value={personalInfo.scholarship} />
235 <InfoRow label="Тековен план" value={personalInfo.currentPlan} />
236 <InfoRow label="Бр. во матична книга" value={personalInfo.registryNumber} />
237 <InfoRow label="Група на студирање" value={personalInfo.studyGroup} />
238 {personalInfo.notes && (
239 <div className="mt-4 p-4 bg-blue-50 rounded-lg">
240 <div className="text-sm font-medium text-blue-800 mb-1">Забелешка:</div>
241 <div className="text-sm text-blue-700">{personalInfo.notes}</div>
242 </div>
243 )}
244 </Section>
245
246 {/* Birth Information */}
247 <Section title="Податоци за раѓање" icon={faMapMarkerAlt}>
248 <InfoRow label="Место на раѓање" value={birthInfo.placeOfBirth} />
249 <InfoRow label="Општина на раѓање" value={birthInfo.municipalityOfBirth} />
250 <InfoRow label="Земја" value={birthInfo.country} />
251 </Section>
252
253 {/* Contact Information */}
254 <div className="lg:col-span-2">
255 <Section title="Контакт" icon={faAddressCard}>
256 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
257 <div>
258 <InfoRow label="Место на живеење" value={contact.placeOfResidence} icon={faMapMarkerAlt} />
259 <InfoRow label="Општина на живеење" value={contact.municipalityOfResidence} />
260 <InfoRow label="Држава" value={contact.country} />
261 <InfoRow label="Адреса" value={contact.address} />
262 <InfoRow label="Адреса на престој" value={contact.temporaryAddress} />
263 </div>
264 <div>
265 <InfoRow label="Телефон" value={contact.phone} icon={faPhone} />
266 <InfoRow label="Моб. телефон" value={contact.mobilePhone} icon={faPhone} />
267 <InfoRow label="Број на пасош" value={contact.passportNumber} icon={faPassport} />
268 <InfoRow label="Датум на истекување на пасошот" value={contact.passportExpiryDate} />
269 <InfoRow label="Е-пошта" value={contact.email} icon={faEnvelope} />
270 <InfoRow label="Microsoft email" value={contact.microsoftEmail} icon={faEnvelope} />
271 </div>
272 </div>
273 </Section>
274 </div>
275 </div>
276 </div>
277 );
278}
Note: See TracBrowser for help on using the repository browser.