initial
This commit is contained in:
77
frontend/src/components/layout/Layout.tsx
Normal file
77
frontend/src/components/layout/Layout.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Outlet, Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { Gamepad2, LogOut, Trophy, User } from 'lucide-react'
|
||||
|
||||
export function Layout() {
|
||||
const { user, isAuthenticated, logout } = useAuthStore()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="bg-gray-800 border-b border-gray-700">
|
||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Link to="/" className="flex items-center gap-2 text-xl font-bold text-white">
|
||||
<Gamepad2 className="w-8 h-8 text-primary-500" />
|
||||
<span>Игровой Марафон</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-4">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link
|
||||
to="/marathons"
|
||||
className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors"
|
||||
>
|
||||
<Trophy className="w-5 h-5" />
|
||||
<span>Марафоны</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-3 ml-4 pl-4 border-l border-gray-700">
|
||||
<div className="flex items-center gap-2 text-gray-300">
|
||||
<User className="w-5 h-5" />
|
||||
<span>{user?.nickname}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 text-gray-400 hover:text-white transition-colors"
|
||||
title="Выйти"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link to="/login" className="text-gray-300 hover:text-white transition-colors">
|
||||
Войти
|
||||
</Link>
|
||||
<Link to="/register" className="btn btn-primary">
|
||||
Регистрация
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 container mx-auto px-4 py-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-800 border-t border-gray-700 py-4">
|
||||
<div className="container mx-auto px-4 text-center text-gray-500 text-sm">
|
||||
Игровой Марафон © {new Date().getFullYear()}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
frontend/src/components/ui/Button.tsx
Normal file
40
frontend/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { forwardRef, type ButtonHTMLAttributes } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={disabled || isLoading}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
'bg-primary-600 hover:bg-primary-700 text-white': variant === 'primary',
|
||||
'bg-gray-700 hover:bg-gray-600 text-white': variant === 'secondary',
|
||||
'bg-red-600 hover:bg-red-700 text-white': variant === 'danger',
|
||||
'bg-transparent hover:bg-gray-800 text-gray-300': variant === 'ghost',
|
||||
'px-3 py-1.5 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-base': size === 'md',
|
||||
'px-6 py-3 text-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Button.displayName = 'Button'
|
||||
54
frontend/src/components/ui/Card.tsx
Normal file
54
frontend/src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Card({ children, className }: CardProps) {
|
||||
return (
|
||||
<div className={clsx('bg-gray-800 rounded-xl p-6 shadow-lg', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CardHeaderProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CardHeader({ children, className }: CardHeaderProps) {
|
||||
return (
|
||||
<div className={clsx('mb-4', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CardTitleProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CardTitle({ children, className }: CardTitleProps) {
|
||||
return (
|
||||
<h3 className={clsx('text-xl font-bold text-white', className)}>
|
||||
{children}
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
interface CardContentProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CardContent({ children, className }: CardContentProps) {
|
||||
return (
|
||||
<div className={clsx('text-gray-300', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
36
frontend/src/components/ui/Input.tsx
Normal file
36
frontend/src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { forwardRef, type InputHTMLAttributes } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, label, error, id, ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-300 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={clsx(
|
||||
'w-full px-4 py-2 bg-gray-800 border rounded-lg text-white placeholder-gray-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
||||
'transition-colors',
|
||||
error ? 'border-red-500' : 'border-gray-700',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Input.displayName = 'Input'
|
||||
3
frontend/src/components/ui/index.ts
Normal file
3
frontend/src/components/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Button } from './Button'
|
||||
export { Input } from './Input'
|
||||
export { Card, CardHeader, CardTitle, CardContent } from './Card'
|
||||
Reference in New Issue
Block a user