- Timestamp:
- 01/30/26 01:40:27 (8 months ago)
- Branches:
- master
- Children:
- ba52069
- Parents:
- 298f9b3
- Location:
- src
- Files:
-
- 5 added
- 13 edited
-
app/[locale]/layout.tsx (added)
-
app/layout.tsx (modified) (3 diffs)
-
app/page.tsx (modified) (13 diffs)
-
app/professor/profile/page.tsx (modified) (9 diffs)
-
app/professor/students/page.tsx (modified) (13 diffs)
-
app/students/applications/page.tsx (modified) (16 diffs)
-
app/students/documents/page.tsx (modified) (26 diffs)
-
app/students/profile/page.tsx (modified) (9 diffs)
-
app/students/semesters/page.tsx (modified) (12 diffs)
-
app/students/subjects/page.tsx (modified) (17 diffs)
-
components/LanguageSwitcher.tsx (added)
-
components/exams.tsx (modified) (11 diffs)
-
components/header.tsx (modified) (3 diffs)
-
components/navbar.tsx (modified) (2 diffs)
-
components/professor-navbar.tsx (modified) (2 diffs)
-
i18n.ts (added)
-
locales/en/translation.json (added)
-
locales/mk/translation.json (added)
Legend:
- Unmodified
- Added
- Removed
-
src/app/layout.tsx
r298f9b3 r50f2fb4 1 import type { Metadata } from "next";1 "use client"; 2 2 import { Geist, Geist_Mono } from "next/font/google"; 3 3 import "./globals.css"; 4 4 import '@fortawesome/fontawesome-svg-core/styles.css'; 5 5 import { config } from '@fortawesome/fontawesome-svg-core'; 6 import { I18nextProvider } from 'react-i18next'; 7 import i18n from '../i18n'; 6 8 7 // Prevent FontAwesome from adding CSS automatically8 9 config.autoAddCss = false; 9 10 … … 18 19 }); 19 20 20 export const metadata: Metadata = {21 title: "IKnow - Student Portal",22 description: "Student portal for Ss. Cyril and Methodius University",23 };24 25 21 export default function RootLayout({ 26 22 children, … … 30 26 return ( 31 27 <html lang="en"> 32 <body 33 className={`${geistSans.variable} ${geistMono.variable} antialiased`}34 >35 < div className="mx-auto max-w-6xl px-4">{children}</div>28 <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> 29 <I18nextProvider i18n={i18n}> 30 <div className="mx-auto max-w-6xl px-4">{children}</div> 31 </I18nextProvider> 36 32 </body> 37 33 </html> -
src/app/page.tsx
r298f9b3 r50f2fb4 12 12 import { useState } from 'react'; 13 13 import { login } from '@/lib/auth'; 14 import { useTranslation } from 'react-i18next'; 15 import LanguageSwitcher from '@/components/LanguageSwitcher'; 14 16 15 17 export default function LoginPage() { 18 const { t } = useTranslation(); 16 19 const [showPassword, setShowPassword] = useState(false); 17 20 const [isSubmitting, setIsSubmitting] = useState(false); … … 54 57 <div className="w-full flex flex-col gap-4"> 55 58 <div className="bg-yellow-100 border border-yellow-200 rounded-xl shadow-sm p-5"> 56 <div className="text-sm font-bold text-gray-900 mb-2"> Login for Student</div>59 <div className="text-sm font-bold text-gray-900 mb-2">{t('login_student_title')}</div> 57 60 <div className="text-xs text-gray-800 whitespace-pre-wrap font-mono"> 58 {`"email":"boris@example.com", 59 "password":"your-password-here",`} 61 {t('login_student_example')} 60 62 </div> 61 63 <div className="mt-3 text-xs text-gray-700"> 62 may take up to 30 seconds afte long inactivity64 {t('login_note_slow')} 63 65 </div> 64 66 </div> 65 67 66 68 <div className="bg-yellow-100 border border-yellow-200 rounded-xl shadow-sm p-5"> 67 <div className="text-sm font-bold text-gray-900 mb-2"> Login for Professor</div>69 <div className="text-sm font-bold text-gray-900 mb-2">{t('login_professor_title')}</div> 68 70 <div className="text-xs text-gray-800 whitespace-pre-wrap font-mono"> 69 {`"email":"andonov@gmail.com", 70 "password":"123lol456",`} 71 {t('login_professor_example')} 71 72 </div> 72 73 </div> … … 84 85 </div> 85 86 </div> 86 <h1 className="text-2xl font-bold mb-2"> Добредојде</h1>87 <h1 className="text-2xl font-bold mb-2">{t('login_welcome')}</h1> 87 88 <p className="text-blue-100 text-sm"> 88 Најавете се во вашиот IKnow акаунт89 {t('login_subtitle')} 89 90 </p> 90 91 </div> … … 92 93 {/* Form */} 93 94 <div className="p-8"> 95 {/* Language Switcher above email field, centered */} 96 <div className="flex justify-center mb-6"> 97 <LanguageSwitcher /> 98 </div> 94 99 <form onSubmit={handleSubmit} className="space-y-6"> 95 100 {/* Email Field */} 96 101 <div> 97 102 <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> 98 Внесете е-пошта103 {t('login_email_label')} 99 104 </label> 100 105 <div className="relative"> … … 110 115 onChange={handleInputChange} 111 116 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" 112 placeholder= "Внесете е-пошта"117 placeholder={t('login_email_placeholder')} 113 118 /> 114 119 </div> … … 118 123 <div> 119 124 <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2"> 120 Лозинка125 {t('login_password_label')} 121 126 </label> 122 127 <div className="relative"> … … 132 137 onChange={handleInputChange} 133 138 className="w-full pl-10 pr-12 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors" 134 placeholder= "Внесете лозинка"139 placeholder={t('login_password_placeholder')} 135 140 /> 136 141 <button … … 157 162 /> 158 163 <label htmlFor="remember" className="ml-2 block text-sm text-gray-700"> 159 Запомни ме164 {t('login_remember_me')} 160 165 </label> 161 166 </div> … … 164 169 className="text-sm text-primary hover:text-blue-700 font-medium transition-colors" 165 170 > 166 Заборавена лозинка?171 {t('login_forgot_password')} 167 172 </button> 168 173 </div> … … 170 175 {errorMessage && ( 171 176 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 172 { errorMessage}177 {t(errorMessage) || errorMessage} 173 178 </div> 174 179 )} … … 181 186 > 182 187 <FontAwesomeIcon icon={faSignInAlt} className="h-4 w-4" /> 183 {isSubmitting ? 'Најави се...' : 'Најави се'}188 {isSubmitting ? t('login_button_loading') : t('login_button')} 184 189 </button> 185 190 </form> … … 189 194 <div className="bg-gray-50 px-8 py-4 border-t border-gray-100"> 190 195 <p className="text-center text-sm text-gray-600"> 191 Немате акаунт?{' '}196 {t('login_no_account')}{' '} 192 197 <button className="text-primary hover:text-blue-700 font-medium transition-colors"> 193 Контактирајте ја администрацијата198 {t('login_contact_admin')} 194 199 </button> 195 200 </p> … … 202 207 <FontAwesomeIcon icon={faGraduationCap} className="text-primary" /> 203 208 <span className="text-sm text-gray-600"> 204 Студентски информационен систем209 {t('login_footer')} 205 210 </span> 206 211 </div> -
src/app/professor/profile/page.tsx
r298f9b3 r50f2fb4 18 18 import { getAccessToken } from "@/lib/auth"; 19 19 import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; 20 import { useTranslation } from 'react-i18next'; 20 21 21 22 type PersonalInfo = { … … 94 95 } 95 96 96 const 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> 97 const InfoRow = ({ label, value, icon }: InfoRowProps) => { 98 const { t } = useTranslation(); 99 return ( 100 <div className="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0"> 101 <div className="flex items-center gap-2 text-gray-600 font-medium"> 102 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />} 103 <span>{t(label)}:</span> 104 </div> 105 <div className="text-gray-900 font-semibold text-right max-w-xs break-words"> 106 {value || t('n_a')} 107 </div> 101 108 </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 ); 109 ); 110 }; 107 111 108 112 interface SectionProps { … … 112 116 } 113 117 114 const 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> 118 const Section = ({ title, icon, children }: SectionProps) => { 119 const { t } = useTranslation(); 120 return ( 121 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 122 <div className="bg-primary text-white px-6 py-4"> 123 <div className="flex items-center gap-3"> 124 <FontAwesomeIcon icon={icon} className="text-xl" /> 125 <h2 className="text-xl font-bold">{t(title)}</h2> 126 </div> 127 </div> 128 <div className="p-6">{children}</div> 121 129 </div> 122 <div className="p-6">{children}</div> 123 </div> 124 ); 130 ); 131 }; 125 132 126 133 export default function ProfessorProfilePage() { … … 128 135 const [isLoading, setIsLoading] = useState(true); 129 136 const [errorMessage, setErrorMessage] = useState<string | null>(null); 137 const { t } = useTranslation(); 130 138 131 139 useEffect(() => { … … 177 185 return ( 178 186 <div className="min-h-screen pb-8"> 179 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> Loading...</div>187 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">{t('loading')}</div> 180 188 </div> 181 189 ); … … 186 194 <div className="min-h-screen pb-8"> 187 195 <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."}196 {errorMessage ?? t('failed_to_load_profile')} 189 197 </div> 190 198 </div> … … 210 218 </h1> 211 219 <div className="text-lg opacity-90"> 212 Индекс: {personalInfo.index} | ЕМБГ: {personalInfo.embg}220 {t('index')}: {personalInfo.index} | {t('embg')}: {personalInfo.embg} 213 221 </div> 214 222 </div> … … 219 227 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> 220 228 {/* 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} />229 <Section title="personal_info" icon={faIdCard}> 230 <InfoRow label="first_name" value={personalInfo.firstName} /> 231 <InfoRow label="middle_name" value={personalInfo.middleName} /> 232 <InfoRow label="last_name" value={personalInfo.lastName} /> 233 <InfoRow label="maiden_name" value={personalInfo.maidenName} /> 234 <InfoRow label="date_of_birth" value={personalInfo.dateOfBirth} icon={faCalendarAlt} /> 227 235 <InfoRow 228 label=" Пол"236 label="gender" 229 237 value={personalInfo.gender} 230 icon={personalInfo.gender === "машки"? faMars : faVenus}238 icon={personalInfo.gender === t('male') ? faMars : faVenus} 231 239 /> 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} />240 <InfoRow label="nationality" value={personalInfo.nationality} icon={faFlag} /> 241 <InfoRow label="citizenship" value={personalInfo.citizenship} /> 242 <InfoRow label="scholarship" value={personalInfo.scholarship} /> 243 <InfoRow label="current_plan" value={personalInfo.currentPlan} /> 244 <InfoRow label="registry_number" value={personalInfo.registryNumber} /> 245 <InfoRow label="study_group" value={personalInfo.studyGroup} /> 238 246 {personalInfo.notes && ( 239 247 <div className="mt-4 p-4 bg-blue-50 rounded-lg"> 240 <div className="text-sm font-medium text-blue-800 mb-1"> Забелешка:</div>248 <div className="text-sm font-medium text-blue-800 mb-1">{t('note')}:</div> 241 249 <div className="text-sm text-blue-700">{personalInfo.notes}</div> 242 250 </div> … … 245 253 246 254 {/* 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} />255 <Section title="birth_info" icon={faMapMarkerAlt}> 256 <InfoRow label="place_of_birth" value={birthInfo.placeOfBirth} /> 257 <InfoRow label="municipality_of_birth" value={birthInfo.municipalityOfBirth} /> 258 <InfoRow label="country" value={birthInfo.country} /> 251 259 </Section> 252 260 253 261 {/* Contact Information */} 254 262 <div className="lg:col-span-2"> 255 <Section title=" Контакт" icon={faAddressCard}>263 <Section title="contact" icon={faAddressCard}> 256 264 <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> 257 265 <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} />266 <InfoRow label="place_of_residence" value={contact.placeOfResidence} icon={faMapMarkerAlt} /> 267 <InfoRow label="municipality_of_residence" value={contact.municipalityOfResidence} /> 268 <InfoRow label="country" value={contact.country} /> 269 <InfoRow label="address" value={contact.address} /> 270 <InfoRow label="temporary_address" value={contact.temporaryAddress} /> 263 271 </div> 264 272 <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=" Microsoftemail" value={contact.microsoftEmail} icon={faEnvelope} />273 <InfoRow label="phone" value={contact.phone} icon={faPhone} /> 274 <InfoRow label="mobile_phone" value={contact.mobilePhone} icon={faPhone} /> 275 <InfoRow label="passport_number" value={contact.passportNumber} icon={faPassport} /> 276 <InfoRow label="passport_expiry_date" value={contact.passportExpiryDate} /> 277 <InfoRow label="email" value={contact.email} icon={faEnvelope} /> 278 <InfoRow label="microsoft_email" value={contact.microsoftEmail} icon={faEnvelope} /> 271 279 </div> 272 280 </div> -
src/app/professor/students/page.tsx
r298f9b3 r50f2fb4 2 2 3 3 import { useEffect, useMemo, useState } from "react"; 4 import { useTranslation } from 'react-i18next'; 4 5 5 6 type UsersBySubject = { … … 36 37 37 38 export default function ProfessorStudentsPage() { 39 const { t } = useTranslation(); 38 40 const [subjects, setSubjects] = useState<SubjectsAndUsers[]>([]); 39 41 const [loading, setLoading] = useState(true); … … 129 131 <div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> 130 132 <div className="flex flex-col gap-4 md:flex-row md:items-end md:justify-between"> 133 131 134 <div> 132 <h1 className="text-2xl font-semibold text-gray-900"> Professor • Students</h1>135 <h1 className="text-2xl font-semibold text-gray-900">{t('prof_students_title')}</h1> 133 136 <p className="text-gray-700 mt-1"> 134 Data comes from<span className="font-mono">/api/prof/students</span> (demo store).137 {t('prof_students_data_note')} <span className="font-mono">/api/prof/students</span> (demo store). 135 138 </p> 136 139 </div> … … 138 141 <div className="flex flex-col sm:flex-row gap-3"> 139 142 <div className="flex flex-col"> 140 <label className="text-sm text-gray-700"> Filter by subject</label>143 <label className="text-sm text-gray-700">{t('filter_by_subject')}</label> 141 144 <select 142 145 className="border border-gray-300 rounded-lg px-3 py-2" … … 144 147 onChange={(e) => setSubjectFilter(e.target.value)} 145 148 > 146 <option value="all"> All subjects</option>149 <option value="all">{t('all_subjects')}</option> 147 150 {subjects 148 151 .filter((s) => typeof s.Id === "number") 149 152 .map((s) => ( 150 153 <option key={String(s.Id)} value={String(s.Id)}> 151 {s.Name ?? ` Subject${s.Id}`}154 {s.Name ?? `${t('subject')} ${s.Id}`} 152 155 </option> 153 156 ))} … … 156 159 157 160 <div className="flex flex-col"> 158 <label className="text-sm text-gray-700"> Find student by index / ID</label>161 <label className="text-sm text-gray-700">{t('find_student_by_id')}</label> 159 162 <input 160 163 className="border border-gray-300 rounded-lg px-3 py-2" 161 placeholder= "e.g. 20001"164 placeholder={t('student_id_placeholder')} 162 165 value={studentIdQuery} 163 166 onChange={(e) => setStudentIdQuery(e.target.value)} … … 177 180 <thead className="bg-gray-50"> 178 181 <tr> 179 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b"> Student</th>180 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b"> ID</th>181 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b"> Subject</th>182 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b"> Grade</th>183 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b"> Actions</th>182 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b">{t('student')}</th> 183 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b">{t('id')}</th> 184 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b">{t('subject')}</th> 185 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b">{t('grade')}</th> 186 <th className="text-left text-sm font-semibold text-gray-700 px-4 py-3 border-b">{t('actions')}</th> 184 187 </tr> 185 188 </thead> … … 188 191 <tr> 189 192 <td className="px-4 py-4 text-gray-700" colSpan={5}> 190 Loading...193 {t('loading')} 191 194 </td> 192 195 </tr> … … 194 197 <tr> 195 198 <td className="px-4 py-4 text-gray-700" colSpan={5}> 196 No students found.199 {t('no_students_found')} 197 200 </td> 198 201 </tr> … … 227 230 </select> 228 231 <span className="text-sm text-gray-600"> 229 Current: {r.grade > 0 ? r.grade : "—"}232 {t('current')}: {r.grade > 0 ? r.grade : t('none')} 230 233 </span> 231 234 </div> … … 245 248 }} 246 249 > 247 Add grade250 {t('add_grade')} 248 251 </button> 249 252 … … 260 263 }} 261 264 > 262 Edit grade265 {t('edit_grade')} 263 266 </button> 264 267 … … 275 278 }} 276 279 > 277 Remove grade280 {t('remove_grade')} 278 281 </button> 279 282 </div> -
src/app/students/applications/page.tsx
r298f9b3 r50f2fb4 14 14 } from '@fortawesome/free-solid-svg-icons'; 15 15 import { useState } from 'react'; 16 import { useTranslation } from 'react-i18next'; 16 17 import applicationsData from '@/data/applications.json'; 17 18 … … 28 29 29 30 const CompletedBadge = ({ completed }: { completed: string }) => { 30 if (completed === "Да") { 31 return ( 32 <span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full"> 31 const { t } = useTranslation(); 32 if (completed === t('yes', 'Да')) { 33 return ( 34 <span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full" title={t('completed')}> 33 35 <FontAwesomeIcon icon={faCheckCircle} className="w-4 h-4" /> 34 36 </span> … … 36 38 } else { 37 39 return ( 38 <span className="inline-flex items-center justify-center w-6 h-6 bg-red-100 text-red-600 rounded-full" >40 <span className="inline-flex items-center justify-center w-6 h-6 bg-red-100 text-red-600 rounded-full" title={t('not_completed')}> 39 41 <FontAwesomeIcon icon={faTimesCircle} className="w-4 h-4" /> 40 42 </span> … … 64 66 65 67 export default function ApplicationsPage() { 68 const { t } = useTranslation(); 66 69 const [selectedSession, setSelectedSession] = useState(applicationsData.currentSession.id); 67 70 const [isDropdownOpen, setIsDropdownOpen] = useState(false); 68 71 69 72 const currentSessionData = applicationsData.examSessions.find(s => s.id === selectedSession) || applicationsData.currentSession; 70 73 const { applications } = applicationsData; … … 79 82 </div> 80 83 <div> 81 <h1 className="text-3xl font-bold mb-2"> Пријави</h1>84 <h1 className="text-3xl font-bold mb-2">{t('applications_header', 'Пријави')}</h1> 82 85 <p className="text-lg opacity-90"> 83 Електронски пријави за испити86 {t('applications_subheader', 'Електронски пријави за испити')} 84 87 </p> 85 88 </div> … … 92 95 <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2"> 93 96 <FontAwesomeIcon icon={faCalendarAlt} className="text-primary" /> 94 Избери испитна сесија:97 {t('select_exam_session', 'Избери испитна сесија:')} 95 98 </h2> 96 99 97 100 <div className="relative"> 98 101 <button … … 102 105 <div className="flex items-center justify-between"> 103 106 <span className="text-sm font-medium text-primary"> 104 { currentSessionData.name}107 {t(currentSessionData.id, currentSessionData.name)} 105 108 </span> 106 <FontAwesomeIcon 107 icon={faChevronDown} 109 <FontAwesomeIcon 110 icon={faChevronDown} 108 111 className={`w-4 h-4 text-gray-400 transition-transform ml-4 ${isDropdownOpen ? 'rotate-180' : ''}`} 109 112 /> 110 113 </div> 111 114 </button> 112 115 113 116 {isDropdownOpen && ( 114 117 <div className="absolute z-10 right-0 mt-1 w-80 bg-white border border-gray-200 rounded-lg shadow-lg"> … … 122 125 className="w-full px-4 py-3 text-left text-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 first:rounded-t-lg last:rounded-b-lg" 123 126 > 124 <div className="font-medium text-gray-900">{ session.name}</div>125 <div className="text-xs text-gray-500">{session.year} - { session.semester} - {session.session}</div>127 <div className="font-medium text-gray-900">{t(session.id, session.name)}</div> 128 <div className="text-xs text-gray-500">{session.year} - {t(session.semester, session.semester)} - {t(session.session, session.session)}</div> 126 129 </button> 127 130 ))} … … 135 138 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 136 139 <div className="bg-primary text-white px-6 py-4"> 137 <h2 className="text-xl font-bold"> Пријавени испити</h2>138 </div> 139 140 <h2 className="text-xl font-bold">{t('registered_exams', 'Пријавени испити')}</h2> 141 </div> 142 140 143 <div className="overflow-x-auto"> 141 144 <table className="w-full"> … … 143 146 <tr> 144 147 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> 145 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Сериски број</th>146 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Код</th>147 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Предмет</th>148 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Завршена</th>149 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Таксени</th>150 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Датум</th>151 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Наставник</th>152 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Декада</th>148 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('serial_number', 'Сериски број')}</th> 149 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('code', 'Код')}</th> 150 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('subject', 'Предмет')}</th> 151 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('completed', 'Завршена')}</th> 152 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('fee', 'Таксени')}</th> 153 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('date', 'Датум')}</th> 154 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('instructor', 'Наставник')}</th> 155 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('decade', 'Декада')}</th> 153 156 </tr> 154 157 </thead> … … 166 169 <div className="flex items-center gap-2"> 167 170 <FontAwesomeIcon icon={faFileText} className="w-4 h-4 text-primary" /> 168 { application.subject}171 {t(application.subject, application.subject)} 169 172 </div> 170 173 </TableCell> … … 179 182 <div className="flex items-center gap-2"> 180 183 <FontAwesomeIcon icon={faUser} className="w-4 h-4 text-gray-400" /> 181 <span className="font-medium text-gray-900">{ application.instructor}</span>184 <span className="font-medium text-gray-900">{t(application.instructor, application.instructor)}</span> 182 185 </div> 183 186 </TableCell> … … 194 197 <FontAwesomeIcon icon={faInfoCircle} className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" /> 195 198 <div> 196 <h3 className="font-medium text-blue-900 mb-1"> Важна забелешка</h3>199 <h3 className="font-medium text-blue-900 mb-1">{t('important_note', 'Важна забелешка')}</h3> 197 200 <p className="text-sm text-blue-800 leading-relaxed"> 198 { applicationsData.note}201 {t('applications_important_note_text')} 199 202 </p> 200 203 </div> … … 211 214 </div> 212 215 <div> 213 <h3 className="text-lg font-bold text-gray-900"> Пријавени испити</h3>216 <h3 className="text-lg font-bold text-gray-900">{t('registered_exams', 'Пријавени испити')}</h3> 214 217 <p className="text-2xl font-bold text-blue-600"> 215 218 {applications.length} … … 225 228 </div> 226 229 <div> 227 <h3 className="text-lg font-bold text-gray-900"> Завршени</h3>230 <h3 className="text-lg font-bold text-gray-900">{t('completed', 'Завршени')}</h3> 228 231 <p className="text-2xl font-bold text-green-600"> 229 {applications.filter(app => app.completed === "Да").length}232 {applications.filter(app => app.completed === t('yes', 'Да')).length} 230 233 </p> 231 234 </div> … … 239 242 </div> 240 243 <div> 241 <h3 className="text-lg font-bold text-gray-900"> Вкупна такса</h3>244 <h3 className="text-lg font-bold text-gray-900">{t('total_fee', 'Вкупна такса')}</h3> 242 245 <p className="text-2xl font-bold text-yellow-600"> 243 246 {applications.reduce((sum, app) => sum + parseFloat(app.fee.replace(',', '.')), 0).toFixed(2)} -
src/app/students/documents/page.tsx
r298f9b3 r50f2fb4 15 15 import { useState } from 'react'; 16 16 import documentsData from '@/data/documents.json'; 17 import { useTranslation } from 'react-i18next'; 17 18 18 19 interface TableCellProps { … … 28 29 29 30 const StatusBadge = ({ status }: { status: string }) => { 30 if (status === "approved") { 31 const { t } = useTranslation(); 32 if (status === t('approved', 'Одобрено')) { 31 33 return ( 32 34 <span className="inline-flex items-center justify-center w-8 h-8 bg-green-100 text-green-600 rounded-full"> … … 43 45 44 46 const PriceBadge = ({ price }: { price: number }) => { 47 const { t } = useTranslation(); 45 48 if (price === 0) { 46 return <span className="font-medium text-green-600"> 0,00</span>;49 return <span className="font-medium text-green-600">{t('free', '0,00')}</span>; 47 50 } 48 51 return <span className="font-medium text-blue-600">{price.toFixed(2)}</span>; … … 55 58 const [currentPage, setCurrentPage] = useState(1); 56 59 const [recordsPerPage, setRecordsPerPage] = useState(15); 60 const { t } = useTranslation(); 57 61 58 62 const selectedDocument = documentsData.documentTypes.find(d => d.id === selectedDocumentType); … … 73 77 </div> 74 78 <div> 75 <h1 className="text-3xl font-bold mb-2"> Документи</h1>79 <h1 className="text-3xl font-bold mb-2">{t('documents')}</h1> 76 80 <p className="text-lg opacity-90"> 77 Барање и преглед на документи81 {t('documents_overview', 'Преглед на вашите документи и нивниот статус.')} 78 82 </p> 79 83 </div> … … 85 89 <h2 className="text-lg font-semibold text-gray-900 mb-6 flex items-center gap-2"> 86 90 <FontAwesomeIcon icon={faFileText} className="text-primary" /> 87 Ново барање за документ91 {t('new_document_request', 'Ново барање за документ')} 88 92 </h2> 89 93 … … 92 96 <div> 93 97 <label className="block text-sm font-medium text-gray-700 mb-2"> 94 Изберете документ:98 {t('select_document', 'Избери документ')}: 95 99 </label> 96 100 <div className="relative"> … … 101 105 <div className="flex items-center justify-between"> 102 106 <span className="text-sm text-gray-900 truncate"> 103 {selectedDocument ?.name || "Изберете документ"}107 {selectedDocument ? t(selectedDocument.id) : t('select_document', 'Избери документ')} 104 108 </span> 105 109 <FontAwesomeIcon … … 121 125 className="w-full px-4 py-3 text-left text-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 border-b border-gray-100 last:border-b-0" 122 126 > 123 <div className="font-medium text-gray-900">{ docType.name}</div>127 <div className="font-medium text-gray-900">{t(docType.id)}</div> 124 128 {docType.price > 0 && ( 125 <div className="text-xs text-blue-600 mt-1"> Цена: {docType.price} мкд</div>129 <div className="text-xs text-blue-600 mt-1">{t('price', 'Цена')}: {docType.price} мкд</div> 126 130 )} 127 131 </button> … … 135 139 <div> 136 140 <label className="block text-sm font-medium text-gray-700 mb-2"> 137 Коментар:141 {t('comment', 'Коментар')}: 138 142 </label> 139 143 <textarea … … 142 146 rows={4} 143 147 className="w-full border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary resize-none" 144 placeholder= "Додајте коментар..."148 placeholder={t('add_comment', 'Додај коментар')} 145 149 /> 146 150 </div> … … 153 157 > 154 158 <FontAwesomeIcon icon={faFileText} className="w-4 h-4" /> 155 Внеси159 {t('submit', 'Поднеси')} 156 160 </button> 157 161 </div> … … 161 165 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 162 166 <div className="bg-primary text-white px-6 py-4"> 163 <h2 className="text-xl font-bold"> Мои документи</h2>167 <h2 className="text-xl font-bold">{t('my_documents', 'Мои документи')}</h2> 164 168 </div> 165 169 … … 169 173 <tr> 170 174 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> 171 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Архива</th>172 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Датум</th>173 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Барање</th>174 <th className="px-4 py-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> Цена</th>175 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Платено</th>176 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Документ</th>177 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Плати онлајн</th>178 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider"> Статус</th>179 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Коментар</th>175 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('archive', 'Архива')}</th> 176 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('date', 'Датум')}</th> 177 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('request', 'Барање')}</th> 178 <th className="px-4 py-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{t('price', 'Цена')}</th> 179 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('paid', 'Платено')}</th> 180 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('document', 'Документ')}</th> 181 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('pay_online', 'Плати онлајн')}</th> 182 <th className="px-4 py-4 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{t('status', 'Статус')}</th> 183 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('comment', 'Коментар')}</th> 180 184 </tr> 181 185 </thead> … … 187 191 <TableCell className="text-gray-600">{document.date}</TableCell> 188 192 <TableCell className="font-medium text-gray-900 max-w-xs"> 189 { document.request}193 {t(document.request)} 190 194 </TableCell> 191 195 <TableCell className="text-right"> … … 206 210 <FontAwesomeIcon icon={faCheckCircle} className="w-5 h-5 text-green-600" /> 207 211 ) : ( 208 <span className="text-gray-400"> —</span>212 <span className="text-gray-400">{t('none', '—')}</span> 209 213 )} 210 214 </TableCell> … … 214 218 <TableCell> 215 219 {document.comment || ( 216 <span className="text-gray-400"> —</span>220 <span className="text-gray-400">{t('none', '—')}</span> 217 221 )} 218 222 </TableCell> … … 227 231 <div className="flex items-center gap-4 text-sm text-gray-700"> 228 232 <div className="flex items-center gap-2"> 229 <span> Прикажи редови:</span>233 <span>{t('show_rows', 'Прикажи редови')}:</span> 230 234 <select 231 235 value={recordsPerPage} … … 239 243 </div> 240 244 <div> 241 Страна<input245 {t('page', 'Страница')} <input 242 246 type="number" 243 247 min="1" … … 246 250 onChange={(e) => setCurrentPage(Number(e.target.value))} 247 251 className="w-12 border border-gray-300 rounded px-2 py-1 text-sm text-center focus:outline-none focus:ring-1 focus:ring-primary" 248 /> од{totalPages}252 /> {t('of', 'од')} {totalPages} 249 253 </div> 250 254 </div> … … 266 270 </button> 267 271 <span className="px-4 py-2 bg-primary text-white rounded text-sm font-medium"> 268 Прва272 {t('first', 'Прва')} 269 273 </span> 270 274 <button … … 283 287 </button> 284 288 <span className="ml-4 text-sm text-gray-700"> 285 Последна289 {t('last', 'Последна')} 286 290 </span> 287 291 </div> 288 292 289 293 <div className="text-sm text-gray-700"> 290 Вкупно: {documents.length}294 {t('total', 'Вкупно')}: {documents.length} 291 295 </div> 292 296 </div> … … 297 301 <FontAwesomeIcon icon={faMoneyBillWave} className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" /> 298 302 <p className="text-sm text-blue-800 leading-relaxed"> 299 { paymentInfo}303 {t('documents_payment_info', paymentInfo)} 300 304 </p> 301 305 </div> … … 311 315 </div> 312 316 <div> 313 <h3 className="text-lg font-bold text-gray-900"> Вкупно документи</h3>317 <h3 className="text-lg font-bold text-gray-900">{t('total_documents', 'Вкупно документи')}</h3> 314 318 <p className="text-2xl font-bold text-blue-600"> 315 319 {documents.length} … … 325 329 </div> 326 330 <div> 327 <h3 className="text-lg font-bold text-gray-900"> Одобрени</h3>331 <h3 className="text-lg font-bold text-gray-900">{t('approved', 'Одобрени')}</h3> 328 332 <p className="text-2xl font-bold text-green-600"> 329 333 {documents.filter(doc => doc.status === "approved").length} … … 339 343 </div> 340 344 <div> 341 <h3 className="text-lg font-bold text-gray-900"> Вкупна цена</h3>345 <h3 className="text-lg font-bold text-gray-900">{t('total_price', 'Вкупна цена')}</h3> 342 346 <p className="text-2xl font-bold text-yellow-600"> 343 {documents.reduce((sum, doc) => sum + doc.price, 0).toFixed(2)} мкд347 {documents.reduce((sum, doc) => sum + doc.price, 0).toFixed(2)} {t('mkd', 'мкд')} 344 348 </p> 345 349 </div> -
src/app/students/profile/page.tsx
r298f9b3 r50f2fb4 20 20 import { getAccessToken } from '@/lib/auth'; 21 21 import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; 22 import { useTranslation } from 'react-i18next'; 22 23 23 24 type PersonalInfo = { … … 94 95 } 95 96 96 const 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> 97 const InfoRow = ({ label, value, icon }: InfoRowProps) => { 98 const { t } = useTranslation(); 99 return ( 100 <div className="flex justify-between items-center py-3 border-b border-gray-100 last:border-b-0"> 101 <div className="flex items-center gap-2 text-gray-600 font-medium"> 102 {icon && <FontAwesomeIcon icon={icon} className="w-4 h-4" />} 103 <span>{t(label)}:</span> 104 </div> 105 <div className="text-gray-900 font-semibold text-right max-w-xs break-words"> 106 {value || t('n_a')} 107 </div> 101 108 </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 ); 109 ); 110 }; 107 111 108 112 interface SectionProps { … … 112 116 } 113 117 114 const 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> 118 const Section = ({ title, icon, children }: SectionProps) => { 119 const { t } = useTranslation(); 120 return ( 121 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 122 <div className="bg-primary text-white px-6 py-4"> 123 <div className="flex items-center gap-3"> 124 <FontAwesomeIcon icon={icon} className="text-xl" /> 125 <h2 className="text-xl font-bold">{t(title)}</h2> 126 </div> 127 </div> 128 <div className="p-6"> 129 {children} 120 130 </div> 121 131 </div> 122 <div className="p-6"> 123 {children} 124 </div> 125 </div> 126 ); 132 ); 133 }; 127 134 128 135 export default function ProfilePage() { … … 130 137 const [isLoading, setIsLoading] = useState(true); 131 138 const [errorMessage, setErrorMessage] = useState<string | null>(null); 139 const { t } = useTranslation(); 132 140 133 141 useEffect(() => { … … 180 188 <div className="min-h-screen pb-8"> 181 189 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 182 Loading...190 {t('loading')} 183 191 </div> 184 192 </div> … … 190 198 <div className="min-h-screen pb-8"> 191 199 <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.'}200 {errorMessage ?? t('failed_to_load_profile')} 193 201 </div> 194 202 </div> … … 214 222 </h1> 215 223 <div className="text-lg opacity-90"> 216 Индекс: {personalInfo.index} | ЕМБГ: {personalInfo.embg}224 {t('index')}: {personalInfo.index} | {t('embg')}: {personalInfo.embg} 217 225 </div> 218 226 <div className="text-base opacity-80 mt-1"> … … 227 235 228 236 {/* Personal Information */} 229 <Section title=" Лични податоци" icon={faIdCard}>230 <InfoRow label=" Име" value={personalInfo.firstName} />231 <InfoRow label=" Средно име" value={personalInfo.middleName} />232 <InfoRow label=" Презиме" value={personalInfo.lastName} />233 <InfoRow label=" Моминско презиме" value={personalInfo.maidenName} />237 <Section title="personal_info" icon={faIdCard}> 238 <InfoRow label="first_name" value={personalInfo.firstName} /> 239 <InfoRow label="middle_name" value={personalInfo.middleName} /> 240 <InfoRow label="last_name" value={personalInfo.lastName} /> 241 <InfoRow label="maiden_name" value={personalInfo.maidenName} /> 234 242 <InfoRow 235 label=" Датум на раѓање"243 label="date_of_birth" 236 244 value={personalInfo.dateOfBirth} 237 245 icon={faCalendarAlt} 238 246 /> 239 247 <InfoRow 240 label=" Пол"248 label="gender" 241 249 value={personalInfo.gender} 242 icon={personalInfo.gender === 'машки'? faMars : faVenus}250 icon={personalInfo.gender === t('male') ? faMars : faVenus} 243 251 /> 244 252 <InfoRow 245 label=" Националност"253 label="nationality" 246 254 value={personalInfo.nationality} 247 255 icon={faFlag} 248 256 /> 249 <InfoRow label=" Државјанство" value={personalInfo.citizenship} />250 <InfoRow label=" Стипендија" value={personalInfo.scholarship} />251 <InfoRow label=" Тековен план" value={personalInfo.currentPlan} />252 <InfoRow label=" Бр. во матична книга" value={personalInfo.registryNumber} />253 <InfoRow label=" Група на студирање" value={personalInfo.studyGroup} />257 <InfoRow label="citizenship" value={personalInfo.citizenship} /> 258 <InfoRow label="scholarship" value={personalInfo.scholarship} /> 259 <InfoRow label="current_plan" value={personalInfo.currentPlan} /> 260 <InfoRow label="registry_number" value={personalInfo.registryNumber} /> 261 <InfoRow label="study_group" value={personalInfo.studyGroup} /> 254 262 {personalInfo.notes && ( 255 263 <div className="mt-4 p-4 bg-blue-50 rounded-lg"> 256 <div className="text-sm font-medium text-blue-800 mb-1"> Забелешка:</div>264 <div className="text-sm font-medium text-blue-800 mb-1">{t('note')}:</div> 257 265 <div className="text-sm text-blue-700">{personalInfo.notes}</div> 258 266 </div> … … 261 269 262 270 {/* Birth Information */} 263 <Section title=" Податоци за раѓање" icon={faMapMarkerAlt}>264 <InfoRow label=" Место на раѓање" value={birthInfo.placeOfBirth} />265 <InfoRow label=" Општина на раѓање" value={birthInfo.municipalityOfBirth} />266 <InfoRow label=" Земја" value={birthInfo.country} />271 <Section title="birth_info" icon={faMapMarkerAlt}> 272 <InfoRow label="place_of_birth" value={birthInfo.placeOfBirth} /> 273 <InfoRow label="municipality_of_birth" value={birthInfo.municipalityOfBirth} /> 274 <InfoRow label="country" value={birthInfo.country} /> 267 275 </Section> 268 276 269 277 {/* Previous Education */} 270 <Section title=" Претходно образование" icon={faSchool}>271 <InfoRow label=" Тип" value={previousEducation.type} />272 <InfoRow label=" Професија" value={previousEducation.profession} />273 <InfoRow label=" Просек" value={previousEducation.average} />274 <InfoRow label=" Јазик" value={previousEducation.language} />275 <InfoRow label=" Земја" value={previousEducation.country} />276 <InfoRow label=" Претходен универзитет" value={previousEducation.previousUniversity} />277 <InfoRow label=" Претходен факултет" value={previousEducation.previousFaculty} />278 <InfoRow label=" Режим на претходни студии" value={previousEducation.previousStudyMode} />278 <Section title="previous_education" icon={faSchool}> 279 <InfoRow label="type" value={previousEducation.type} /> 280 <InfoRow label="profession" value={previousEducation.profession} /> 281 <InfoRow label="average" value={previousEducation.average} /> 282 <InfoRow label="language" value={previousEducation.language} /> 283 <InfoRow label="country" value={previousEducation.country} /> 284 <InfoRow label="previous_university" value={previousEducation.previousUniversity} /> 285 <InfoRow label="previous_faculty" value={previousEducation.previousFaculty} /> 286 <InfoRow label="previous_study_mode" value={previousEducation.previousStudyMode} /> 279 287 </Section> 280 288 281 289 {/* Enrollment Information */} 282 <Section title=" Податоци за упис" icon={faGraduationCap}>283 <InfoRow label=" Година на упис" value={enrollmentInfo.enrollmentYear} />284 <InfoRow label=" Статус" value={enrollmentInfo.status} />285 <InfoRow label=" Циклус" value={enrollmentInfo.cycle} />286 <InfoRow label=" Запишана програма" value={enrollmentInfo.program} />287 <InfoRow label=" Квота на прв упис" value={enrollmentInfo.quota} />288 <InfoRow label=" Бр. дипл. ср. образование" value={enrollmentInfo.secondaryEducationNumber} />289 <InfoRow label=" Кред. пр. образование" value={enrollmentInfo.previousEducationCredits} />290 <Section title="enrollment_info" icon={faGraduationCap}> 291 <InfoRow label="enrollment_year" value={enrollmentInfo.enrollmentYear} /> 292 <InfoRow label="status" value={enrollmentInfo.status} /> 293 <InfoRow label="cycle" value={enrollmentInfo.cycle} /> 294 <InfoRow label="program" value={enrollmentInfo.program} /> 295 <InfoRow label="quota" value={enrollmentInfo.quota} /> 296 <InfoRow label="secondary_education_number" value={enrollmentInfo.secondaryEducationNumber} /> 297 <InfoRow label="previous_education_credits" value={enrollmentInfo.previousEducationCredits} /> 290 298 </Section> 291 299 292 300 {/* Contact Information */} 293 301 <div className="lg:col-span-2"> 294 <Section title=" Контакт" icon={faAddressCard}>302 <Section title="contact" icon={faAddressCard}> 295 303 <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> 296 304 <div> 297 305 <InfoRow 298 label=" Место на живеење"306 label="place_of_residence" 299 307 value={contact.placeOfResidence} 300 308 icon={faMapMarkerAlt} 301 309 /> 302 <InfoRow label=" Општина на живеење" value={contact.municipalityOfResidence} />303 <InfoRow label=" Држава" value={contact.country} />304 <InfoRow label=" Адреса" value={contact.address} />305 <InfoRow label=" Адреса на престој" value={contact.temporaryAddress} />310 <InfoRow label="municipality_of_residence" value={contact.municipalityOfResidence} /> 311 <InfoRow label="country" value={contact.country} /> 312 <InfoRow label="address" value={contact.address} /> 313 <InfoRow label="temporary_address" value={contact.temporaryAddress} /> 306 314 </div> 307 315 <div> 308 <InfoRow label=" Телефон" value={contact.phone} icon={faPhone} />309 <InfoRow label=" Моб. телефон" value={contact.mobilePhone} icon={faPhone} />310 <InfoRow label=" Број на пасош" value={contact.passportNumber} icon={faPassport} />311 <InfoRow label=" Датум на истекување на пасошот" value={contact.passportExpiryDate} />316 <InfoRow label="phone" value={contact.phone} icon={faPhone} /> 317 <InfoRow label="mobile_phone" value={contact.mobilePhone} icon={faPhone} /> 318 <InfoRow label="passport_number" value={contact.passportNumber} icon={faPassport} /> 319 <InfoRow label="passport_expiry_date" value={contact.passportExpiryDate} /> 312 320 <InfoRow 313 label=" Е-пошта"321 label="email" 314 322 value={contact.email} 315 323 icon={faEnvelope} 316 324 /> 317 325 <InfoRow 318 label=" Microsoftemail"326 label="microsoft_email" 319 327 value={contact.microsoftEmail} 320 328 icon={faEnvelope} -
src/app/students/semesters/page.tsx
r298f9b3 r50f2fb4 14 14 import { useEffect, useState } from 'react'; 15 15 import { getAccessToken } from '@/lib/auth'; 16 import { useTranslation } from 'react-i18next'; 16 17 17 18 type Semester = { … … 54 55 55 56 const StatusBadge = ({ status }: { status: string }) => { 57 const { t } = useTranslation(); 56 58 const baseClasses = "px-2 py-1 rounded-full text-xs font-medium"; 57 58 if (status === "валиден") { 59 if (status === t('valid')) { 59 60 return ( 60 61 <span className={`${baseClasses} bg-green-100 text-green-800 flex items-center gap-1`}> 61 62 <FontAwesomeIcon icon={faCheckCircle} className="w-3 h-3" /> 62 { status}63 {t('valid')} 63 64 </span> 64 65 ); 65 66 } 66 67 67 return ( 68 68 <span className={`${baseClasses} bg-gray-100 text-gray-800`}> 69 { status}69 {t(status) || status} 70 70 </span> 71 71 ); … … 73 73 74 74 const YesNoBadge = ({ value }: { value: string }) => { 75 if (value === "Да") { 75 const { t } = useTranslation(); 76 if (value === t('yes')) { 76 77 return ( 77 78 <span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full"> … … 79 80 </span> 80 81 ); 81 } else if (value === "Не") {82 } else if (value === t('no')) { 82 83 return ( 83 84 <span className="inline-flex items-center justify-center w-6 h-6 bg-red-100 text-red-600 rounded-full"> … … 112 113 const [isLoading, setIsLoading] = useState(true); 113 114 const [errorMessage, setErrorMessage] = useState<string | null>(null); 115 const { t } = useTranslation(); 114 116 115 117 useEffect(() => { … … 162 164 <div className="min-h-screen pb-8"> 163 165 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 164 Loading...166 {t('loading')} 165 167 </div> 166 168 </div> … … 187 189 </div> 188 190 <div> 189 <h1 className="text-3xl font-bold mb-2"> Семестри</h1>191 <h1 className="text-3xl font-bold mb-2">{t('semesters')}</h1> 190 192 <p className="text-lg opacity-90"> 191 Преглед на сите запишани семестри и нивниот статус193 {t('semesters_overview')} 192 194 </p> 193 195 </div> … … 198 200 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 199 201 <div className="bg-primary text-white px-6 py-4"> 200 <h2 className="text-xl font-bold"> Листа на семестри</h2>202 <h2 className="text-xl font-bold">{t('semesters_list')}</h2> 201 203 </div> 202 204 … … 206 208 <tr> 207 209 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> 208 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Семестар</th>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"> Квота</th>211 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Забелешка</th>212 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Студ.Ком.</th>213 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Сума</th>214 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Платено</th>215 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> УКИМ</th>216 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Креирано на</th>217 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Датум промена</th>218 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Ц.Кр</th>219 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Тип</th>220 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Док.</th>221 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Док1.</th>222 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Вериф.</th>223 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Таксени</th>224 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Потписи</th>225 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Статус</th>226 <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> 227 229 </tr> 228 230 </thead> … … 335 337 </div> 336 338 <div> 337 <h3 className="text-lg font-bold text-gray-900"> Завршени семестри</h3>339 <h3 className="text-lg font-bold text-gray-900">{t('completed_semesters')}</h3> 338 340 <p className="text-2xl font-bold text-green-600"> 339 341 {semesters.filter(s => s.completed !== "Не").length} … … 349 351 </div> 350 352 <div> 351 <h3 className="text-lg font-bold text-gray-900"> Верифицирани</h3>353 <h3 className="text-lg font-bold text-gray-900">{t('verified_semesters')}</h3> 352 354 <p className="text-2xl font-bold text-blue-600"> 353 355 {semesters.filter(s => s.verified === "Да").length} … … 363 365 </div> 364 366 <div> 365 <h3 className="text-lg font-bold text-gray-900"> Вкупно семестри</h3>367 <h3 className="text-lg font-bold text-gray-900">{t('total_semesters')}</h3> 366 368 <p className="text-2xl font-bold text-primary"> 367 369 {semesters.length} -
src/app/students/subjects/page.tsx
r298f9b3 r50f2fb4 9 9 import { useEffect, useState } from 'react'; 10 10 import { getAccessToken } from '@/lib/auth'; 11 import { useTranslation } from 'react-i18next'; 11 12 12 13 interface Subject { … … 147 148 148 149 const StatusBadge = ({ status }: { status: string }) => { 150 const { t } = useTranslation(); 149 151 const baseClasses = "px-3 py-1 rounded-md text-xs font-medium"; 150 151 if (status === "Зад.") { 152 if (status === t('mandatory_short')) { 152 153 return ( 153 154 <span className={`${baseClasses} bg-blue-100 text-blue-800`}> 154 { status}155 {t('mandatory_short')} 155 156 </span> 156 157 ); 157 } else if (status === "Изб.") {158 } else if (status === t('elective_short')) { 158 159 return ( 159 160 <span className={`${baseClasses} bg-green-100 text-green-800`}> 160 { status}161 {t('elective_short')} 161 162 </span> 162 163 ); 163 164 } 164 165 165 return ( 166 166 <span className={`${baseClasses} bg-gray-100 text-gray-800`}> 167 { status}167 {t(status) || status} 168 168 </span> 169 169 ); … … 176 176 const [isLoading, setIsLoading] = useState(true); 177 177 const [errorMessage, setErrorMessage] = useState<string | null>(null); 178 const { t } = useTranslation(); 178 179 179 180 useEffect(() => { … … 275 276 <div className="min-h-screen pb-8"> 276 277 <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> 277 Loading...278 {t('loading')} 278 279 </div> 279 280 </div> … … 285 286 <div className="min-h-screen pb-8"> 286 287 <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 {errorMessage ?? t('failed_to_load_subjects')} 288 289 </div> 289 290 </div> … … 308 309 </div> 309 310 <div> 310 <h1 className="text-3xl font-bold mb-2"> Предмети</h1>311 <h1 className="text-3xl font-bold mb-2">{t('subjects')}</h1> 311 312 <p className="text-lg opacity-90"> 312 Преглед на запишани предмети по семестри313 {t('subjects_overview')} 313 314 </p> 314 315 </div> … … 325 326 <div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100"> 326 327 <div className="text-sm text-gray-600 mb-2"> 327 Статус: <span className="text-primary font-semibold">Запишан од студент</span>328 {t('status')}: <span className="text-primary font-semibold">{t('enrolled_by_student')}</span> 328 329 </div> 329 330 <div className="text-sm text-gray-600"> 330 Број на тикет: <span className="font-semibold">{currentSemester.ticketNumber}</span>331 {t('ticket_number')}: <span className="font-semibold">{currentSemester.ticketNumber}</span> 331 332 </div> 332 333 </div> … … 335 336 <div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100"> 336 337 <div className="text-sm text-gray-600 mb-2"> 337 Долг од документи: <span className="font-semibold">{currentSemester.debt}</span>338 {t('debt_from_documents')}: <span className="font-semibold">{currentSemester.debt}</span> 338 339 </div> 339 340 340 341 <div className="relative mt-4"> 341 342 <label className="block text-sm font-medium text-gray-700 mb-2"> 342 Избери семестар:343 {t('select_semester')}: 343 344 </label> 344 345 <div className="relative"> … … 370 371 > 371 372 <div className="font-medium text-gray-900">{semester.name}</div> 372 <div className="text-xs text-gray-500"> Статус: {semester.status}</div>373 <div className="text-xs text-gray-500">{t('status')}: {semester.status}</div> 373 374 </button> 374 375 ))} … … 380 381 <div className="mt-4 text-sm"> 381 382 <div className="text-primary font-medium"> 382 Сериски број: {currentSemesterData.serviceNumber}383 {t('serial_number')}: {currentSemesterData.serviceNumber} 383 384 </div> 384 385 </div> … … 389 390 <h3 className="font-semibold text-gray-900 mb-3 flex items-center gap-2"> 390 391 <FontAwesomeIcon icon={faBook} className="w-4 h-4 text-primary" /> 391 Запишани предмети392 {t('enrolled_subjects')} 392 393 </h3> 393 394 <div className="text-3xl font-bold text-primary"> … … 403 404 <h2 className="text-xl font-bold flex items-center gap-2"> 404 405 <FontAwesomeIcon icon={faFileInvoice} /> 405 Финансиски информации406 {t('financial_info')} 406 407 </h2> 407 408 </div> … … 413 414 <div className="space-y-4"> 414 415 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 415 <span className="text-gray-600"> Сума:</span>416 <span className="text-gray-600">{t('sum')}:</span> 416 417 <span className="font-semibold text-primary">{currentSemester.financialInfo.sum}</span> 417 418 </div> 418 419 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 419 <span className="text-gray-600"> Платено:</span>420 <span className="text-gray-600">{t('paid')}:</span> 420 421 <span className="font-semibold text-green-600">{currentSemester.financialInfo.paid}</span> 421 422 </div> 422 423 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 423 <span className="text-gray-600"> Должи:</span>424 <span className="text-gray-600">{t('due')}:</span> 424 425 <span className="font-semibold text-red-600">{currentSemester.financialInfo.due}</span> 425 426 </div> 426 427 <div className="mt-4 p-4 bg-blue-50 rounded-lg"> 427 <div className="text-sm font-medium text-blue-800 mb-1"> Материјални трошоци:</div>428 <div className="text-sm text-blue-700">{ currentSemester.financialInfo.materialCosts}</div>428 <div className="text-sm font-medium text-blue-800 mb-1">{t('material_costs')}:</div> 429 <div className="text-sm text-blue-700">{t('material_costs_info')}</div> 429 430 </div> 430 431 </div> … … 433 434 <div className="space-y-4"> 434 435 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 435 <span className="text-gray-600"> Кредити:</span>436 <span className="text-gray-600">{t('credits')}:</span> 436 437 <span className="font-semibold text-primary">{currentSemester.financialInfo.credits}</span> 437 438 </div> 438 439 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 439 <span className="text-gray-600"> МКСА:</span>440 <span className="text-gray-600">{t('mksa')}:</span> 440 441 <span className="font-semibold">{currentSemester.financialInfo.MKSA}</span> 441 442 </div> 442 443 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 443 <span className="text-gray-600"> Електронско запишување:</span>444 <span className="text-gray-600">{t('electronic_registration')}:</span> 444 445 <span className="font-semibold">{currentSemester.financialInfo.electronicRegistration}</span> 445 446 </div> 446 447 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 447 <span className="text-gray-600"> Е-УКИМ:</span>448 <span className="text-gray-600">{t('eukim')}:</span> 448 449 <span className="font-semibold">{currentSemester.financialInfo.eUKIM}</span> 449 450 </div> 450 451 <div className="flex justify-between items-center py-2 border-b border-gray-100"> 451 <span className="text-gray-600"> Банкарска провизија:</span>452 <span className="text-gray-600">{t('bank_provision')}:</span> 452 453 <span className="font-semibold">{currentSemester.financialInfo.bankProvision}</span> 453 454 </div> 454 455 <div className="flex justify-between items-center py-3 border-t-2 border-primary bg-primary bg-opacity-5 rounded-lg px-4"> 455 <span className="font-bold text-white"> Тотал:</span>456 <span className="font-bold text-white">{t('total')}:</span> 456 457 <span className="font-bold text-xl text-white">{currentSemester.financialInfo.total}</span> 457 458 </div> … … 466 467 <div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> 467 468 <div className="bg-primary text-white px-6 py-4"> 468 <h2 className="text-xl font-bold"> Листа на предмети</h2>469 <h2 className="text-xl font-bold">{t('subjects_list')}</h2> 469 470 </div> 470 471 … … 474 475 <tr> 475 476 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th> 476 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Код</th>477 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Часови</th>478 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Кој пат</th>479 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Предмет</th>480 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Семестар</th>481 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Статус</th>482 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Потпис</th>483 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Група</th>484 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Професор</th>477 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('code')}</th> 478 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('hours')}</th> 479 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('which_time')}</th> 480 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('subject')}</th> 481 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('semester')}</th> 482 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('status')}</th> 483 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('signature')}</th> 484 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('group')}</th> 485 <th className="px-4 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{t('professor')}</th> 485 486 </tr> 486 487 </thead> … … 495 496 <div className="flex items-center gap-2"> 496 497 <FontAwesomeIcon icon={faBook} className="w-4 h-4 text-primary" /> 497 { subject.name}498 {t(subject.name, subject.name)} 498 499 </div> 499 500 </TableCell> 500 <TableCell className="text-center font-medium text-primary">{ subject.semester}</TableCell>501 <TableCell className="text-center font-medium text-primary">{t(subject.semester.toString(), subject.semester.toString())}</TableCell> 501 502 <TableCell> 502 503 <StatusBadge status={subject.status} /> 503 504 </TableCell> 504 505 <TableCell className="text-center"> 505 {subject.signature ||(506 {subject.signature ? t(subject.signature, subject.signature) : ( 506 507 <span className="text-gray-400">—</span> 507 508 )} 508 509 </TableCell> 509 510 <TableCell className="text-center"> 510 {subject.group ||(511 {subject.group ? t(subject.group, subject.group) : ( 511 512 <span className="text-gray-400">—</span> 512 513 )} 513 514 </TableCell> 514 515 <TableCell> 515 {subject.professor ||(516 {subject.professor ? t(subject.professor, subject.professor) : ( 516 517 <span className="text-gray-400">—</span> 517 518 )} -
src/components/exams.tsx
r298f9b3 r50f2fb4 1 1 "use client" 2 2 import { useEffect, useMemo, useState } from 'react'; 3 import { useTranslation } from 'react-i18next'; 3 4 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 5 import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'; … … 33 34 34 35 const Exams = () => { 36 const { t } = useTranslation(); 35 37 const [sortField, setSortField] = useState<'grade' | 'date' | null>(null); 36 38 const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); … … 185 187 return ( 186 188 <div className="bg-white rounded-lg shadow-sm p-6"> 187 Loading...189 {t('loading', 'Loading...')} 188 190 </div> 189 191 ); … … 194 196 <div className="bg-white rounded-lg shadow-sm p-6"> 195 197 <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"> 196 { errorMessage}198 {t('failed_to_load_exams', errorMessage)} 197 199 </div> 198 200 </div> … … 208 210 <div className="text-center"> 209 211 <div className="text-3xl font-bold text-gray-900">{stats.average}</div> 210 <div className="text-sm text-gray-500 mt-1"> Просек</div>212 <div className="text-sm text-gray-500 mt-1">{t('average', 'Просек')}</div> 211 213 </div> 212 214 … … 217 219 <span className="text-lg text-gray-400">/{stats.credits.total}</span> 218 220 </div> 219 <div className="text-sm text-gray-500 mt-1"> Кредити</div>221 <div className="text-sm text-gray-500 mt-1">{t('credits', 'Кредити')}</div> 220 222 </div> 221 223 … … 223 225 <div className="text-center"> 224 226 <div className="text-3xl font-bold text-gray-900">{stats.passed}</div> 225 <div className="text-sm text-gray-500 mt-1"> Положени</div>227 <div className="text-sm text-gray-500 mt-1">{t('passed', 'Положени')}</div> 226 228 </div> 227 229 … … 229 231 <div className="text-center"> 230 232 <div className="text-3xl font-bold text-gray-900">{stats.remaining}</div> 231 <div className="text-sm text-gray-500 mt-1"> Останато</div>233 <div className="text-sm text-gray-500 mt-1">{t('remaining', 'Останато')}</div> 232 234 </div> 233 235 </div> 234 236 235 237 {/* Progress Bar spanning entire statistics section */} 236 238 <div className="w-full bg-gray-200 rounded-full h-2"> 237 <div 239 <div 238 240 className="bg-blue-500 h-2 rounded-full transition-all duration-300" 239 241 style={{ width: `${creditsPercentage}%` }} … … 244 246 {/* Exams Table */} 245 247 <div> 246 <h3 className="text-xl font-semibold text-gray-900 mb-4"> Испити</h3>247 248 <h3 className="text-xl font-semibold text-gray-900 mb-4">{t('exams', 'Испити')}</h3> 249 248 250 <div className="overflow-x-auto"> 249 251 <table className="min-w-full"> … … 251 253 <tr className="border-b border-gray-200"> 252 254 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500">#</th> 253 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500"> Предмет</th>254 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500"> Семестар</th>255 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500"> Кредити</th>255 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500">{t('subject', 'Предмет')}</th> 256 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500">{t('semester', 'Семестар')}</th> 257 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500">{t('credits', 'Кредити')}</th> 256 258 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500"> 257 <button 259 <button 258 260 onClick={handleSortByDate} 259 261 className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-all duration-200 transform hover:scale-105" 260 262 > 261 Датум262 <FontAwesomeIcon 263 icon={getSortIcon('date')} 263 {t('date', 'Датум')} 264 <FontAwesomeIcon 265 icon={getSortIcon('date')} 264 266 className={`text-xs transition-colors duration-200 ${ 265 267 sortField === 'date' ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-600' … … 269 271 </th> 270 272 <th className="text-left py-3 px-4 text-sm font-medium text-gray-500"> 271 <button 273 <button 272 274 onClick={handleSortByGrade} 273 275 className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-all duration-200 transform hover:scale-105" 274 276 > 275 Оценка276 <FontAwesomeIcon 277 icon={getSortIcon('grade')} 277 {t('grade', 'Оценка')} 278 <FontAwesomeIcon 279 icon={getSortIcon('grade')} 278 280 className={`text-xs transition-colors duration-200 ${ 279 281 sortField === 'grade' ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-600' -
src/components/header.tsx
r298f9b3 r50f2fb4 1 1 "use client" 2 2 3 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 4 import { faUser } from '@fortawesome/free-solid-svg-icons'; 4 5 import Image from 'next/image'; 5 6 7 import { useEffect, useState } from 'react'; 8 import { getAccessToken, clearAuthTokens } from '@/lib/auth'; 9 import { useTranslation } from 'react-i18next'; 10 import LanguageSwitcher from './LanguageSwitcher'; 11 6 12 const Header = () => { 13 14 const [userName, setUserName] = useState<string>(''); 15 const { t } = useTranslation(); 16 17 useEffect(() => { 18 async function fetchUser() { 19 const token = getAccessToken(); 20 if (!token) return; 21 try { 22 const response = await fetch('https://iknow-api.onrender.com/api/user/getUser', { 23 method: 'GET', 24 headers: { Authorization: `Bearer ${token}` }, 25 }); 26 if (!response.ok) return; 27 const data = await response.json(); 28 // Try both student and professor shape 29 if (data?.personalInfo) { 30 const p = data.personalInfo; 31 setUserName([p.firstName, p.middleName, p.lastName].filter(Boolean).join(' ')); 32 } 33 } catch {} 34 } 35 fetchUser(); 36 return () => {}; 37 }, []); 38 39 const handleLogout = () => { 40 clearAuthTokens(); 41 if (typeof window !== 'undefined') { 42 window.location.href = '/'; 43 } 44 }; 45 7 46 return ( 8 47 <header className="bg-white shadow-sm border-b border-gray-200"> 9 48 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 10 <div className="flex justify-between items-center py-4">49 <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center py-4 gap-4 sm:gap-0"> 11 50 {/* Left side - Logos and University Info */} 12 <div className="flex items-center gap- 4">51 <div className="flex items-center gap-2 justify-center sm:justify-start"> 13 52 {/* UKIM Logo */} 14 53 <div className="flex-shrink-0"> … … 21 60 /> 22 61 </div> 23 24 62 {/* FINKI Logo */} 25 63 <div className="flex-shrink-0"> … … 32 70 /> 33 71 </div> 34 35 72 {/* University Text */} 36 73 <div className="hidden sm:block"> 37 74 <div className="text-gray-900 font-medium text-sm"> 38 Универзитет „Св. КИРИЛ И МЕТОДИЈ“ - Скопје75 {t('university')} 39 76 </div> 40 77 <div className="text-gray-700 text-sm"> 41 Факултет за информатички науки и компјутерско инженерство78 {t('faculty')} 42 79 </div> 43 80 </div> 44 81 </div> 45 46 82 {/* Right side - User Profile */} 47 <div className="flex items-center gap-4"> 48 <div className="flex items-center gap-3"> 83 <div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-4 w-full sm:w-auto justify-center sm:justify-end"> 84 {/* Language Switcher - now left of user info */} 85 <div className="sm:mr-2 w-full sm:w-auto flex justify-center sm:justify-end"> 86 <LanguageSwitcher /> 87 </div> 88 <div className="flex items-center gap-2 sm:gap-3 w-full sm:w-auto justify-center sm:justify-end"> 49 89 {/* User Avatar */} 50 <div className=" flex items-center justify-center w-10 h-10 bg-gray-800 text-white rounded-full">90 <div className="hidden sm:flex items-center justify-center w-10 h-10 bg-gray-800 text-white rounded-full"> 51 91 <FontAwesomeIcon icon={faUser} className="text-sm" /> 52 92 </div> 53 54 93 {/* User Name */} 55 94 <div className="hidden sm:block text-gray-900 font-medium"> 56 Стефан Савески95 {userName || '...'} 57 96 </div> 58 59 {/* Logout Button */} 60 <button className="text-blue-600 hover:text-blue-800 font-medium text-sm transition-colors duration-200"> 61 Logout 97 {/* Logout Button - more stylish and full width on mobile */} 98 <button 99 className="w-full sm:w-auto ml-0 sm:ml-2 px-4 py-2 border-2 border-primary text-primary bg-white rounded-lg font-semibold flex items-center justify-center gap-2 hover:bg-blue-50 transition-all duration-200 shadow-sm" 100 onClick={handleLogout} 101 > 102 <svg xmlns='http://www.w3.org/2000/svg' className='h-4 w-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'><path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1' /></svg> 103 {t('logout')} 62 104 </button> 63 105 </div> -
src/components/navbar.tsx
r298f9b3 r50f2fb4 10 10 import { useState } from 'react'; 11 11 import Link from 'next/link'; 12 import { useTranslation } from 'react-i18next'; 12 13 13 14 const Navbar = () => { … … 18 19 }; 19 20 21 const { t } = useTranslation(); 20 22 const menuItems = [ 21 { icon: faUser, label: 'Профил', href: '/students/profile' },22 { icon: faBookmark, label: 'Семестри', href: '/students/semesters' },23 { icon: faBook, label: 'Предмети', href: '/students/subjects' },24 { icon: faPenToSquare, label: 'Пријави', href: '/students/applications' },25 { icon: faListCheck, label: 'Положени', href: '/students/exams' },26 { icon: faFilePdf, label: 'Документи', href: '/students/documents' }23 { icon: faUser, label: t('profile'), href: '/students/profile' }, 24 { icon: faBookmark, label: t('semesters'), href: '/students/semesters' }, 25 { icon: faBook, label: t('subjects'), href: '/students/subjects' }, 26 { icon: faPenToSquare, label: t('applications'), href: '/students/applications' }, 27 { icon: faListCheck, label: t('exams'), href: '/students/exams' }, 28 { icon: faFilePdf, label: t('documents'), href: '/students/documents' } 27 29 ]; 28 30 -
src/components/professor-navbar.tsx
r298f9b3 r50f2fb4 5 5 import Link from "next/link"; 6 6 import { useState } from "react"; 7 import { useTranslation } from 'react-i18next'; 7 8 8 9 const ProfessorNavbar = () => { … … 13 14 }; 14 15 16 const { t } = useTranslation(); 15 17 const menuItems = [ 16 { icon: faUser, label: "Профил", href: "/professor/profile" },17 { icon: faUsers, label: "Студенти", href: "/professor/students" },18 { icon: faUser, label: t('profile'), href: "/professor/profile" }, 19 { icon: faUsers, label: t('students') || 'Студенти', href: "/professor/students" }, 18 20 ]; 19 21
Note:
See TracChangeset
for help on using the changeset viewer.
