Postgres/Redis/RabbitMQ/MinIO/Ollama теперь общие серверы сети (адреса в .env), локально в докере остаётся только Elasticsearch + app-сервисы. Старый docker-compose.yml (полностью автономный стек) сохранён как docker-compose.selfhosted.yml.example на случай отдельного GPU-сервера в будущем. Добавлен docker-compose.prod.yml (app + свой nginx с TLS, собирающий фронтенд в статику). Makefile переписан под три режима (dev/test-up/prod). Мелкая чистка неиспользуемых импортов во фронтенде.
152 lines
5.8 KiB
TypeScript
152 lines
5.8 KiB
TypeScript
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||
import { GraduationCap, Search, BookOpen, Upload, LayoutDashboard, LogOut } from 'lucide-react';
|
||
import { clsx } from 'clsx';
|
||
import { useAuthStore } from '../store/auth';
|
||
import { EmailVerificationModal } from './EmailVerificationModal';
|
||
|
||
interface LayoutProps {
|
||
children: React.ReactNode;
|
||
}
|
||
|
||
export function Layout({ children }: LayoutProps) {
|
||
const { isAuthenticated, user, logout } = useAuthStore();
|
||
const navigate = useNavigate();
|
||
|
||
const handleLogout = () => {
|
||
logout();
|
||
navigate('/');
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Навигация */}
|
||
<nav className="bg-white border-b border-gray-100 sticky top-0 z-50">
|
||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex items-center justify-between h-16">
|
||
{/* Логотип */}
|
||
<Link
|
||
to="/"
|
||
className="flex items-center gap-2.5 text-gray-900 hover:text-brand-600 transition-colors"
|
||
>
|
||
<div className="p-1.5 bg-brand-600 rounded-lg">
|
||
<GraduationCap className="w-5 h-5 text-white" />
|
||
</div>
|
||
<span className="font-semibold text-base hidden sm:block">Академический помощник</span>
|
||
<span className="font-semibold text-base sm:hidden">АкадПомощник</span>
|
||
</Link>
|
||
|
||
{/* Центральная навигация */}
|
||
<div className="flex items-center gap-1">
|
||
<NavLink
|
||
to="/search"
|
||
className={({ isActive }) =>
|
||
clsx(
|
||
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||
isActive
|
||
? 'bg-brand-50 text-brand-700'
|
||
: 'text-gray-600 hover:bg-gray-100'
|
||
)
|
||
}
|
||
>
|
||
<Search className="w-4 h-4" />
|
||
<span className="hidden md:block">Поиск</span>
|
||
</NavLink>
|
||
|
||
<NavLink
|
||
to="/check"
|
||
className={({ isActive }) =>
|
||
clsx(
|
||
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||
isActive
|
||
? 'bg-brand-50 text-brand-700'
|
||
: 'text-gray-600 hover:bg-gray-100'
|
||
)
|
||
}
|
||
>
|
||
<Upload className="w-4 h-4" />
|
||
<span className="hidden md:block">Плагиат</span>
|
||
</NavLink>
|
||
|
||
<NavLink
|
||
to="/bibliography"
|
||
className={({ isActive }) =>
|
||
clsx(
|
||
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||
isActive
|
||
? 'bg-brand-50 text-brand-700'
|
||
: 'text-gray-600 hover:bg-gray-100'
|
||
)
|
||
}
|
||
>
|
||
<BookOpen className="w-4 h-4" />
|
||
<span className="hidden md:block">Библиография</span>
|
||
</NavLink>
|
||
</div>
|
||
|
||
{/* Авторизация */}
|
||
<div className="flex items-center gap-2">
|
||
{isAuthenticated ? (
|
||
<>
|
||
<NavLink
|
||
to="/cabinet"
|
||
className={({ isActive }) =>
|
||
clsx(
|
||
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||
isActive
|
||
? 'bg-brand-50 text-brand-700'
|
||
: 'text-gray-600 hover:bg-gray-100'
|
||
)
|
||
}
|
||
>
|
||
<LayoutDashboard className="w-4 h-4" />
|
||
<span className="hidden md:block">Кабинет</span>
|
||
</NavLink>
|
||
<div className="flex items-center gap-2 pl-2 border-l border-gray-100">
|
||
<div className="flex items-center gap-1.5">
|
||
<div className="w-8 h-8 bg-brand-100 rounded-full flex items-center justify-center">
|
||
<span className="text-xs font-semibold text-brand-700">
|
||
{user?.name?.[0]?.toUpperCase() || 'U'}
|
||
</span>
|
||
</div>
|
||
<span className="text-sm text-gray-600 hidden lg:block">{user?.name}</span>
|
||
</div>
|
||
<button
|
||
onClick={handleLogout}
|
||
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||
title="Выйти"
|
||
>
|
||
<LogOut className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Link
|
||
to="/login"
|
||
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||
>
|
||
Войти
|
||
</Link>
|
||
<Link
|
||
to="/register"
|
||
className="px-4 py-2 text-sm font-medium bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
|
||
>
|
||
Регистрация
|
||
</Link>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
{/* Основной контент */}
|
||
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{children}
|
||
</main>
|
||
|
||
<EmailVerificationModal />
|
||
</div>
|
||
);
|
||
}
|