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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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
Note: See TracChangeset for help on using the changeset viewer.