Changeset b67f274 for src/app


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/app
Files:
4 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
Note: See TracChangeset for help on using the changeset viewer.