diff --git a/src/App.tsx b/src/App.tsx index ba8eb72..c1e156b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,10 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { Login } from './pages/Login'; import { Profile } from './pages/Profile'; import { Settings } from './pages/Settings'; +import { Messages } from './pages/Messages'; +import { Schedule } from './pages/Schedule'; +import { Grades } from './pages/Grades'; +import { Debts } from './pages/Debts'; import { useStore } from './store/useStore'; import { ThemeProvider } from './components/ThemeProvider'; import { Layout } from './components/Layout'; @@ -19,7 +23,7 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) = const PlaceholderPage: React.FC<{ title: string }> = ({ title }) => (

{title}

-

Coming soon...

+

Функционал в разработке...

); @@ -41,7 +45,7 @@ function App() { path="/schedule" element={ - + } /> @@ -49,7 +53,23 @@ function App() { path="/grades" element={ - + + + } + /> + + + + } + /> + + } /> @@ -57,7 +77,7 @@ function App() { path="/messages" element={ - + } /> diff --git a/src/api/client.ts b/src/api/client.ts index 9a9a763..a28fff7 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { useStore } from '../store/useStore'; -import type { Profile, ApiResponse } from '../types/api'; +import type { Profile, ApiResponse, MessageListItem, MessageDetails, Schedule, Grades, GradeEntry } from '../types/api'; import { mockApi } from './mock'; const getClient = () => { @@ -30,4 +30,46 @@ export const api = { const response = await client.get('/v1/health'); return response.data; }, + getMessages: async (type: 'in' | 'out' = 'in') => { + if (useStore.getState().isMockMode) { + return mockApi.getMessages(type); + } + const client = getClient(); + const response = await client.get>('/v1/messages', { + params: { type }, + }); + return response.data.data.entries; + }, + getMessageDetails: async (id: string) => { + if (useStore.getState().isMockMode) { + return mockApi.getMessageDetails(id); + } + const client = getClient(); + const response = await client.get>(`/v1/messages/${id}`); + return response.data.data; + }, + getSchedule: async () => { + if (useStore.getState().isMockMode) { + return mockApi.getSchedule(); + } + const client = getClient(); + const response = await client.get>('/v1/schedule'); + return response.data.data; + }, + getGrades: async () => { + if (useStore.getState().isMockMode) { + return mockApi.getGrades(); + } + const client = getClient(); + const response = await client.get>('/v1/grades'); + return response.data.data; + }, + getDebts: async () => { + if (useStore.getState().isMockMode) { + return mockApi.getDebts(); + } + const client = getClient(); + const response = await client.get>('/v1/debts'); + return response.data.data; + }, }; diff --git a/src/api/mock.ts b/src/api/mock.ts index 637da4e..bac270f 100644 --- a/src/api/mock.ts +++ b/src/api/mock.ts @@ -1,4 +1,4 @@ -import type { Profile } from '../types/api'; +import type { Profile, MessageListItem, MessageDetails, Schedule, Grades, GradeEntry } from '../types/api'; export const mockProfile: Profile = { fullName: 'Иванов Иван Иванович', @@ -12,6 +12,38 @@ export const mockProfile: Profile = { }, }; +export const mockMessages: MessageListItem[] = [ + { id: '1', date: '10.04.2026', subject: 'Оплата обучения', senderOrRecipient: 'Бухгалтерия', hasFiles: true }, + { id: '2', date: '09.04.2026', subject: 'Пересдача экзамена', senderOrRecipient: 'Деканат ИСС', hasFiles: false }, + { id: '3', date: '08.04.2026', subject: 'Приглашение на конференцию', senderOrRecipient: 'Студсовет', hasFiles: false }, +]; + +export const mockSchedule: Schedule = { + days: [ + { + date: '13.04.2026, Понедельник', + lessons: [ + { time: '09:00 - 10:35', subject: 'Высшая математика', type: 'Лекция', teacher: 'Петров П.П.', room: '123/1' }, + { time: '10:45 - 12:20', subject: 'Физика', type: 'Практика', teacher: 'Сидоров С.С.', room: '456/2' }, + ] + }, + { + date: '14.04.2026, Вторник', + lessons: [ + { time: '13:00 - 14:35', subject: 'Иностранный язык', type: 'Практика', teacher: 'Smith J.', room: '222/1' }, + ] + } + ] +}; + +export const mockGrades: Grades = { + entries: [ + { subject: 'Информатика', type: 'Экзамен', mark: 'Отлично', teacher: 'Николаев Н.Н.', semester: '1' }, + { subject: 'История', type: 'Зачет', mark: 'Зачтено', teacher: 'Александров А.А.', semester: '1' }, + { subject: 'Математический анализ', type: 'Экзамен', mark: 'Хорошо', teacher: 'Петров П.П.', semester: '1' }, + ] +}; + export const mockApi = { getProfile: async (): Promise => { return new Promise((resolve) => { @@ -23,4 +55,35 @@ export const mockApi = { setTimeout(() => resolve({ status: 'ok', mock: true }), 300); }); }, + getMessages: async (type: 'in' | 'out'): Promise => { + return new Promise((resolve) => { + setTimeout(() => resolve(type === 'in' ? mockMessages : []), 500); + }); + }, + getMessageDetails: async (id: string): Promise => { + return new Promise((resolve) => { + setTimeout(() => resolve({ + id, + text: 'Текст сообщения от бэкенда. Пожалуйста, обратите внимание на сроки.', + history: [ + { date: '10.04.2026', author: 'Бухгалтерия', text: 'Первое сообщение в истории.' } + ] + }), 500); + }); + }, + getSchedule: async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => resolve(mockSchedule), 500); + }); + }, + getGrades: async (): Promise => { + return new Promise((resolve) => { + setTimeout(() => resolve(mockGrades), 500); + }); + }, + getDebts: async (): Promise<{ entries: GradeEntry[] }> => { + return new Promise((resolve) => { + setTimeout(() => resolve({ entries: [] }), 500); + }); + }, }; diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index ff5f3ed..e6aac7a 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -9,7 +9,8 @@ import { ChevronDown, ChevronRight, Menu, - LogOut + LogOut, + AlertCircle } from 'lucide-react'; import { useStore } from '../store/useStore'; import { useDisplay } from '../hooks/useDisplay'; @@ -22,7 +23,7 @@ interface NavItem { icon: any; iconFilled: any; badgeKey?: 'messages'; - children?: { path: string; label: string }[]; + children?: { path: string; label: string; icon?: any }[]; } const navItems: NavItem[] = [ @@ -33,7 +34,7 @@ const navItems: NavItem[] = [ icon: Calendar, iconFilled: Calendar, children: [ - { path: '/schedule/week', label: 'Текущая неделя' }, + { path: '/schedule', label: 'Текущая неделя' }, { path: '/schedule/session', label: 'Сессия' }, ] }, @@ -43,8 +44,8 @@ const navItems: NavItem[] = [ icon: GraduationCap, iconFilled: GraduationCap, children: [ - { path: '/grades/list', label: 'Оценки' }, - { path: '/grades/statements', label: 'Ведомости' }, + { path: '/grades', label: 'Оценки' }, + { path: '/debts', label: 'Задолженности', icon: AlertCircle }, ] }, { @@ -120,7 +121,19 @@ export const Navigation: React.FC = () => { - {isExpanded && Bonch} + {isExpanded ? ( +
+
+ Б +
+ Bonch +
+ ) : null}
@@ -153,8 +166,9 @@ export const Navigation: React.FC = () => { `nav-item ${isActive ? 'active' : ''}`} - onClick={() => { + onClick={(e) => { if (hasChildren && isExpanded) { + e.preventDefault(); // Don't navigate to parent if it has children in rail toggleSubmenu(item.label); } }} @@ -179,6 +193,7 @@ export const Navigation: React.FC = () => { to={child.path} className={({ isActive }) => `submenu-item ${isActive ? 'active' : ''}`} > + {child.icon && } {child.label} ))} diff --git a/src/components/TextField.tsx b/src/components/TextField.tsx index 8db8e08..cf30e84 100644 --- a/src/components/TextField.tsx +++ b/src/components/TextField.tsx @@ -17,6 +17,7 @@ export const TextField: React.FC = ({ ...props }) => { const [isFocused, setIsFocused] = useState(false); + const [isTouched, setIsTouched] = useState(false); const isFilled = value !== undefined && value !== ''; const handleFocus = (e: React.FocusEvent) => { @@ -26,11 +27,15 @@ export const TextField: React.FC = ({ const handleBlur = (e: React.FocusEvent) => { setIsFocused(false); + setIsTouched(true); onBlur?.(e); }; + // Show error only after touch + const showError = isTouched && !!error; + return ( -
+
= ({ onFocus={handleFocus} onBlur={handleBlur} className="m3-text-field-input" - placeholder=" " // Keep empty for :placeholder-shown trick + placeholder=" " />