feat(ui): Solve issues 4-10 and polish UI
- Added top padding to all pages (Fix #4) - Implemented smooth accordion animations for navigation and messages (Fix #5, #9) - Fixed double selection highlight in navigation bar (Fix #6) - Enabled nested schedule routing for 'Session' view (Fix #7) - Softened switching animations for message types (Fix #8) - Reworked messages to use accordion expansion and removed history (Fix #9) - Moved Support section into Settings page (Fix #10) - Cleaned up unused imports and refined layouts Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -37,8 +37,9 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Fix #7: Use wildcard for nested routes */}
|
||||
<Route
|
||||
path="/schedule"
|
||||
path="/schedule/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Schedule />
|
||||
|
||||
@@ -3,7 +3,6 @@ import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
CalendarDays,
|
||||
GraduationCap,
|
||||
LifeBuoy,
|
||||
LogOut,
|
||||
Menu,
|
||||
MessageSquareText,
|
||||
@@ -47,7 +46,6 @@ const navItems: NavItemConfig[] = [
|
||||
]
|
||||
},
|
||||
{ path: '/messages', label: 'Сообщения', icon: MessageSquareText, badgeKey: 'messages' },
|
||||
{ path: '/support', label: 'Поддержка', icon: LifeBuoy },
|
||||
];
|
||||
|
||||
export const Navigation: React.FC = () => {
|
||||
@@ -80,7 +78,7 @@ export const Navigation: React.FC = () => {
|
||||
if (navMode === 'bar') {
|
||||
return (
|
||||
<nav className={cn("navigation-bar", scrollDir === 'down' && "hidden")}>
|
||||
{navItems.slice(0, 5).map((item) => (
|
||||
{navItems.slice(0, 4).map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
@@ -109,15 +107,15 @@ export const Navigation: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{isExpanded && profile && (
|
||||
<div className="mx-4 mb-6 p-3 rounded-2xl bg-sidebar-accent/30 flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10 border border-white/10">
|
||||
<AvatarFallback className="bg-primary text-primary-foreground text-xs">
|
||||
<div className="mx-4 mb-6 p-3 rounded-2xl bg-sidebar-accent/30 flex items-center gap-3 border border-white/5 animate-in fade-in slide-in-from-left-2 duration-300">
|
||||
<Avatar className="h-10 w-10 border border-white/10 shadow-sm">
|
||||
<AvatarFallback className="bg-primary text-primary-foreground text-xs font-bold">
|
||||
{getInitials(profile.fullName)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold truncate">{profile.fullName}</div>
|
||||
<div className="text-[11px] opacity-60 truncate uppercase tracking-wider">{profile.group || 'БЕЗ ГРУППЫ'}</div>
|
||||
<div className="text-sm font-semibold truncate leading-tight">{profile.fullName}</div>
|
||||
<div className="text-[10px] opacity-60 truncate uppercase tracking-widest mt-0.5">{profile.group || 'БЕЗ ГРУППЫ'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -131,7 +129,7 @@ export const Navigation: React.FC = () => {
|
||||
<div key={item.label} className="nav-group mb-1">
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) => cn("nav-item", isActive && "active")}
|
||||
className={({ isActive }) => cn("nav-item", (isActive || (hasChildren && isItemExpanded)) && "active")}
|
||||
onClick={(e) => {
|
||||
if (hasChildren && isExpanded) {
|
||||
e.preventDefault();
|
||||
@@ -145,25 +143,30 @@ export const Navigation: React.FC = () => {
|
||||
</div>
|
||||
{isExpanded && <span className="nav-item-label">{item.label}</span>}
|
||||
{isExpanded && hasChildren && (
|
||||
<div className="ml-auto">
|
||||
<div className="ml-auto opacity-60">
|
||||
{isItemExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
|
||||
{isExpanded && isItemExpanded && hasChildren && (
|
||||
<div className="submenu">
|
||||
{item.children!.map(child => (
|
||||
<NavLink
|
||||
key={child.path}
|
||||
to={child.path}
|
||||
className={({ isActive }) => cn("submenu-item", isActive && "active")}
|
||||
>
|
||||
{child.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="submenu"
|
||||
style={{
|
||||
maxHeight: isExpanded && isItemExpanded ? '200px' : '0',
|
||||
opacity: isExpanded && isItemExpanded ? 1 : 0,
|
||||
margin: isExpanded && isItemExpanded ? '4px 0 8px' : '0'
|
||||
}}
|
||||
>
|
||||
{item.children?.map(child => (
|
||||
<NavLink
|
||||
key={child.path}
|
||||
to={child.path}
|
||||
className={({ isActive }) => cn("submenu-item", isActive && "active")}
|
||||
>
|
||||
{child.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -172,7 +175,7 @@ export const Navigation: React.FC = () => {
|
||||
<div className="mt-auto p-4 space-y-2">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={({ isActive }) => cn("nav-item mx-0 h-10", isActive && "active")}
|
||||
className={({ isActive }) => cn("nav-item mx-0 h-10 px-4", isActive && "active")}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<MoreVertical size={18} />
|
||||
@@ -181,7 +184,7 @@ export const Navigation: React.FC = () => {
|
||||
</NavLink>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="nav-item mx-0 h-10 w-full hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
className="nav-item mx-0 h-10 w-full px-4 hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<LogOut size={18} />
|
||||
|
||||
@@ -1,133 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { MessageListItem, MessageDetails } from '../types/api';
|
||||
import { Mail, Send, Paperclip, ChevronRight, Inbox, MessageSquareText, ArrowLeft, Download, Reply } from 'lucide-react';
|
||||
import { Mail, Send, Paperclip, ChevronDown, Inbox, MessageSquareText, Download, Reply, Loader2 } 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 [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [details, setDetails] = useState<Record<string, MessageDetails>>({});
|
||||
const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());
|
||||
const [type, setType] = useState<'in' | 'out'>('in');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detailsLoading, setDetailsLoading] = useState(false);
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMessages = async () => {
|
||||
setLoading(true);
|
||||
setListLoading(true);
|
||||
setExpandedId(null);
|
||||
try {
|
||||
const data = await api.getMessages(type);
|
||||
setMessages(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setListLoading(false);
|
||||
}
|
||||
};
|
||||
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 toggleExpand = async (id: string) => {
|
||||
if (expandedId === id) {
|
||||
setExpandedId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedId(id);
|
||||
|
||||
if (!details[id]) {
|
||||
setLoadingIds(prev => new Set(prev).add(id));
|
||||
try {
|
||||
const data = await api.getMessageDetails(id);
|
||||
setDetails(prev => ({ ...prev, [id]: data }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoadingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<div className="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 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
||||
<MessageSquareText size={24} />
|
||||
@@ -138,7 +67,7 @@ export const Messages: React.FC = () => {
|
||||
<div className="flex p-1 rounded-2xl bg-muted/30 border border-white/5 w-fit">
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all",
|
||||
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200",
|
||||
type === 'in' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setType('in')}
|
||||
@@ -148,7 +77,7 @@ export const Messages: React.FC = () => {
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all",
|
||||
"flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200",
|
||||
type === 'out' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
onClick={() => setType('out')}
|
||||
@@ -159,57 +88,126 @@ export const Messages: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
{listLoading ? (
|
||||
<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>
|
||||
<Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
|
||||
<p className="text-sm font-medium italic opacity-60">Загрузка сообщений...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{messages.length === 0 ? (
|
||||
<div className="surface-card flex flex-col items-center justify-center py-20 text-muted-foreground gap-3">
|
||||
<Mail size={40} className="opacity-20" />
|
||||
<p className="text-sm">Нет сообщений в данной категории</p>
|
||||
<div className="surface-card animate-in fade-in zoom-in-95 duration-300 flex flex-col items-center justify-center py-20 text-muted-foreground gap-3">
|
||||
<Mail size={40} className="opacity-10" />
|
||||
<p className="text-sm italic">Нет сообщений в данной категории</p>
|
||||
</div>
|
||||
) : (
|
||||
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">
|
||||
{type === 'in' ? <Mail size={22} /> : <Send size={22} />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-primary truncate">
|
||||
{msg.senderOrRecipient}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground whitespace-nowrap">
|
||||
{msg.date}
|
||||
</span>
|
||||
messages.map((msg, index) => {
|
||||
const isExpanded = expandedId === msg.id;
|
||||
const isLoading = loadingIds.has(msg.id);
|
||||
const msgDetails = details[msg.id];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
"surface-card flex flex-col p-0 overflow-hidden transition-all duration-300",
|
||||
isExpanded ? "ring-2 ring-primary/20 bg-card/80" : "animate-in fade-in slide-in-from-bottom-2"
|
||||
)}
|
||||
style={{ animationDelay: `${index * 50}ms`, animationFillMode: 'both' }}
|
||||
>
|
||||
<div
|
||||
onClick={() => toggleExpand(msg.id)}
|
||||
className="flex items-center gap-4 p-4 sm:p-5 cursor-pointer hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className={cn(
|
||||
"hidden sm:flex w-12 h-12 rounded-2xl items-center justify-center shrink-0 transition-all duration-300",
|
||||
isExpanded ? "bg-primary text-primary-foreground scale-90" : "bg-primary/10 text-primary"
|
||||
)}>
|
||||
{type === 'in' ? <Mail size={22} /> : <Send size={22} />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2 mb-1">
|
||||
<span className={cn(
|
||||
"text-[10px] font-bold uppercase tracking-widest truncate transition-colors",
|
||||
isExpanded ? "text-primary" : "text-muted-foreground opacity-80"
|
||||
)}>
|
||||
{msg.senderOrRecipient}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground whitespace-nowrap opacity-60">
|
||||
{msg.date}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className={cn(
|
||||
"text-base font-semibold truncate transition-colors",
|
||||
isExpanded && "text-primary whitespace-normal"
|
||||
)}>
|
||||
{msg.subject}
|
||||
</h3>
|
||||
{!isExpanded && msg.hasFiles && (
|
||||
<div className="flex items-center gap-1.5 mt-1.5 text-primary opacity-80">
|
||||
<Paperclip size={12} />
|
||||
<span className="text-[10px] font-bold uppercase">Вложения</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center text-muted-foreground transition-transform duration-300",
|
||||
isExpanded && "rotate-180 text-primary"
|
||||
)}>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold truncate group-hover:text-primary transition-colors">
|
||||
{msg.subject}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
{msg.hasFiles && (
|
||||
<Badge variant="secondary" className="h-5 text-[10px] px-2 bg-primary/5 text-primary border-primary/10">
|
||||
<Paperclip size={10} className="mr-1" />
|
||||
Вложения
|
||||
</Badge>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground truncate opacity-60">
|
||||
Нажмите, чтобы прочитать полностью...
|
||||
</span>
|
||||
|
||||
<div className={cn(
|
||||
"overflow-hidden transition-all duration-300 ease-in-out",
|
||||
isExpanded ? "max-h-[1000px] border-t border-white/5" : "max-h-0"
|
||||
)}>
|
||||
<div className="p-5 sm:p-8 space-y-6">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8 gap-3 text-muted-foreground">
|
||||
<Loader2 size={18} className="animate-spin text-primary" />
|
||||
<span className="text-sm italic">Загрузка содержания...</span>
|
||||
</div>
|
||||
) : msgDetails ? (
|
||||
<>
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button variant="outline" size="xs" className="rounded-xl h-8 px-3 gap-2 border-white/10">
|
||||
<Reply size={14} />
|
||||
Ответить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none text-foreground/90 leading-relaxed whitespace-pre-wrap text-sm sm:text-base">
|
||||
{msgDetails.text}
|
||||
</div>
|
||||
|
||||
{msgDetails.files && msgDetails.files.length > 0 && (
|
||||
<div className="pt-6 mt-6 border-t border-white/5 space-y-3">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground">Вложения</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{msgDetails.files.map((file, i) => (
|
||||
<div key={i} className="flex items-center gap-2.5 p-2.5 rounded-xl bg-muted/30 border border-white/5 hover:bg-primary/5 hover:border-primary/20 transition-all cursor-pointer group">
|
||||
<Paperclip size={14} className="text-primary/60 group-hover:text-primary transition-colors" />
|
||||
<span className="text-xs font-medium truncate max-w-[150px]">{file}</span>
|
||||
<Download size={12} className="ml-1 opacity-0 group-hover:opacity-60 transition-opacity" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center py-4 text-destructive text-sm font-medium">
|
||||
Не удалось загрузить текст сообщения
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary transition-colors">
|
||||
<ChevronRight size={18} />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { Schedule as ScheduleType } from '../types/api';
|
||||
import { Clock, User, MapPin, CalendarDays } from 'lucide-react';
|
||||
import { Clock, User, MapPin, CalendarDays, Loader2 } from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const Schedule: React.FC = () => {
|
||||
const [schedule, setSchedule] = useState<ScheduleType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const location = useLocation();
|
||||
const isSession = location.pathname.includes('/session');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSchedule = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.getSchedule();
|
||||
setSchedule(data);
|
||||
@@ -19,35 +23,37 @@ export const Schedule: React.FC = () => {
|
||||
}
|
||||
};
|
||||
fetchSchedule();
|
||||
}, []);
|
||||
}, [isSession]);
|
||||
|
||||
if (loading) return (
|
||||
<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>
|
||||
<Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
|
||||
<p className="text-sm font-medium italic opacity-60">Загрузка расписания...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="page-enter max-w-4xl mx-auto space-y-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center gap-3 mb-2 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
||||
<CalendarDays size={24} />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Расписание</h1>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{isSession ? 'Расписание сессии' : 'Расписание занятий'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{!schedule || schedule.days.length === 0 ? (
|
||||
<div className="surface-card text-center py-12 text-muted-foreground">
|
||||
<div className="surface-card text-center py-12 text-muted-foreground italic">
|
||||
Нет данных о расписании
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-10">
|
||||
{schedule.days.map((day, idx) => (
|
||||
<section key={idx} className="space-y-4">
|
||||
<section key={idx} className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-500" style={{ animationDelay: `${idx * 100}ms`, animationFillMode: 'both' }}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-px flex-1 bg-white/5" />
|
||||
<h2 className="text-[11px] font-bold text-primary uppercase tracking-[0.2em]">
|
||||
<h2 className="text-[11px] font-bold text-primary uppercase tracking-[0.2em] bg-primary/5 px-3 py-1 rounded-full border border-primary/10">
|
||||
{day.date}
|
||||
</h2>
|
||||
<div className="h-px flex-1 bg-white/5" />
|
||||
@@ -57,7 +63,7 @@ export const Schedule: React.FC = () => {
|
||||
{day.lessons.map((lesson, lIdx) => (
|
||||
<div
|
||||
key={lIdx}
|
||||
className="surface-card flex flex-col sm:flex-row gap-4 sm:items-center p-5 hover:bg-card/60 transition-colors"
|
||||
className="surface-card flex flex-col sm:flex-row gap-4 sm:items-center p-5 hover:bg-card/60 transition-all active:scale-[0.99]"
|
||||
>
|
||||
<div className="flex sm:flex-col items-center sm:items-start gap-3 sm:gap-1 sm:min-w-[110px] border-b sm:border-b-0 sm:border-r border-white/5 pb-3 sm:pb-0 sm:pr-4">
|
||||
<div className="flex items-center gap-2 text-primary font-bold">
|
||||
@@ -71,10 +77,10 @@ export const Schedule: React.FC = () => {
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
<div>
|
||||
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-1">
|
||||
<div className="text-[10px] font-bold text-primary uppercase tracking-wider mb-1 opacity-80">
|
||||
{lesson.type || 'Занятие'}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold leading-tight">{lesson.subject}</h3>
|
||||
<h3 className="text-lg font-bold leading-tight group-hover:text-primary transition-colors">{lesson.subject}</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useStore } from '../store/useStore';
|
||||
import { Palette, Moon, Sun, Monitor, LogOut, Settings2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Palette, Moon, Sun, Monitor, LogOut, Settings2, LifeBuoy, ChevronRight, HelpCircle, MessageCircle } from 'lucide-react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { TextField } from '../components/TextField';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -32,7 +32,7 @@ export const Settings: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page-enter max-w-3xl mx-auto space-y-8">
|
||||
<div className="page-enter max-w-3xl mx-auto space-y-8 pb-12">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
||||
<Settings2 size={24} />
|
||||
@@ -137,6 +137,45 @@ export const Settings: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Support Section Moved Here - Issue #10 */}
|
||||
<section className="surface-card space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2">
|
||||
<LifeBuoy size={20} className="text-primary" />
|
||||
Поддержка
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">Помощь и обратная связь</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Link to="/support" className="flex items-center justify-between p-4 rounded-2xl bg-muted/30 border border-transparent hover:border-primary/20 hover:bg-primary/5 transition-all group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
||||
<MessageCircle size={20} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold">Написать в поддержку</div>
|
||||
<div className="text-xs text-muted-foreground">Создать новое обращение</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={18} className="text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</Link>
|
||||
|
||||
<button className="flex items-center justify-between p-4 rounded-2xl bg-muted/30 border border-transparent hover:border-primary/20 hover:bg-primary/5 transition-all group">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
||||
<HelpCircle size={20} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-bold">Частые вопросы (FAQ)</div>
|
||||
<div className="text-xs text-muted-foreground">База знаний для студентов</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={18} className="text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -54,16 +54,17 @@
|
||||
|
||||
--md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1.0);
|
||||
--md-sys-motion-duration-medium: 200ms;
|
||||
--md-sys-motion-duration-long: 400ms;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
@apply border-border outline-ring/45;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
@apply h-full overflow-x-hidden;
|
||||
background-color: var(--md-sys-color-background);
|
||||
}
|
||||
|
||||
@@ -119,19 +120,24 @@
|
||||
}
|
||||
|
||||
.page-enter {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
animation: fadeIn var(--md-sys-motion-duration-long) var(--md-sys-motion-easing-emphasized);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateX(-10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
/* Navigation System */
|
||||
|
||||
.navigation-bar {
|
||||
@apply fixed left-0 right-0 bottom-0 h-[var(--nav-bar-height)] bg-background/80 backdrop-blur-xl flex justify-around items-center px-4 z-50 transition-transform duration-200 border-t border-white/5;
|
||||
@apply fixed left-0 right-0 bottom-0 h-[var(--nav-bar-height)] bg-background/80 backdrop-blur-xl flex justify-around items-center px-4 z-50 transition-transform duration-300 border-t border-white/5;
|
||||
}
|
||||
|
||||
.navigation-bar.hidden {
|
||||
@@ -147,7 +153,7 @@
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
@apply flex items-center no-underline text-muted-foreground h-14 px-3 relative transition-all duration-200 rounded-2xl mx-2;
|
||||
@apply flex items-center no-underline text-muted-foreground h-14 px-3 relative transition-all duration-300 rounded-2xl mx-2;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
@@ -155,23 +161,27 @@
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
@apply bg-sidebar-primary text-sidebar-primary-foreground;
|
||||
@apply bg-sidebar-primary text-sidebar-primary-foreground font-semibold;
|
||||
}
|
||||
|
||||
.navigation-bar .nav-item {
|
||||
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16;
|
||||
@apply flex-col justify-center flex-1 gap-1 p-0 mx-1 h-16 bg-transparent;
|
||||
}
|
||||
|
||||
.navigation-bar .nav-item.active {
|
||||
@apply bg-transparent text-primary;
|
||||
}
|
||||
|
||||
.nav-item-icon-wrapper {
|
||||
@apply w-12 h-8 flex items-center justify-center rounded-2xl transition-colors duration-200 relative;
|
||||
@apply w-12 h-8 flex items-center justify-center rounded-2xl transition-all duration-300 relative;
|
||||
}
|
||||
|
||||
.nav-item.active .nav-item-icon-wrapper {
|
||||
.navigation-bar .nav-item.active .nav-item-icon-wrapper {
|
||||
@apply bg-primary/10;
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
@apply text-[11px] font-medium whitespace-nowrap;
|
||||
@apply text-[11px] font-medium whitespace-nowrap transition-all duration-300;
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .nav-item {
|
||||
@@ -179,13 +189,13 @@
|
||||
}
|
||||
|
||||
.navigation-rail.expanded .nav-item-label {
|
||||
@apply text-sm ml-3 flex-1;
|
||||
@apply text-sm ml-3 flex-1 opacity-100;
|
||||
}
|
||||
|
||||
/* Submenu */
|
||||
|
||||
.submenu {
|
||||
@apply flex flex-col pl-14 pr-4 gap-1 mb-2;
|
||||
@apply flex flex-col pl-14 pr-4 gap-1 mb-2 overflow-hidden transition-all duration-300;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
@@ -199,3 +209,31 @@
|
||||
.submenu-item.active {
|
||||
@apply bg-primary/10 text-primary font-medium;
|
||||
}
|
||||
|
||||
/* Main Content with Padding Fix #4 */
|
||||
.main-content {
|
||||
@apply flex-1 p-6 transition-all duration-300 ease-[var(--md-sys-motion-easing-emphasized)];
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.navigation-bar { display: none; }
|
||||
.main-content {
|
||||
margin-left: var(--nav-rail-width);
|
||||
padding-top: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1240px) {
|
||||
.main-content.with-expanded-rail {
|
||||
margin-left: var(--nav-rail-expanded-width);
|
||||
padding-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 599px) {
|
||||
.main-content {
|
||||
padding-bottom: calc(var(--nav-bar-height) + 2rem);
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user