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:
13
src/App.tsx
13
src/App.tsx
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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
59
src/pages/Docs.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
70
src/pages/Support.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user