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:
2026-04-11 02:17:12 +03:00
parent 741e4f7db7
commit 24fb28f70a
6 changed files with 285 additions and 200 deletions

View File

@@ -37,8 +37,9 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
{/* Fix #7: Use wildcard for nested routes */}
<Route <Route
path="/schedule" path="/schedule/*"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<Schedule /> <Schedule />

View File

@@ -3,7 +3,6 @@ import { NavLink, useNavigate } from 'react-router-dom';
import { import {
CalendarDays, CalendarDays,
GraduationCap, GraduationCap,
LifeBuoy,
LogOut, LogOut,
Menu, Menu,
MessageSquareText, MessageSquareText,
@@ -47,7 +46,6 @@ const navItems: NavItemConfig[] = [
] ]
}, },
{ path: '/messages', label: 'Сообщения', icon: MessageSquareText, badgeKey: 'messages' }, { path: '/messages', label: 'Сообщения', icon: MessageSquareText, badgeKey: 'messages' },
{ path: '/support', label: 'Поддержка', icon: LifeBuoy },
]; ];
export const Navigation: React.FC = () => { export const Navigation: React.FC = () => {
@@ -80,7 +78,7 @@ export const Navigation: React.FC = () => {
if (navMode === 'bar') { if (navMode === 'bar') {
return ( return (
<nav className={cn("navigation-bar", scrollDir === 'down' && "hidden")}> <nav className={cn("navigation-bar", scrollDir === 'down' && "hidden")}>
{navItems.slice(0, 5).map((item) => ( {navItems.slice(0, 4).map((item) => (
<NavLink <NavLink
key={item.path} key={item.path}
to={item.path} to={item.path}
@@ -109,15 +107,15 @@ export const Navigation: React.FC = () => {
</div> </div>
{isExpanded && profile && ( {isExpanded && profile && (
<div className="mx-4 mb-6 p-3 rounded-2xl bg-sidebar-accent/30 flex items-center gap-3"> <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"> <Avatar className="h-10 w-10 border border-white/10 shadow-sm">
<AvatarFallback className="bg-primary text-primary-foreground text-xs"> <AvatarFallback className="bg-primary text-primary-foreground text-xs font-bold">
{getInitials(profile.fullName)} {getInitials(profile.fullName)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-sm font-semibold truncate">{profile.fullName}</div> <div className="text-sm font-semibold truncate leading-tight">{profile.fullName}</div>
<div className="text-[11px] opacity-60 truncate uppercase tracking-wider">{profile.group || 'БЕЗ ГРУППЫ'}</div> <div className="text-[10px] opacity-60 truncate uppercase tracking-widest mt-0.5">{profile.group || 'БЕЗ ГРУППЫ'}</div>
</div> </div>
</div> </div>
)} )}
@@ -131,7 +129,7 @@ export const Navigation: React.FC = () => {
<div key={item.label} className="nav-group mb-1"> <div key={item.label} className="nav-group mb-1">
<NavLink <NavLink
to={item.path} to={item.path}
className={({ isActive }) => cn("nav-item", isActive && "active")} className={({ isActive }) => cn("nav-item", (isActive || (hasChildren && isItemExpanded)) && "active")}
onClick={(e) => { onClick={(e) => {
if (hasChildren && isExpanded) { if (hasChildren && isExpanded) {
e.preventDefault(); e.preventDefault();
@@ -145,25 +143,30 @@ export const Navigation: React.FC = () => {
</div> </div>
{isExpanded && <span className="nav-item-label">{item.label}</span>} {isExpanded && <span className="nav-item-label">{item.label}</span>}
{isExpanded && hasChildren && ( {isExpanded && hasChildren && (
<div className="ml-auto"> <div className="ml-auto opacity-60">
{isItemExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />} {isItemExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</div> </div>
)} )}
</NavLink> </NavLink>
{isExpanded && isItemExpanded && hasChildren && ( <div
<div className="submenu"> className="submenu"
{item.children!.map(child => ( style={{
<NavLink maxHeight: isExpanded && isItemExpanded ? '200px' : '0',
key={child.path} opacity: isExpanded && isItemExpanded ? 1 : 0,
to={child.path} margin: isExpanded && isItemExpanded ? '4px 0 8px' : '0'
className={({ isActive }) => cn("submenu-item", isActive && "active")} }}
> >
{child.label} {item.children?.map(child => (
</NavLink> <NavLink
))} key={child.path}
</div> to={child.path}
)} className={({ isActive }) => cn("submenu-item", isActive && "active")}
>
{child.label}
</NavLink>
))}
</div>
</div> </div>
); );
})} })}
@@ -172,7 +175,7 @@ export const Navigation: React.FC = () => {
<div className="mt-auto p-4 space-y-2"> <div className="mt-auto p-4 space-y-2">
<NavLink <NavLink
to="/settings" 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"> <div className="flex items-center gap-3">
<MoreVertical size={18} /> <MoreVertical size={18} />
@@ -181,7 +184,7 @@ export const Navigation: React.FC = () => {
</NavLink> </NavLink>
<button <button
onClick={handleLogout} 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"> <div className="flex items-center gap-3">
<LogOut size={18} /> <LogOut size={18} />

View File

@@ -1,133 +1,62 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { api } from '../api/client'; import { api } from '../api/client';
import type { MessageListItem, MessageDetails } from '../types/api'; 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 { cn } from '@/lib/utils';
import { Badge } from '../components/ui/badge';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
export const Messages: React.FC = () => { export const Messages: React.FC = () => {
const [messages, setMessages] = useState<MessageListItem[]>([]); const [messages, setMessages] = useState<MessageListItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null); const [expandedId, setExpandedId] = useState<string | null>(null);
const [details, setDetails] = useState<MessageDetails | 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 [type, setType] = useState<'in' | 'out'>('in');
const [loading, setLoading] = useState(true); const [listLoading, setListLoading] = useState(true);
const [detailsLoading, setDetailsLoading] = useState(false);
useEffect(() => { useEffect(() => {
const fetchMessages = async () => { const fetchMessages = async () => {
setLoading(true); setListLoading(true);
setExpandedId(null);
try { try {
const data = await api.getMessages(type); const data = await api.getMessages(type);
setMessages(data); setMessages(data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} finally { } finally {
setLoading(false); setListLoading(false);
} }
}; };
fetchMessages(); fetchMessages();
}, [type]); }, [type]);
const handleSelectMessage = async (id: string) => { const toggleExpand = async (id: string) => {
setSelectedId(id); if (expandedId === id) {
setDetailsLoading(true); setExpandedId(null);
try { return;
const data = await api.getMessageDetails(id); }
setDetails(data);
} catch (err) { setExpandedId(id);
console.error(err);
} finally { if (!details[id]) {
setDetailsLoading(false); 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 ( return (
<div className="page-enter max-w-4xl mx-auto space-y-6"> <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"> <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="flex items-center gap-3">
<div className="p-2 rounded-xl bg-primary/10 text-primary"> <div className="p-2 rounded-xl bg-primary/10 text-primary">
<MessageSquareText size={24} /> <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"> <div className="flex p-1 rounded-2xl bg-muted/30 border border-white/5 w-fit">
<button <button
className={cn( 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" type === 'in' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
)} )}
onClick={() => setType('in')} onClick={() => setType('in')}
@@ -148,7 +77,7 @@ export const Messages: React.FC = () => {
</button> </button>
<button <button
className={cn( 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" type === 'out' ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
)} )}
onClick={() => setType('out')} onClick={() => setType('out')}
@@ -159,57 +88,126 @@ export const Messages: React.FC = () => {
</div> </div>
</div> </div>
{loading ? ( {listLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4"> <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" /> <Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
<p className="text-sm font-medium">Загрузка сообщений...</p> <p className="text-sm font-medium italic opacity-60">Загрузка сообщений...</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{messages.length === 0 ? ( {messages.length === 0 ? (
<div className="surface-card flex flex-col items-center justify-center py-20 text-muted-foreground gap-3"> <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-20" /> <Mail size={40} className="opacity-10" />
<p className="text-sm">Нет сообщений в данной категории</p> <p className="text-sm italic">Нет сообщений в данной категории</p>
</div> </div>
) : ( ) : (
messages.map((msg) => ( messages.map((msg, index) => {
<div const isExpanded = expandedId === msg.id;
key={msg.id} const isLoading = loadingIds.has(msg.id);
onClick={() => handleSelectMessage(msg.id)} const msgDetails = details[msg.id];
className="surface-card group hover:bg-card/60 cursor-pointer flex items-center gap-4 p-4"
> return (
<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"> <div
{type === 'in' ? <Mail size={22} /> : <Send size={22} />} key={msg.id}
</div> className={cn(
<div className="flex-1 min-w-0"> "surface-card flex flex-col p-0 overflow-hidden transition-all duration-300",
<div className="flex items-center justify-between gap-2 mb-1"> isExpanded ? "ring-2 ring-primary/20 bg-card/80" : "animate-in fade-in slide-in-from-bottom-2"
<span className="text-xs font-bold uppercase tracking-wider text-primary truncate"> )}
{msg.senderOrRecipient} style={{ animationDelay: `${index * 50}ms`, animationFillMode: 'both' }}
</span> >
<span className="text-[11px] text-muted-foreground whitespace-nowrap"> <div
{msg.date} onClick={() => toggleExpand(msg.id)}
</span> 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> </div>
<h3 className="text-base font-semibold truncate group-hover:text-primary transition-colors">
{msg.subject} <div className={cn(
</h3> "overflow-hidden transition-all duration-300 ease-in-out",
<div className="flex items-center gap-3 mt-2"> isExpanded ? "max-h-[1000px] border-t border-white/5" : "max-h-0"
{msg.hasFiles && ( )}>
<Badge variant="secondary" className="h-5 text-[10px] px-2 bg-primary/5 text-primary border-primary/10"> <div className="p-5 sm:p-8 space-y-6">
<Paperclip size={10} className="mr-1" /> {isLoading ? (
Вложения <div className="flex items-center justify-center py-8 gap-3 text-muted-foreground">
</Badge> <Loader2 size={18} className="animate-spin text-primary" />
)} <span className="text-sm italic">Загрузка содержания...</span>
<span className="text-xs text-muted-foreground truncate opacity-60"> </div>
Нажмите, чтобы прочитать полностью... ) : msgDetails ? (
</span> <>
<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> </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> </div>
)} )}

View File

@@ -1,14 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { api } from '../api/client'; import { api } from '../api/client';
import type { Schedule as ScheduleType } from '../types/api'; 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 = () => { export const Schedule: React.FC = () => {
const [schedule, setSchedule] = useState<ScheduleType | null>(null); const [schedule, setSchedule] = useState<ScheduleType | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const location = useLocation();
const isSession = location.pathname.includes('/session');
useEffect(() => { useEffect(() => {
const fetchSchedule = async () => { const fetchSchedule = async () => {
setLoading(true);
try { try {
const data = await api.getSchedule(); const data = await api.getSchedule();
setSchedule(data); setSchedule(data);
@@ -19,35 +23,37 @@ export const Schedule: React.FC = () => {
} }
}; };
fetchSchedule(); fetchSchedule();
}, []); }, [isSession]);
if (loading) return ( if (loading) return (
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground gap-4"> <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" /> <Loader2 className="w-8 h-8 text-primary/40 animate-spin" />
<p className="text-sm font-medium">Загрузка расписания...</p> <p className="text-sm font-medium italic opacity-60">Загрузка расписания...</p>
</div> </div>
); );
return ( return (
<div className="page-enter max-w-4xl mx-auto space-y-8"> <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"> <div className="p-2 rounded-xl bg-primary/10 text-primary">
<CalendarDays size={24} /> <CalendarDays size={24} />
</div> </div>
<h1 className="text-2xl font-bold tracking-tight">Расписание</h1> <h1 className="text-2xl font-bold tracking-tight">
{isSession ? 'Расписание сессии' : 'Расписание занятий'}
</h1>
</div> </div>
{!schedule || schedule.days.length === 0 ? ( {!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>
) : ( ) : (
<div className="space-y-10"> <div className="space-y-10">
{schedule.days.map((day, idx) => ( {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="flex items-center gap-4">
<div className="h-px flex-1 bg-white/5" /> <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} {day.date}
</h2> </h2>
<div className="h-px flex-1 bg-white/5" /> <div className="h-px flex-1 bg-white/5" />
@@ -57,7 +63,7 @@ export const Schedule: React.FC = () => {
{day.lessons.map((lesson, lIdx) => ( {day.lessons.map((lesson, lIdx) => (
<div <div
key={lIdx} 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 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"> <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 className="flex-1 space-y-3">
<div> <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 || 'Занятие'} {lesson.type || 'Занятие'}
</div> </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>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useStore } from '../store/useStore'; import { useStore } from '../store/useStore';
import { Palette, Moon, Sun, Monitor, LogOut, Settings2 } from 'lucide-react'; import { Palette, Moon, Sun, Monitor, LogOut, Settings2, LifeBuoy, ChevronRight, HelpCircle, MessageCircle } from 'lucide-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, Link } from 'react-router-dom';
import { TextField } from '../components/TextField'; import { TextField } from '../components/TextField';
import { Button } from '../components/ui/button'; import { Button } from '../components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -32,7 +32,7 @@ export const Settings: React.FC = () => {
}; };
return ( 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="flex items-center gap-3 mb-2">
<div className="p-2 rounded-xl bg-primary/10 text-primary"> <div className="p-2 rounded-xl bg-primary/10 text-primary">
<Settings2 size={24} /> <Settings2 size={24} />
@@ -137,6 +137,45 @@ export const Settings: React.FC = () => {
</div> </div>
</section> </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"> <section className="pt-4">
<Button <Button
variant="outline" variant="outline"

View File

@@ -54,16 +54,17 @@
--md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1.0); --md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1.0);
--md-sys-motion-duration-medium: 200ms; --md-sys-motion-duration-medium: 200ms;
--md-sys-motion-duration-long: 400ms;
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/45;
} }
html, html,
body { body {
min-height: 100%; @apply h-full overflow-x-hidden;
background-color: var(--md-sys-color-background); background-color: var(--md-sys-color-background);
} }
@@ -119,19 +120,24 @@
} }
.page-enter { .page-enter {
animation: fadeIn 0.3s ease-out; animation: fadeIn var(--md-sys-motion-duration-long) var(--md-sys-motion-easing-emphasized);
} }
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); } from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
/* Navigation System */ /* Navigation System */
.navigation-bar { .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 { .navigation-bar.hidden {
@@ -147,7 +153,7 @@
} }
.nav-item { .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 { .nav-item:hover {
@@ -155,23 +161,27 @@
} }
.nav-item.active { .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 { .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 { .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; @apply bg-primary/10;
} }
.nav-item-label { .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 { .navigation-rail.expanded .nav-item {
@@ -179,13 +189,13 @@
} }
.navigation-rail.expanded .nav-item-label { .navigation-rail.expanded .nav-item-label {
@apply text-sm ml-3 flex-1; @apply text-sm ml-3 flex-1 opacity-100;
} }
/* Submenu */ /* Submenu */
.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 { .submenu-item {
@@ -199,3 +209,31 @@
.submenu-item.active { .submenu-item.active {
@apply bg-primary/10 text-primary font-medium; @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;
}
}