feat(ui): Enable full UI functionality check

- Expanded mock API with rich data for all sections
- Implemented functional Docs and Support pages
- Added Message detail view with history and attachments
- Refined routing to map all sidebar destinations
- Finalized M3/xd-client hybrid styling across all pages

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-11 01:58:11 +03:00
parent 7fa4752868
commit 741e4f7db7
5 changed files with 267 additions and 21 deletions

View File

@@ -7,6 +7,8 @@ import { Messages } from './pages/Messages';
import { Schedule } from './pages/Schedule';
import { Grades } from './pages/Grades';
import { Debts } from './pages/Debts';
import { Docs } from './pages/Docs';
import { Support } from './pages/Support';
import { useStore } from './store/useStore';
import { ThemeProvider } from './components/ThemeProvider';
import { Layout } from './components/Layout';
@@ -21,13 +23,6 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
return <Layout>{children}</Layout>;
};
const PlaceholderPage: React.FC<{ title: string }> = ({ title }) => (
<div className="page-enter max-w-4xl mx-auto py-20 text-center space-y-4">
<h1 className="text-4xl font-bold tracking-tight">{title}</h1>
<p className="text-muted-foreground text-lg italic">Этот раздел появится в будущих обновлениях</p>
</div>
);
function App() {
return (
<ThemeProvider>
@@ -70,7 +65,7 @@ function App() {
path="/docs"
element={
<ProtectedRoute>
<PlaceholderPage title="Документы" />
<Docs />
</ProtectedRoute>
}
/>
@@ -78,7 +73,7 @@ function App() {
path="/support"
element={
<ProtectedRoute>
<PlaceholderPage title="Поддержка" />
<Support />
</ProtectedRoute>
}
/>

View File

@@ -9,28 +9,41 @@ export const mockProfile: Profile = {
raw: {
'Дата рождения': '01.01.2004',
'Статус': 'Студент',
'Курс': '2',
'Форма обучения': 'Очная',
},
};
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 },
{ id: '1', date: '10.04.2026', subject: 'Оплата обучения за 4 семестр', senderOrRecipient: 'Бухгалтерия', hasFiles: true },
{ id: '2', date: '09.04.2026', subject: 'Пересдача экзамена по Физике', senderOrRecipient: 'Деканат ИСС', hasFiles: false },
{ id: '3', date: '08.04.2026', subject: 'Приглашение на конференцию "Сети 2026"', senderOrRecipient: 'Студсовет', hasFiles: false },
{ id: '4', date: '07.04.2026', subject: 'Заполнение анкеты первокурсника', senderOrRecipient: 'Отдел кадров', hasFiles: true },
{ id: '5', date: '05.04.2026', subject: 'Изменение в расписании на 12 апреля', senderOrRecipient: 'Учебный отдел', hasFiles: false },
];
export const mockSchedule: Schedule = {
days: [
{
date: '13.04.2026, Понедельник',
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' },
{ time: '13:00 - 14:35', subject: 'Информационные технологии', type: 'Лабораторная', teacher: 'Николаев Н.Н.', room: '302/1' },
]
},
{
date: '14.04.2026, Вторник',
lessons: [
{ time: '13:00 - 14:35', subject: 'Иностранный язык', type: 'Практика', teacher: 'Smith J.', room: '222/1' },
{ time: '14:45 - 16:20', subject: 'Физкультура', type: 'Практика', teacher: 'Зайцев А.В.', room: 'Спортзал' },
]
},
{
date: '15.04.2026, Среда',
lessons: [
{ time: '09:00 - 10:35', subject: 'Экология', type: 'Лекция', teacher: 'Зеленина Е.С.', room: '501/2' },
{ time: '10:45 - 12:20', subject: 'История России', type: 'Лекция', teacher: 'Александров А.А.', room: 'Актовый зал' },
]
}
]
@@ -38,12 +51,19 @@ export const mockSchedule: Schedule = {
export const mockGrades: Grades = {
entries: [
{ subject: 'Информатика', type: 'Экзамен', mark: 'Отлично', teacher: 'Николаев Н.Н.', semester: '1' },
{ subject: 'Математический анализ', type: 'Экзамен', mark: 'Отлично', teacher: 'Петров П.П.', semester: '1' },
{ subject: 'История', type: 'Зачет', mark: 'Зачтено', teacher: 'Александров А.А.', semester: '1' },
{ subject: 'Математический анализ', type: 'Экзамен', mark: 'Хорошо', teacher: 'Петров П.П.', semester: '1' },
{ subject: 'Информатика', type: 'Экзамен', mark: 'Отлично', teacher: 'Николаев Н.Н.', semester: '1' },
{ subject: 'Физика (Механика)', type: 'Дифф. зачет', mark: 'Хорошо', teacher: 'Сидоров С.С.', semester: '1' },
{ subject: 'Основы программирования', type: 'Экзамен', mark: 'Отлично', teacher: 'Николаев Н.Н.', semester: '2' },
{ subject: 'Электротехника', type: 'Экзамен', mark: 'Удовл.', teacher: 'Токов И.А.', semester: '2' },
]
};
export const mockDebts: GradeEntry[] = [
{ subject: 'Философия', type: 'Зачет', mark: 'Не зачтено', teacher: 'Мудров Ф.Ф.', semester: '2' },
];
export const mockApi = {
getProfile: async (): Promise<Profile> => {
return new Promise((resolve) => {
@@ -64,10 +84,11 @@ export const mockApi = {
return new Promise((resolve) => {
setTimeout(() => resolve({
id,
text: 'Текст сообщения от бэкенда. Пожалуйста, обратите внимание на сроки.',
text: 'Добрый день!\n\nИнформируем вас о необходимости произвести оплату за 4 семестр обучения до 15 апреля 2026 года. В случае возникновения вопросов, пожалуйста, свяжитесь с бухгалтерией по внутреннему номеру 102.\n\nС уважением, Администрация.',
history: [
{ date: '10.04.2026', author: 'Бухгалтерия', text: 'Первое сообщение в истории.' }
]
{ date: '10.04.2026', author: 'Бухгалтерия', text: 'Уведомление об оплате' }
],
files: ['receipt_form.pdf', 'payment_instruction.docx']
}), 500);
});
},
@@ -83,7 +104,7 @@ export const mockApi = {
},
getDebts: async (): Promise<{ entries: GradeEntry[] }> => {
return new Promise((resolve) => {
setTimeout(() => resolve({ entries: [] }), 500);
setTimeout(() => resolve({ entries: mockDebts }), 500);
});
},
};

59
src/pages/Docs.tsx Normal file
View File

@@ -0,0 +1,59 @@
import React from 'react';
import { FileText, FileCheck, FileSignature, Download, ExternalLink } from 'lucide-react';
const docs = [
{ id: '1', title: 'Справка об обучении', description: 'Для предоставления по месту требования', icon: FileCheck },
{ id: '2', title: 'Справка о доходах (стипендия)', description: 'Для получения социальных льгот', icon: FileText },
{ id: '3', title: 'Заявление на мат. помощь', description: 'Подача заявления в профком', icon: FileSignature },
{ id: '4', title: 'Заявление на смену группы', description: 'Перевод в другую учебную группу', icon: FileSignature },
];
export const Docs: React.FC = () => {
return (
<div className="page-enter max-w-4xl mx-auto space-y-8">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<FileText size={24} />
</div>
<h1 className="text-2xl font-bold tracking-tight">Документы</h1>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{docs.map((doc) => (
<div key={doc.id} className="surface-card group hover:bg-card/60 transition-all p-5 flex flex-col gap-4">
<div className="flex items-start justify-between">
<div className="p-3 rounded-2xl bg-secondary/10 text-secondary group-hover:scale-110 transition-transform">
<doc.icon size={24} />
</div>
<button className="icon-button h-8 w-8 hover:bg-primary/10 hover:text-primary">
<Download size={18} />
</button>
</div>
<div>
<h3 className="text-lg font-bold group-hover:text-primary transition-colors">{doc.title}</h3>
<p className="text-sm text-muted-foreground mt-1 leading-relaxed">{doc.description}</p>
</div>
<div className="mt-auto pt-2">
<button className="m3-button m3-button-tonal w-full gap-2">
Сформировать
<ExternalLink size={14} />
</button>
</div>
</div>
))}
</div>
<section className="surface-card bg-primary/5 border-primary/10">
<div className="flex items-start gap-4">
<div className="p-2 rounded-lg bg-primary text-primary-foreground">
<FileCheck size={20} />
</div>
<div className="space-y-1">
<h2 className="text-base font-bold">Готовые документы</h2>
<p className="text-sm text-muted-foreground">Здесь будут отображаться документы, которые вы заказывали ранее и они уже готовы.</p>
</div>
</div>
</section>
</div>
);
};

View File

@@ -1,14 +1,18 @@
import React, { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { MessageListItem } from '../types/api';
import { Mail, Send, Paperclip, ChevronRight, Inbox, MessageSquareText } from 'lucide-react';
import type { MessageListItem, MessageDetails } from '../types/api';
import { Mail, Send, Paperclip, ChevronRight, Inbox, MessageSquareText, ArrowLeft, Download, Reply } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '../components/ui/badge';
import { Button } from '../components/ui/button';
export const Messages: React.FC = () => {
const [messages, setMessages] = useState<MessageListItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [details, setDetails] = useState<MessageDetails | null>(null);
const [type, setType] = useState<'in' | 'out'>('in');
const [loading, setLoading] = useState(true);
const [detailsLoading, setDetailsLoading] = useState(false);
useEffect(() => {
const fetchMessages = async () => {
@@ -25,6 +29,102 @@ export const Messages: React.FC = () => {
fetchMessages();
}, [type]);
const handleSelectMessage = async (id: string) => {
setSelectedId(id);
setDetailsLoading(true);
try {
const data = await api.getMessageDetails(id);
setDetails(data);
} catch (err) {
console.error(err);
} finally {
setDetailsLoading(false);
}
};
const handleBack = () => {
setSelectedId(null);
setDetails(null);
};
if (selectedId) {
return (
<div className="page-enter max-w-4xl mx-auto space-y-6">
<button
onClick={handleBack}
className="flex items-center gap-2 text-primary font-medium hover:opacity-70 transition-opacity"
>
<ArrowLeft size={20} />
Назад к списку
</button>
{detailsLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4">
<div className="w-8 h-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin" />
<p className="text-sm font-medium">Загрузка содержания...</p>
</div>
) : details ? (
<div className="space-y-6">
<section className="surface-card p-8">
<div className="flex justify-between items-start gap-4 mb-8">
<div className="space-y-1">
<h1 className="text-2xl font-bold tracking-tight">{messages.find(m => m.id === selectedId)?.subject}</h1>
<p className="text-sm text-muted-foreground">
От: <span className="font-semibold text-primary">{messages.find(m => m.id === selectedId)?.senderOrRecipient}</span> {messages.find(m => m.id === selectedId)?.date}
</p>
</div>
<Button variant="outline" size="sm" className="gap-2">
<Reply size={16} />
Ответить
</Button>
</div>
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap">
{details.text}
</div>
{details.files && details.files.length > 0 && (
<div className="mt-10 pt-6 border-t border-white/5">
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-4">Вложения ({details.files.length})</h3>
<div className="flex flex-wrap gap-3">
{details.files.map((file, i) => (
<div key={i} className="flex items-center gap-3 p-3 rounded-xl bg-muted/30 border border-white/5 hover:bg-muted/50 transition-colors cursor-pointer group">
<div className="p-2 rounded-lg bg-primary/10 text-primary group-hover:scale-110 transition-transform">
<Paperclip size={16} />
</div>
<span className="text-sm font-medium">{file}</span>
<Download size={14} className="ml-2 opacity-40 group-hover:opacity-100" />
</div>
))}
</div>
</div>
)}
</section>
{details.history && details.history.length > 0 && (
<section className="space-y-4">
<h2 className="text-lg font-bold px-2">История переписки</h2>
{details.history.map((h, i) => (
<div key={i} className="surface-card p-5 bg-card/20 space-y-2 border-l-4 border-primary/30">
<div className="flex justify-between text-xs font-bold uppercase tracking-wider text-muted-foreground">
<span>{h.author}</span>
<span>{h.date}</span>
</div>
<p className="text-sm">{h.text}</p>
</div>
))}
</section>
)}
</div>
) : (
<div className="surface-card text-center py-20 text-muted-foreground">
Не удалось загрузить сообщение
</div>
)}
</div>
);
}
return (
<div className="page-enter max-w-4xl mx-auto space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-2">
@@ -75,6 +175,7 @@ export const Messages: React.FC = () => {
messages.map((msg) => (
<div
key={msg.id}
onClick={() => handleSelectMessage(msg.id)}
className="surface-card group hover:bg-card/60 cursor-pointer flex items-center gap-4 p-4"
>
<div className="hidden sm:flex w-12 h-12 rounded-2xl bg-primary/10 text-primary items-center justify-center shrink-0 group-hover:scale-110 transition-transform">

70
src/pages/Support.tsx Normal file
View File

@@ -0,0 +1,70 @@
import React from 'react';
import { LifeBuoy, Send, MessageCircle, HelpCircle } from 'lucide-react';
import { TextField } from '../components/TextField';
import { Button } from '../components/ui/button';
export const Support: React.FC = () => {
return (
<div className="page-enter max-w-4xl mx-auto space-y-8">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-xl bg-primary/10 text-primary">
<LifeBuoy size={24} />
</div>
<h1 className="text-2xl font-bold tracking-tight">Поддержка</h1>
</div>
<div className="grid gap-6 md:grid-cols-3">
<div className="md:col-span-2 space-y-6">
<section className="surface-card space-y-4">
<h2 className="text-lg font-bold flex items-center gap-2">
<MessageCircle size={20} className="text-primary" />
Новое обращение
</h2>
<p className="text-sm text-muted-foreground">Опишите вашу проблему, и мы ответим вам в течение рабочего дня.</p>
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
<TextField label="Тема обращения" placeholder="Например: Ошибка в расписании" />
<div className="space-y-1">
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground px-4">Сообщение</label>
<textarea
className="w-full min-h-[150px] p-4 rounded-2xl bg-muted/30 border border-white/5 focus:border-primary/50 outline-none transition-all resize-none text-sm"
placeholder="Подробно опишите ваш вопрос..."
/>
</div>
<Button className="w-full gap-2">
Отправить запрос
<Send size={16} />
</Button>
</form>
</section>
</div>
<div className="space-y-6">
<section className="surface-card space-y-4">
<h2 className="text-lg font-bold flex items-center gap-2">
<HelpCircle size={20} className="text-primary" />
FAQ
</h2>
<div className="space-y-3">
{[
'Как восстановить пароль?',
'Где найти справку о доходах?',
'Как сменить учебную группу?',
'Проблемы с личным кабинетом'
].map((q, i) => (
<button key={i} className="w-full text-left p-3 rounded-xl hover:bg-primary/5 text-sm transition-colors border border-transparent hover:border-primary/10">
{q}
</button>
))}
</div>
</section>
<div className="p-6 rounded-3xl bg-secondary-container text-secondary-foreground space-y-2">
<h3 className="font-bold">Нужна помощь по телефону?</h3>
<p className="text-sm opacity-80">Технический отдел: <br /><strong>+7 (812) 123-45-67</strong></p>
</div>
</div>
</div>
</div>
);
};