Changeset b67f274 for src


Ignore:
Timestamp:
12/21/25 18:28:25 (9 months ago)
Author:
Stefan-Saveski <stefansaveski19@…>
Branches:
master
Children:
90f7842
Parents:
82c9c53
Message:

Refactor Exams component to fetch passed subjects from API and remove hardcoded data; delete unused JSON data files; implement authentication token management in auth.ts

Location:
src
Files:
1 added
2 deleted
5 edited

Legend:

Unmodified
Added
Removed
  • src/app/page.tsx

    r82c9c53 rb67f274  
    1111} from '@fortawesome/free-solid-svg-icons';
    1212import { useState } from 'react';
     13import { login } from '@/lib/auth';
    1314
    1415export default function LoginPage() {
    1516  const [showPassword, setShowPassword] = useState(false);
     17  const [isSubmitting, setIsSubmitting] = useState(false);
     18  const [errorMessage, setErrorMessage] = useState<string | null>(null);
    1619  const [formData, setFormData] = useState({
    17     username: '',
     20    email: '',
    1821    password: ''
    1922  });
     
    2730  };
    2831
    29   const handleSubmit = (e: React.FormEvent) => {
     32  const handleSubmit = async (e: React.FormEvent) => {
    3033    e.preventDefault();
    31     // Handle login logic here
    32     console.log('Login attempt:', formData);
     34    setErrorMessage(null);
     35    setIsSubmitting(true);
     36    try {
     37      await login({ email: formData.email, password: formData.password });
     38      window.location.href = '/students/profile';
     39    } catch (err) {
     40      setErrorMessage(err instanceof Error ? err.message : 'Login failed.');
     41    } finally {
     42      setIsSubmitting(false);
     43    }
    3344  };
    3445
     
    5768            <div className="p-8">
    5869              <form onSubmit={handleSubmit} className="space-y-6">
    59                 {/* Username Field */}
     70                {/* Email Field */}
    6071                <div>
    61                   <label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-2">
    62                     Корисничко име
     72                  <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
     73                    Внесете е-пошта
    6374                  </label>
    6475                  <div className="relative">
     
    6778                    </div>
    6879                    <input
    69                       id="username"
    70                       name="username"
    71                       type="text"
     80                      id="email"
     81                      name="email"
     82                      type="email"
    7283                      required
    73                       value={formData.username}
     84                      value={formData.email}
    7485                      onChange={handleInputChange}
    7586                      className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors"
    76                       placeholder="Внесете корисничко име"
     87                      placeholder="Внесете е-пошта"
    7788                    />
    7889                  </div>
     
    132143                </div>
    133144
     145                {errorMessage && (
     146                  <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
     147                    {errorMessage}
     148                  </div>
     149                )}
     150
    134151                {/* Submit Button */}
    135152                <button
    136153                  type="submit"
    137                   className="w-full bg-primary hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl"
     154                  disabled={isSubmitting}
     155                  className="w-full bg-primary hover:bg-blue-700 disabled:opacity-60 disabled:hover:bg-primary text-white font-medium py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2 shadow-lg hover:shadow-xl"
    138156                >
    139157                  <FontAwesomeIcon icon={faSignInAlt} className="h-4 w-4" />
    140                   Најави се
     158                  {isSubmitting ? 'Најави се...' : 'Најави се'}
    141159                </button>
    142160              </form>
  • src/app/students/profile/page.tsx

    r82c9c53 rb67f274  
    1717  faPassport
    1818} from '@fortawesome/free-solid-svg-icons';
    19 import studentData from '@/data/student-profile.json';
    20 
     19import { useEffect, useState } from 'react';
     20import { getAccessToken } from '@/lib/auth';
    2121import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
     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
     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
     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};
    2289
    2390interface InfoRowProps {
     
    60127
    61128export default function ProfilePage() {
     129  const [studentData, setStudentData] = useState<StudentProfile | null>(null);
     130  const [isLoading, setIsLoading] = useState(true);
     131  const [errorMessage, setErrorMessage] = useState<string | null>(null);
     132
     133  useEffect(() => {
     134    let cancelled = false;
     135
     136    async function load() {
     137      setIsLoading(true);
     138      setErrorMessage(null);
     139
     140      const token = getAccessToken();
     141      if (!token) {
     142        setErrorMessage('Not authenticated. Please login again.');
     143        setIsLoading(false);
     144        return;
     145      }
     146
     147      try {
     148        const response = await fetch('http://localhost:5147/api/user/getUser', {
     149          method: 'GET',
     150          headers: {
     151            Authorization: `Bearer ${token}`,
     152          },
     153        });
     154
     155        if (!response.ok) {
     156          const text = await response.text().catch(() => '');
     157          throw new Error(text || `Failed to load profile (${response.status})`);
     158        }
     159
     160        const data = (await response.json()) as StudentProfile;
     161        if (!cancelled) setStudentData(data);
     162      } catch (err) {
     163        if (!cancelled) {
     164          setErrorMessage(err instanceof Error ? err.message : 'Failed to load profile.');
     165        }
     166      } finally {
     167        if (!cancelled) setIsLoading(false);
     168      }
     169    }
     170
     171    void load();
     172
     173    return () => {
     174      cancelled = true;
     175    };
     176  }, []);
     177
     178  if (isLoading) {
     179    return (
     180      <div className="min-h-screen pb-8">
     181        <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
     182          Loading...
     183        </div>
     184      </div>
     185    );
     186  }
     187
     188  if (errorMessage || !studentData) {
     189    return (
     190      <div className="min-h-screen pb-8">
     191        <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
     192          {errorMessage ?? 'Failed to load profile.'}
     193        </div>
     194      </div>
     195    );
     196  }
     197
    62198  const { personalInfo, birthInfo, previousEducation, enrollmentInfo, contact } = studentData;
    63199
  • src/app/students/semesters/page.tsx

    r82c9c53 rb67f274  
    1212  faTimesCircle
    1313} from '@fortawesome/free-solid-svg-icons';
    14 import semestersData from '@/data/semesters.json';
     14import { useEffect, useState } from 'react';
     15import { getAccessToken } from '@/lib/auth';
     16
     17type Semester = {
     18  id: number | string;
     19  semester: string;
     20  direction: string;
     21  quota: string;
     22  note: string;
     23  studentCom: string;
     24  sum: string;
     25  paid: string;
     26  ukim: string;
     27  createdOn: string;
     28  dateChanged: string;
     29  credits: string;
     30  type: string;
     31  doc: string;
     32  doc1: string;
     33  verified: string;
     34  taxes: string;
     35  signatures: string;
     36  status: string;
     37  completed: string;
     38};
     39
     40type SemestersResponse = {
     41  semesters: Semester[];
     42};
    1543
    1644interface TableCellProps {
     
    81109
    82110export default function SemestersPage() {
    83   const { semesters } = semestersData;
     111  const [semesters, setSemesters] = useState<Semester[]>([]);
     112  const [isLoading, setIsLoading] = useState(true);
     113  const [errorMessage, setErrorMessage] = useState<string | null>(null);
     114
     115  useEffect(() => {
     116    let cancelled = false;
     117
     118    async function load() {
     119      setIsLoading(true);
     120      setErrorMessage(null);
     121
     122      const token = getAccessToken();
     123      if (!token) {
     124        setErrorMessage('Not authenticated. Please login again.');
     125        setIsLoading(false);
     126        return;
     127      }
     128
     129      try {
     130        const response = await fetch('http://localhost:5147/api/user/getSemesters', {
     131          method: 'GET',
     132          headers: {
     133            Authorization: `Bearer ${token}`,
     134          },
     135        });
     136
     137        if (!response.ok) {
     138          const text = await response.text().catch(() => '');
     139          throw new Error(text || `Failed to load semesters (${response.status})`);
     140        }
     141
     142        const data = (await response.json()) as SemestersResponse;
     143        if (!cancelled) setSemesters(Array.isArray(data?.semesters) ? data.semesters : []);
     144      } catch (err) {
     145        if (!cancelled) {
     146          setErrorMessage(err instanceof Error ? err.message : 'Failed to load semesters.');
     147        }
     148      } finally {
     149        if (!cancelled) setIsLoading(false);
     150      }
     151    }
     152
     153    void load();
     154
     155    return () => {
     156      cancelled = true;
     157    };
     158  }, []);
     159
     160  if (isLoading) {
     161    return (
     162      <div className="min-h-screen pb-8">
     163        <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
     164          Loading...
     165        </div>
     166      </div>
     167    );
     168  }
     169
     170  if (errorMessage) {
     171    return (
     172      <div className="min-h-screen pb-8">
     173        <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
     174          {errorMessage}
     175        </div>
     176      </div>
     177    );
     178  }
    84179
    85180  return (
  • src/app/students/subjects/page.tsx

    r82c9c53 rb67f274  
    77  faFileInvoice
    88} from '@fortawesome/free-solid-svg-icons';
    9 import { useState } from 'react';
    10 import subjectsData from '@/data/subjects.json';
     9import { useEffect, useState } from 'react';
     10import { getAccessToken } from '@/lib/auth';
    1111
    1212interface Subject {
     
    2121  group: string;
    2222  professor: string;
     23}
     24
     25type SemesterInfo = {
     26  id: number;
     27  name: string;
     28  status: string;
     29  serviceNumber: string | number;
     30};
     31
     32type FinancialInfo = {
     33  sum: string | number;
     34  paid: string;
     35  due: string;
     36  materialCosts: string;
     37  credits: string;
     38  MKSA: string;
     39  electronicRegistration: string;
     40  eUKIM: string;
     41  bankProvision: string;
     42  total: string;
     43};
     44
     45type CurrentSemester = {
     46  id: string;
     47  name: string;
     48  status: string;
     49  serviceNumber: string | number;
     50  ticketNumber: string;
     51  debt: string;
     52  financialInfo: FinancialInfo;
     53};
     54
     55type SubjectsResponse = {
     56  currentSemester: CurrentSemester;
     57  semesters: SemesterInfo[];
     58  subjectsBySemester: Record<string, Subject[]>;
     59  semesterKeyById: Record<number, string>;
     60};
     61
     62type ApiCurrentSemester = {
     63  id: string;
     64  name: string;
     65  status: string;
     66  serviceNumber: string | number;
     67  ticketNumber: string;
     68  debt: string;
     69  financialInfo: {
     70    sum: string | number;
     71    paid: string;
     72    due: string;
     73    materialCosts: string;
     74    credits: string;
     75    totalCredits?: string;
     76    mksa?: string;
     77    MKSA?: string;
     78    electronicRegistration: string;
     79    eUKIM: string;
     80    bankProvision: string;
     81    total: string;
     82  };
     83};
     84
     85type ApiSubjectsResponse = {
     86  semesters: SemesterInfo[];
     87  currentSemester?: ApiCurrentSemester;
     88  currentSemestar?: ApiCurrentSemester;
     89  subjectsBySemester: Record<string, Subject[]>;
     90};
     91
     92function normalizeKey(input: string) {
     93  return input.toLowerCase().replace(/\s|\(|\)|\.|,/g, '');
     94}
     95
     96function detectSeasonFromName(name: string): 'summer' | 'winter' | null {
     97  const n = name.toLowerCase();
     98  if (n.includes('летен')) return 'summer';
     99  if (n.includes('зимски')) return 'winter';
     100  return null;
     101}
     102
     103function buildSemesterKeyById(semesters: SemesterInfo[], keys: string[]) {
     104  const keyBySeason: Partial<Record<'summer' | 'winter', string>> = {};
     105  for (const key of keys) {
     106    const k = key.toLowerCase();
     107    if (k.includes('summer')) keyBySeason.summer = key;
     108    if (k.includes('winter')) keyBySeason.winter = key;
     109  }
     110
     111  const mapping: Record<number, string> = {};
     112  for (const s of semesters) {
     113    const season = detectSeasonFromName(s.name);
     114    const mapped = season ? keyBySeason[season] : undefined;
     115    if (mapped) mapping[s.id] = mapped;
     116  }
     117
     118  // Fallback: if we couldn't infer seasons, try matching by normalized names.
     119  if (Object.keys(mapping).length === 0) {
     120    for (const s of semesters) {
     121      const ns = normalizeKey(s.name);
     122      const match = keys.find((k) => normalizeKey(k).includes(ns) || ns.includes(normalizeKey(k)));
     123      if (match) mapping[s.id] = match;
     124    }
     125  }
     126
     127  // Last resort: map in order.
     128  if (Object.keys(mapping).length === 0) {
     129    semesters.forEach((s, idx) => {
     130      if (keys[idx]) mapping[s.id] = keys[idx];
     131    });
     132  }
     133
     134  return mapping;
    23135}
    24136
     
    59171
    60172export default function SubjectsPage() {
    61   const [selectedSemester, setSelectedSemester] = useState(subjectsData.currentSemester.id);
     173  const [subjectsData, setSubjectsData] = useState<SubjectsResponse | null>(null);
     174  const [selectedSemester, setSelectedSemester] = useState<number | null>(null);
    62175  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
     176  const [isLoading, setIsLoading] = useState(true);
     177  const [errorMessage, setErrorMessage] = useState<string | null>(null);
     178
     179  useEffect(() => {
     180    let cancelled = false;
     181
     182    async function load() {
     183      setIsLoading(true);
     184      setErrorMessage(null);
     185
     186      const token = getAccessToken();
     187      if (!token) {
     188        setErrorMessage('Not authenticated. Please login again.');
     189        setIsLoading(false);
     190        return;
     191      }
     192
     193      try {
     194        const response = await fetch('http://localhost:5147/api/user/getSubjects', {
     195          method: 'GET',
     196          headers: {
     197            Authorization: `Bearer ${token}`,
     198          },
     199        });
     200
     201        if (!response.ok) {
     202          const text = await response.text().catch(() => '');
     203          throw new Error(text || `Failed to load subjects (${response.status})`);
     204        }
     205
     206        const apiData = (await response.json()) as ApiSubjectsResponse;
     207        if (cancelled) return;
     208
     209        const current = apiData.currentSemester ?? apiData.currentSemestar;
     210        if (!current) {
     211          throw new Error('Response missing currentSemestar/currentSemester');
     212        }
     213
     214        const keys = Object.keys(apiData.subjectsBySemester ?? {});
     215        const semesterKeyById = buildSemesterKeyById(apiData.semesters ?? [], keys);
     216
     217        const normalized: SubjectsResponse = {
     218          semesters: apiData.semesters ?? [],
     219          subjectsBySemester: apiData.subjectsBySemester ?? {},
     220          semesterKeyById,
     221          currentSemester: {
     222            id: current.id,
     223            name: current.name,
     224            status: current.status,
     225            serviceNumber: current.serviceNumber,
     226            ticketNumber: current.ticketNumber,
     227            debt: current.debt,
     228            financialInfo: {
     229              sum: current.financialInfo.sum,
     230              paid: current.financialInfo.paid,
     231              due: current.financialInfo.due,
     232              materialCosts: current.financialInfo.materialCosts,
     233              credits: current.financialInfo.credits,
     234              MKSA: current.financialInfo.MKSA ?? current.financialInfo.mksa ?? '',
     235              electronicRegistration: current.financialInfo.electronicRegistration,
     236              eUKIM: current.financialInfo.eUKIM,
     237              bankProvision: current.financialInfo.bankProvision,
     238              total: current.financialInfo.total,
     239            },
     240          },
     241        };
     242
     243        setSubjectsData(normalized);
     244
     245        setSelectedSemester((prev) => {
     246          if (prev !== null) return prev;
     247          const season = detectSeasonFromName(current.name);
     248          if (season) {
     249            const key = keys.find((k) => k.toLowerCase().includes(season));
     250            if (key) {
     251              const matchId = normalized.semesters.find((s) => normalized.semesterKeyById[s.id] === key)?.id;
     252              if (typeof matchId === 'number') return matchId;
     253            }
     254          }
     255          return normalized.semesters[0]?.id ?? null;
     256        });
     257      } catch (err) {
     258        if (!cancelled) {
     259          setErrorMessage(err instanceof Error ? err.message : 'Failed to load subjects.');
     260        }
     261      } finally {
     262        if (!cancelled) setIsLoading(false);
     263      }
     264    }
     265
     266    void load();
     267
     268    return () => {
     269      cancelled = true;
     270    };
     271  }, []);
     272
     273  if (isLoading) {
     274    return (
     275      <div className="min-h-screen pb-8">
     276        <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
     277          Loading...
     278        </div>
     279      </div>
     280    );
     281  }
     282
     283  if (errorMessage || !subjectsData || selectedSemester === null) {
     284    return (
     285      <div className="min-h-screen pb-8">
     286        <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
     287          {errorMessage ?? 'Failed to load subjects.'}
     288        </div>
     289      </div>
     290    );
     291  }
    63292 
    64   const currentSemesterData = subjectsData.semesters.find(s => s.id === selectedSemester) || subjectsData.currentSemester;
    65   const currentSubjects: Subject[] = subjectsData.subjectsBySemester[selectedSemester as keyof typeof subjectsData.subjectsBySemester] || [];
     293  const currentSemesterData =
     294    subjectsData.semesters.find((s) => s.id === selectedSemester) ?? subjectsData.semesters[0];
     295  const semesterKey =
     296    subjectsData.semesterKeyById[selectedSemester] ??
     297    Object.keys(subjectsData.subjectsBySemester)[0];
     298  const currentSubjects: Subject[] = semesterKey ? subjectsData.subjectsBySemester[semesterKey] || [] : [];
    66299  const { currentSemester } = subjectsData;
    67300
  • src/components/exams.tsx

    r82c9c53 rb67f274  
    11"use client"
    2 import { useState } from 'react';
     2import { useEffect, useMemo, useState } from 'react';
    33import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
    44import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons';
     5import { getAccessToken } from '@/lib/auth';
     6
     7type PassedSubject = {
     8  id: number;
     9  subjectId: number;
     10  code: string;
     11  subject: string;
     12  credits: number;
     13  grade: number;
     14  gradeText: string;
     15  date: string; // DD.MM.YYYY
     16  semester: string;
     17  professor: string;
     18};
     19
     20type PassedSubjectsResponse = {
     21  passedSubjects: PassedSubject[];
     22};
     23
     24type ExamRow = {
     25  id: number;
     26  code: string;
     27  subject: string;
     28  date: string;
     29  semester: string;
     30  credits: number;
     31  grade: number;
     32};
    533
    634const Exams = () => {
    735  const [sortField, setSortField] = useState<'grade' | 'date' | null>(null);
    836  const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null);
    9   // Real exam data
    10   const stats = {
    11     average: 8.95,
    12     credits: { current: 108, total: 240 },
    13     passed: 19,
    14     remaining: 22
    15   };
    16 
    17   const exams = [
    18     {
    19       id: 1,
    20       code: "F23L1W005",
    21       subject: "Бизнис и менаџмент",
    22       session: "2024 (Зимска) Прва испитна сесија",
    23       date: "21.02.2024",
    24       semester: 1,
    25       credits: 6.00,
    26       type: "З",
    27       grade: 7
    28     },
    29     {
    30       id: 2,
    31       code: "F23L1W007",
    32       subject: "Вовед во компјутерските науки",
    33       session: "2024 (Зимска) Прва испитна сесија",
    34       date: "06.02.2024",
    35       semester: 1,
    36       credits: 6.00,
    37       type: "З",
    38       grade: 10
    39     },
    40     {
    41       id: 3,
    42       code: "F23L2W003",
    43       subject: "Избрани теми од математика",
    44       session: "2024 (Зимска) Прва испитна сесија",
    45       date: "12.02.2024",
    46       semester: 1,
    47       credits: 6.00,
    48       type: "З",
    49       grade: 9
    50     },
    51     {
    52       id: 4,
    53       code: "F23L1W018",
    54       subject: "Професионални вештини",
    55       session: "2024 (Зимска) Прва испитна сесија",
    56       date: "01.02.2024",
    57       semester: 1,
    58       credits: 4.00,
    59       type: "З",
    60       grade: 9
    61     },
    62     {
    63       id: 5,
    64       code: "F23L1W004",
    65       subject: "Спорт и здравје",
    66       session: "2024 (Летна) Втора испитна сесија",
    67       date: "15.07.2024",
    68       semester: 1,
    69       credits: 2.00,
    70       type: "З",
    71       grade: "Реализиран"
    72     },
    73     {
    74       id: 6,
    75       code: "F23L1W020",
    76       subject: "Структурно програмирање",
    77       session: "2024 (Зимска) Прва испитна сесија",
    78       date: "12.02.2024",
    79       semester: 1,
    80       credits: 6.00,
    81       type: "З",
    82       grade: 10
    83     },
    84     {
    85       id: 7,
    86       code: "F23L1S003",
    87       subject: "Архитектура и организација на компјутери",
    88       session: "2024 (Летна) Втора испитна сесија",
    89       date: "03.07.2024",
    90       semester: 2,
    91       credits: 6.00,
    92       type: "З",
    93       grade: 9
    94     },
    95     {
    96       id: 8,
    97       code: "F23L1S023",
    98       subject: "Бизнис статистика",
    99       session: "2024 (Летна) Втора испитна сесија",
    100       date: "02.07.2024",
    101       semester: 2,
    102       credits: 6.00,
    103       type: "З",
    104       grade: 8
    105     },
    106     {
    107       id: 9,
    108       code: "F23L1S120",
    109       subject: "Креативни вештини за решавање проблеми",
    110       session: "2024 (Летна) Втора испитна сесија",
    111       date: "25.06.2024",
    112       semester: 2,
    113       credits: 6.00,
    114       type: "И",
    115       grade: 10
    116     },
    117     {
    118       id: 10,
    119       code: "F23L1S016",
    120       subject: "Објектно-ориентирано програмирање",
    121       session: "2024 (Летна) Втора испитна сесија",
    122       date: "13.07.2024",
    123       semester: 2,
    124       credits: 6.00,
    125       type: "З",
    126       grade: 10
    127     },
    128     {
    129       id: 11,
    130       code: "F23L1S146",
    131       subject: "Основи на Веб дизајн",
    132       session: "2024 (Летна) Втора испитна сесија",
    133       date: "24.06.2024",
    134       semester: 2,
    135       credits: 6.00,
    136       type: "З",
    137       grade: 10
    138     },
    139     {
    140       id: 12,
    141       code: "F23L2W100",
    142       subject: "Економија за ИКТ инженери",
    143       session: "2025 (Зимска) Прва испитна сесија",
    144       date: "03.02.2025",
    145       semester: 3,
    146       credits: 6.00,
    147       type: "З",
    148       grade: 8
    149     },
    150     {
    151       id: 13,
    152       code: "F23L2W109",
    153       subject: "Интернет програмирање на клиентска страна",
    154       session: "2025 (Зимска) Прва испитна сесија",
    155       date: "30.01.2025",
    156       semester: 3,
    157       credits: 6.00,
    158       type: "И",
    159       grade: 10
    160     },
    161     {
    162       id: 14,
    163       code: "F23L2W014",
    164       subject: "Компјутерски мрежи и безбедност",
    165       session: "2025 (Зимска) Прва испитна сесија",
    166       date: "10.02.2025",
    167       semester: 3,
    168       credits: 6.00,
    169       type: "З",
    170       grade: 7
    171     },
    172     {
    173       id: 15,
    174       code: "F23L2W201",
    175       subject: "Примена на алгоритми и податочни структури",
    176       session: "2025 (Зимска) Прва испитна сесија",
    177       date: "13.02.2025",
    178       semester: 3,
    179       credits: 6.00,
    180       type: "З",
    181       grade: 10
    182     },
    183     {
    184       id: 16,
    185       code: "F23L2W167",
    186       subject: "Шаблони за дизајн на кориснички интерфејси",
    187       session: "2025 (Зимска) Прва испитна сесија",
    188       date: "06.02.2025",
    189       semester: 3,
    190       credits: 6.00,
    191       type: "И",
    192       grade: 10
    193     },
    194     {
    195       id: 17,
    196       code: "F23L2S026",
    197       subject: "Маркетинг",
    198       session: "2025 (Летна) Втора испитна сесија",
    199       date: "12.06.2025",
    200       semester: 4,
    201       credits: 6.00,
    202       type: "З",
    203       grade: 8
    204     },
    205     {
    206       id: 18,
    207       code: "F23L2S017",
    208       subject: "Оперативни системи",
    209       session: "2025 (Летна) Втора испитна сесија",
    210       date: "19.06.2025",
    211       semester: 4,
    212       credits: 6.00,
    213       type: "З",
    214       grade: 7
    215     },
    216     {
    217       id: 19,
    218       code: "F23L2S029",
    219       subject: "Софтверско инженерство",
    220       session: "2025 (Летна) Втора испитна сесија",
    221       date: "23.06.2025",
    222       semester: 4,
    223       credits: 6.00,
    224       type: "З",
    225       grade: 8
     37  const [exams, setExams] = useState<ExamRow[]>([]);
     38  const [isLoading, setIsLoading] = useState(true);
     39  const [errorMessage, setErrorMessage] = useState<string | null>(null);
     40
     41  useEffect(() => {
     42    let cancelled = false;
     43
     44    async function load() {
     45      setIsLoading(true);
     46      setErrorMessage(null);
     47
     48      const token = getAccessToken();
     49      if (!token) {
     50        setErrorMessage('Not authenticated. Please login again.');
     51        setIsLoading(false);
     52        return;
     53      }
     54
     55      try {
     56        const response = await fetch('http://localhost:5147/api/user/getPassedSubjects', {
     57          method: 'GET',
     58          headers: {
     59            Authorization: `Bearer ${token}`,
     60          },
     61        });
     62
     63        if (!response.ok) {
     64          const text = await response.text().catch(() => '');
     65          throw new Error(text || `Failed to load passed subjects (${response.status})`);
     66        }
     67
     68        const data = (await response.json()) as PassedSubjectsResponse;
     69        const rows: ExamRow[] = (data?.passedSubjects ?? []).map((s) => ({
     70          id: s.id,
     71          code: s.code,
     72          subject: s.subject,
     73          date: s.date,
     74          semester: s.semester,
     75          credits: s.credits,
     76          grade: s.grade,
     77        }));
     78
     79        if (!cancelled) setExams(rows);
     80      } catch (err) {
     81        if (!cancelled) {
     82          setErrorMessage(err instanceof Error ? err.message : 'Failed to load passed subjects.');
     83        }
     84      } finally {
     85        if (!cancelled) setIsLoading(false);
     86      }
    22687    }
    227   ];
    228 
    229   const getGradeColor = (grade: number | string) => {
    230     if (grade === "Реализиран") return "bg-blue-100 text-blue-800 border-blue-200";
    231     if (typeof grade === "number") {
    232       if (grade >= 9) return "bg-green-100 text-green-800 border-green-200";
    233       if (grade >= 8) return "bg-yellow-100 text-yellow-800 border-yellow-200";
    234       if (grade >= 6) return "bg-orange-100 text-orange-800 border-orange-200";
    235       return "bg-red-100 text-red-800 border-red-200";
    236     }
    237     return "bg-gray-100 text-gray-800 border-gray-200";
     88
     89    void load();
     90
     91    return () => {
     92      cancelled = true;
     93    };
     94  }, []);
     95
     96  const stats = useMemo(() => {
     97    const passed = exams.length;
     98    const creditsCurrent = exams.reduce((sum, s) => sum + (typeof s.credits === 'number' ? s.credits : 0), 0);
     99    const numericGrades = exams.map((e) => (typeof e.grade === 'number' ? e.grade : null)).filter((g): g is number => g !== null);
     100    const average =
     101      numericGrades.length > 0
     102        ? numericGrades.reduce((sum, g) => sum + g, 0) / numericGrades.length
     103        : 0;
     104
     105    const totalCredits = 240;
     106    const totalExams = 40;
     107    return {
     108      average: Number.isFinite(average) ? Number(average.toFixed(2)) : 0,
     109      credits: { current: creditsCurrent, total: totalCredits },
     110      passed,
     111      remaining: Math.max(0, totalExams - passed),
     112    };
     113  }, [exams]);
     114
     115  const getGradeColor = (grade: number) => {
     116    if (grade >= 9) return "bg-green-100 text-green-800 border-green-200";
     117    if (grade >= 8) return "bg-yellow-100 text-yellow-800 border-yellow-200";
     118    if (grade >= 6) return "bg-orange-100 text-orange-800 border-orange-200";
     119    return "bg-red-100 text-red-800 border-red-200";
    238120  };
    239121
     
    275157    return [...exams].sort((a, b) => {
    276158      if (sortField === 'grade') {
    277         // Handle "Реализиран" grade separately
    278         if (a.grade === "Реализиран" && b.grade === "Реализиран") return 0;
    279         if (a.grade === "Реализиран") return sortOrder === 'asc' ? -1 : 1;
    280         if (b.grade === "Реализиран") return sortOrder === 'asc' ? 1 : -1;
    281        
    282         // Handle numeric grades
    283         const gradeA = typeof a.grade === 'number' ? a.grade : 0;
    284         const gradeB = typeof b.grade === 'number' ? b.grade : 0;
    285        
    286         return sortOrder === 'asc' ? gradeA - gradeB : gradeB - gradeA;
     159        return sortOrder === 'asc' ? a.grade - b.grade : b.grade - a.grade;
    287160      }
    288161     
     
    307180  };
    308181
    309   const creditsPercentage = (stats.credits.current / stats.credits.total) * 100;
     182  const creditsPercentage = stats.credits.total > 0 ? (stats.credits.current / stats.credits.total) * 100 : 0;
     183
     184  if (isLoading) {
     185    return (
     186      <div className="bg-white rounded-lg shadow-sm p-6">
     187        Loading...
     188      </div>
     189    );
     190  }
     191
     192  if (errorMessage) {
     193    return (
     194      <div className="bg-white rounded-lg shadow-sm p-6">
     195        <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
     196          {errorMessage}
     197        </div>
     198      </div>
     199    );
     200  }
    310201
    311202  return (
Note: See TracChangeset for help on using the changeset viewer.