+
= ({
onFocus={handleFocus}
onBlur={handleBlur}
className="m3-text-field-input"
- placeholder=" " // Keep empty for :placeholder-shown trick
+ placeholder=" "
/>
- {(error || supportingText) && (
+ {(showError || supportingText) && (
- {error ? `Ошибка: ${error}` : supportingText}
+ {showError ? error : supportingText}
)}
diff --git a/src/pages/Debts.tsx b/src/pages/Debts.tsx
new file mode 100644
index 0000000..f5cf0bc
--- /dev/null
+++ b/src/pages/Debts.tsx
@@ -0,0 +1,61 @@
+import React, { useEffect, useState } from 'react';
+import { api } from '../api/client';
+import type { GradeEntry } from '../types/api';
+import { AlertCircle, CheckCircle } from 'lucide-react';
+
+export const Debts: React.FC = () => {
+ const [debts, setDebts] = useState
([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchDebts = async () => {
+ try {
+ const debtData = await api.getDebts();
+ setDebts(debtData.entries);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchDebts();
+ }, []);
+
+ if (loading) return Загрузка задолженностей...
;
+
+ return (
+
+
Задолженности
+
+ {debts.length === 0 ? (
+
+
+
У вас нет академических задолженностей. Поздравляем!
+
+ ) : (
+
+ {debts.map((debt, idx) => (
+
+
+
+
{debt.subject}
+
{debt.type} • {debt.semester} семестр
+
+
+ Долг
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/pages/Grades.tsx b/src/pages/Grades.tsx
new file mode 100644
index 0000000..5b8edc0
--- /dev/null
+++ b/src/pages/Grades.tsx
@@ -0,0 +1,75 @@
+import React, { useEffect, useState } from 'react';
+import { api } from '../api/client';
+import type { Grades as GradesType } from '../types/api';
+import { BookOpen } from 'lucide-react';
+
+export const Grades: React.FC = () => {
+ const [grades, setGrades] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchGrades = async () => {
+ try {
+ const data = await api.getGrades();
+ setGrades(data);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchGrades();
+ }, []);
+
+ const getMarkColor = (mark: string) => {
+ mark = mark.toLowerCase();
+ if (mark.includes('отлично') || mark.includes('зачтено')) return 'var(--md-sys-color-primary)';
+ if (mark.includes('хорошо')) return 'var(--md-sys-color-secondary)';
+ if (mark.includes('удовл')) return 'var(--md-sys-color-error)';
+ return 'var(--md-sys-color-outline)';
+ };
+
+ if (loading) return Загрузка успеваемости...
;
+
+ return (
+
+
Успеваемость
+
+ {!grades || grades.entries.length === 0 ? (
+
Нет данных об оценках
+ ) : (
+
+ {grades.entries.map((grade, idx) => (
+
+
+
+
+
+
{grade.subject}
+
{grade.type} • {grade.semester} семестр
+ {grade.teacher &&
{grade.teacher}
}
+
+
+ {grade.mark}
+
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index a5c5597..e9810d6 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -8,18 +8,37 @@ export const Login: React.FC = () => {
const { apiDomain, apiKey, isMockMode, setApiDomain, setApiKey, setMockMode } = useStore();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
+ const [domainError, setDomainError] = useState('');
+ const [keyError, setKeyError] = useState('');
const navigate = useNavigate();
+ const validate = () => {
+ let valid = true;
+ if (!isMockMode) {
+ if (!apiDomain) {
+ setDomainError('API Домен обязателен');
+ valid = false;
+ } else {
+ setDomainError('');
+ }
+ if (!apiKey) {
+ setKeyError('API Ключ обязателен');
+ valid = false;
+ } else {
+ setKeyError('');
+ }
+ }
+ return valid;
+ };
+
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
+ if (!validate()) return;
+
setLoading(true);
setError('');
try {
- if (!isMockMode && (!apiDomain || !apiKey)) {
- throw new Error('Заполните все поля');
- }
-
if (isMockMode && !apiKey) {
setApiKey('mock-session');
}
@@ -38,13 +57,14 @@ export const Login: React.FC = () => {
return (