source: src/app/students/semesters/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: 15.0 KB
Line 
1"use client"
2
3import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4import {
5 faCalendarAlt,
6 faCheck,
7 faTimes,
8 faFileAlt,
9 faMoneyBillWave,
10 faSignature,
11 faCheckCircle,
12 faTimesCircle
13} from '@fortawesome/free-solid-svg-icons';
14import { useEffect, useState } from 'react';
15import { getAccessToken } from '@/lib/auth';
16import { useTranslation } from 'react-i18next';
17
18type Semester = {
19 id: number | string;
20 semester: string;
21 direction: string;
22 quota: string;
23 note: string;
24 studentCom: string;
25 sum: string;
26 paid: string;
27 ukim: string;
28 createdOn: string;
29 dateChanged: string;
30 credits: string;
31 type: string;
32 doc: string;
33 doc1: string;
34 verified: string;
35 taxes: string;
36 signatures: string;
37 status: string;
38 completed: string;
39};
40
41type SemestersResponse = {
42 semesters: Semester[];
43};
44
45interface TableCellProps {
46 children: React.ReactNode;
47 className?: string;
48}
49
50const TableCell = ({ children, className = "" }: TableCellProps) => (
51 <td className={`px-4 py-3 text-sm border-b border-gray-100 ${className}`}>
52 {children}
53 </td>
54);
55
56const StatusBadge = ({ status }: { status: string }) => {
57 const { t } = useTranslation();
58 const baseClasses = "px-2 py-1 rounded-full text-xs font-medium";
59 if (status === t('valid')) {
60 return (
61 <span className={`${baseClasses} bg-green-100 text-green-800 flex items-center gap-1`}>
62 <FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" />
63 {t('valid')}
64 </span>
65 );
66 }
67 return (
68 <span className={`${baseClasses} bg-gray-100 text-gray-800`}>
69 {t(status) || status}
70 </span>
71 );
72};
73
74const YesNoBadge = ({ value }: { value: string }) => {
75 const { t } = useTranslation();
76 if (value === t('yes')) {
77 return (
78 <span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full">
79 <FontAwesomeIcon icon={faCheck} className="w-3 h-3" />
80 </span>
81 );
82 } else if (value === t('no')) {
83 return (
84 <span className="inline-flex items-center justify-center w-6 h-6 bg-red-100 text-red-600 rounded-full">
85 <FontAwesomeIcon icon={faTimes} className="w-3 h-3" />
86 </span>
87 );
88 }
89 return <span className="text-gray-400">—</span>;
90};
91
92const SignatureBadge = ({ signatures }: { signatures: string }) => {
93 const [completed, total] = signatures.split('/').map(Number);
94 const percentage = total > 0 ? (completed / total) * 100 : 0;
95
96 let colorClass = "text-red-600 bg-red-100";
97 if (percentage === 100) {
98 colorClass = "text-green-600 bg-green-100";
99 } else if (percentage >= 50) {
100 colorClass = "text-yellow-600 bg-yellow-100";
101 }
102
103 return (
104 <span className={`px-2 py-1 rounded-full text-xs font-medium flex items-center gap-1 ${colorClass}`}>
105 <FontAwesomeIcon icon={faSignature} className="w-3 h-3" />
106 {signatures}
107 </span>
108 );
109};
110
111export default function SemestersPage() {
112 const [semesters, setSemesters] = useState<Semester[]>([]);
113 const [isLoading, setIsLoading] = useState(true);
114 const [errorMessage, setErrorMessage] = useState<string | null>(null);
115 const { t } = useTranslation();
116
117 useEffect(() => {
118 let cancelled = false;
119
120 async function load() {
121 setIsLoading(true);
122 setErrorMessage(null);
123
124 const token = getAccessToken();
125 if (!token) {
126 setErrorMessage('Not authenticated. Please login again.');
127 setIsLoading(false);
128 return;
129 }
130
131 try {
132 const response = await fetch('https://iknow-api.onrender.com/api/user/getSemesters', {
133 method: 'GET',
134 headers: {
135 Authorization: `Bearer ${token}`,
136 },
137 });
138
139 if (!response.ok) {
140 const text = await response.text().catch(() => '');
141 throw new Error(text || `Failed to load semesters (${response.status})`);
142 }
143
144 const data = (await response.json()) as SemestersResponse;
145 if (!cancelled) setSemesters(Array.isArray(data?.semesters) ? data.semesters : []);
146 } catch (err) {
147 if (!cancelled) {
148 setErrorMessage(err instanceof Error ? err.message : 'Failed to load semesters.');
149 }
150 } finally {
151 if (!cancelled) setIsLoading(false);
152 }
153 }
154
155 void load();
156
157 return () => {
158 cancelled = true;
159 };
160 }, []);
161
162 if (isLoading) {
163 return (
164 <div className="min-h-screen pb-8">
165 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
166 {t('loading')}
167 </div>
168 </div>
169 );
170 }
171
172 if (errorMessage) {
173 return (
174 <div className="min-h-screen pb-8">
175 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
176 {errorMessage}
177 </div>
178 </div>
179 );
180 }
181
182 return (
183 <div className="min-h-screen pb-8">
184 {/* Header */}
185 <div className="bg-primary text-white rounded-xl p-8 mb-8">
186 <div className="flex items-center gap-4">
187 <div className="bg-white bg-opacity-20 rounded-full p-4">
188 <FontAwesomeIcon icon={faCalendarAlt} className="text-3xl text-primary" />
189 </div>
190 <div>
191 <h1 className="text-3xl font-bold mb-2">{t('semesters')}</h1>
192 <p className="text-lg opacity-90">
193 {t('semesters_overview')}
194 </p>
195 </div>
196 </div>
197 </div>
198
199 {/* Table Container */}
200 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
201 <div className="bg-primary text-white px-6 py-4">
202 <h2 className="text-xl font-bold">{t('semesters_list')}</h2>
203 </div>
204
205 <div className="overflow-x-auto">
206 <table className="w-full">
207 <thead className="bg-gray-50">
208 <tr>
209 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th>
210 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('semester')}</th>
211 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('direction')}</th>
212 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('quota')}</th>
213 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('note')}</th>
214 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('student_com')}</th>
215 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('sum')}</th>
216 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('paid')}</th>
217 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('ukim')}</th>
218 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('created_on')}</th>
219 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('date_changed')}</th>
220 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('credits')}</th>
221 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('type')}</th>
222 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('doc')}</th>
223 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('doc1')}</th>
224 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('verified')}</th>
225 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('taxes')}</th>
226 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('signatures')}</th>
227 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('status')}</th>
228 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('completed')}</th>
229 </tr>
230 </thead>
231 <tbody className="divide-y divide-gray-100">
232 {semesters.map((semester) => (
233 <tr key={semester.id} className="hover:bg-gray-50 transition-colors">
234 <TableCell className="font-medium text-gray-900">{semester.id}</TableCell>
235 <TableCell className="font-medium text-primary">{semester.semester}</TableCell>
236 <TableCell>{semester.direction}</TableCell>
237 <TableCell className="max-w-xs">
238 <div className="truncate" title={semester.quota}>
239 {semester.quota}
240 </div>
241 </TableCell>
242 <TableCell>
243 {semester.note ? (
244 <span className="text-yellow-600 font-medium" title={semester.note}>
245 {semester.note.length > 10 ? `${semester.note.substring(0, 10)}...` : semester.note}
246 </span>
247 ) : (
248 <span className="text-gray-400">—</span>
249 )}
250 </TableCell>
251 <TableCell>
252 {semester.studentCom ? (
253 <span className="text-blue-600 font-medium" title={semester.studentCom}>
254 {semester.studentCom.length > 10 ? `${semester.studentCom.substring(0, 10)}...` : semester.studentCom}
255 </span>
256 ) : (
257 <span className="text-gray-400">—</span>
258 )}
259 </TableCell>
260 <TableCell className="font-medium">
261 {semester.sum ? (
262 <span className="text-green-600 flex items-center gap-1">
263 <FontAwesomeIcon icon={faMoneyBillWave} className="w-3 h-3" />
264 {semester.sum}
265 </span>
266 ) : (
267 <span className="text-gray-400">—</span>
268 )}
269 </TableCell>
270 <TableCell className="font-medium">
271 {semester.paid ? (
272 <span className="text-green-600 flex items-center gap-1">
273 <FontAwesomeIcon icon={faMoneyBillWave} className="w-3 h-3" />
274 {semester.paid}
275 </span>
276 ) : (
277 <span className="text-gray-400">—</span>
278 )}
279 </TableCell>
280 <TableCell className="font-medium">
281 {semester.ukim ? (
282 <span className="text-blue-600">{semester.ukim}</span>
283 ) : (
284 <span className="text-gray-400">—</span>
285 )}
286 </TableCell>
287 <TableCell className="text-gray-600">{semester.createdOn}</TableCell>
288 <TableCell className="text-gray-600">{semester.dateChanged}</TableCell>
289 <TableCell className="font-medium text-primary">{semester.credits}</TableCell>
290 <TableCell>
291 <span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
292 {semester.type}
293 </span>
294 </TableCell>
295 <TableCell className="text-center">
296 <YesNoBadge value={semester.doc} />
297 </TableCell>
298 <TableCell className="text-center">
299 <YesNoBadge value={semester.doc1} />
300 </TableCell>
301 <TableCell className="text-center">
302 <YesNoBadge value={semester.verified} />
303 </TableCell>
304 <TableCell className="font-medium text-green-600">{semester.taxes}</TableCell>
305 <TableCell>
306 <SignatureBadge signatures={semester.signatures} />
307 </TableCell>
308 <TableCell>
309 <StatusBadge status={semester.status} />
310 </TableCell>
311 <TableCell>
312 {semester.completed !== "Не" ? (
313 <span className="text-green-600 font-medium flex items-center gap-1">
314 <FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" />
315 {semester.completed}
316 </span>
317 ) : (
318 <span className="text-red-600 font-medium flex items-center gap-1">
319 <FontAwesomeIcon icon={faTimesCircle} className="w-3 h-3" />
320 {semester.completed}
321 </span>
322 )}
323 </TableCell>
324 </tr>
325 ))}
326 </tbody>
327 </table>
328 </div>
329 </div>
330
331 {/* Summary Cards */}
332 <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
333 <div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
334 <div className="flex items-center gap-4">
335 <div className="bg-green-100 text-green-600 rounded-full p-3">
336 <FontAwesomeIcon icon={faCheckCircle} className="text-xl" />
337 </div>
338 <div>
339 <h3 className="text-lg font-bold text-gray-900">{t('completed_semesters')}</h3>
340 <p className="text-2xl font-bold text-green-600">
341 {semesters.filter(s => s.completed !== "Не").length}
342 </p>
343 </div>
344 </div>
345 </div>
346
347 <div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
348 <div className="flex items-center gap-4">
349 <div className="bg-blue-100 text-blue-600 rounded-full p-3">
350 <FontAwesomeIcon icon={faFileAlt} className="text-xl" />
351 </div>
352 <div>
353 <h3 className="text-lg font-bold text-gray-900">{t('verified_semesters')}</h3>
354 <p className="text-2xl font-bold text-blue-600">
355 {semesters.filter(s => s.verified === "Да").length}
356 </p>
357 </div>
358 </div>
359 </div>
360
361 <div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
362 <div className="flex items-center gap-4">
363 <div className="bg-primary text-white rounded-full p-3">
364 <FontAwesomeIcon icon={faCalendarAlt} className="text-xl" />
365 </div>
366 <div>
367 <h3 className="text-lg font-bold text-gray-900">{t('total_semesters')}</h3>
368 <p className="text-2xl font-bold text-primary">
369 {semesters.length}
370 </p>
371 </div>
372 </div>
373 </div>
374 </div>
375 </div>
376 );
377}
Note: See TracBrowser for help on using the repository browser.