Files
links/frontend/linktree-frontend/src/app/components/LayoutWrapper.tsx.bak

303 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/LayoutWrapper.tsx
'use client'
import React, { ReactNode, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import Script from 'next/script'
import { useLocale } from '../contexts/LocaleContext'
import { useTheme } from '../contexts/ThemeContext'
import ThemeToggle from './ThemeToggle'
import LanguageSelector from './LanguageSelector'
import '../layout.css'
interface User {
id: number
username: string
email: string
full_name: string
avatar: string | null
}
export function LayoutWrapper({ children }: { children: ReactNode }) {
const pathname = usePathname() || ''
const isPublicUserPage = /^\/[^\/]+$/.test(pathname)
const [user, setUser] = useState<User | null>(null)
const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
const { t } = useLocale()
// При монтировании пробуем загрузить профиль
useEffect(() => {
const checkAuth = async () => {
const token = localStorage.getItem('token')
if (token) {
try {
const response = await fetch('/api/auth/user', {
headers: { Authorization: `Bearer ${token}` },
})
if (response.ok) {
const data = await response.json()
setUser({
id: data.id,
username: data.username,
email: data.email,
full_name: data.full_name || '',
avatar: data.avatar
})
} else {
// Токен недействителен
localStorage.removeItem('token')
setUser(null)
}
} catch (error) {
console.error('Auth check failed:', error)
localStorage.removeItem('token')
setUser(null)
}
}
setIsLoading(false)
}
checkAuth()
}, [])
const handleLogout = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('token')
}
setUser(null)
router.push('/')
}
return (
<>
{/* Шапка отображается на всех страницах кроме публичных /[username] */}
{!isPublicUserPage && (
<nav className="navbar navbar-expand-lg navbar-light bg-light fixed-top shadow-sm border-bottom">
<div className="container">
<Link href="/" className="navbar-brand d-flex align-items-center">
<Image
src="/assets/img/CAT.png"
alt="CatLink"
width={32}
height={32}
className="me-2"
/>
<span className="fw-bold">CatLink</span>
</Link>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navcol-1"
aria-controls="navcol-1"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navcol-1">
{/* Левое меню */}
<ul className="navbar-nav me-auto">
{user && (
<>
<li className="nav-item">
<Link href="/dashboard" className="nav-link">
<i className="bi bi-speedometer2 me-1"></i>
Дашборд
</Link>
</li>
<li className="nav-item">
<Link href="/profile" className="nav-link">
<i className="bi bi-person-gear me-1"></i>
Профиль
</Link>
</li>
</>
)}
</ul>
{/* Правое меню - всегда отображается */}
<div className="d-flex align-items-center gap-2">
{/* Переключатели темы и языка - всегда видны */}
<ThemeToggle />
<LanguageSelector />
{/* Блок авторизации */}
{isLoading ? (
<div className="spinner-border spinner-border-sm ms-2" role="status">
<span className="visually-hidden">Загрузка...</span>
</div>
) : !user ? (
<div className="d-flex gap-2 ms-2">
<Link href="/auth/login" className="btn btn-outline-primary btn-sm">
<i className="bi bi-box-arrow-in-right me-1"></i>
<span className="d-none d-sm-inline">Вход</span>
</Link>
<Link href="/auth/register" className="btn btn-primary btn-sm">
<i className="bi bi-person-plus me-1"></i>
<span className="d-none d-sm-inline">Регистрация</span>
</Link>
</div>
) : (
<div className="dropdown ms-2">
<button
className="btn btn-outline-secondary dropdown-toggle d-flex align-items-center"
type="button"
id="userDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{user.avatar ? (
<Image
src={
user.avatar.startsWith('http')
? user.avatar
: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}${user.avatar}`
}
alt="Avatar"
width={24}
height={24}
className="rounded-circle me-2"
/>
) : (
<i className="bi bi-person-circle me-2"></i>
)}
<span className="d-none d-md-inline">
{user.full_name?.trim() || user.username}
</span>
</button>
<ul className="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<Link href="/dashboard" className="dropdown-item">
<i className="bi bi-speedometer2 me-2"></i>
Дашборд
</Link>
</li>
<li>
<Link href="/profile" className="dropdown-item">
<i className="bi bi-person-gear me-2"></i>
Профиль
</Link>
</li>
<li><hr className="dropdown-divider" /></li>
<li>
<button
onClick={handleLogout}
className="dropdown-item text-danger"
>
<i className="bi bi-box-arrow-right me-2"></i>
Выход
</button>
</li>
</ul>
</div>
)}
</div>
</div>
</div>
</nav>
)}
{/* Отступ для фиксированного навбара */}
{!isPublicUserPage && <div style={{ height: '76px' }} />}
aria-expanded="false"
>
<Image
src={
user.avatar && user.avatar.startsWith('http')
? user.avatar
: user.avatar
? `${process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'}${user.avatar}`
: '/assets/img/avatar-dhg.png'
}
alt="Avatar"
width={32}
height={32}
className="rounded-circle me-2"
/>
<span className="text-dark fw-medium d-none d-md-inline">
{user.full_name?.trim() || user.username}
</span>
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<Link href="/profile" className="dropdown-item">
<i className="fas fa-user me-2"></i>
{t('profile.edit')}
</Link>
</li>
<li>
<Link href="/dashboard" className="dropdown-item">
<i className="fas fa-tachometer-alt me-2"></i>
{t('dashboard.title')}
</Link>
</li>
<li><hr className="dropdown-divider" /></li>
<li>
<button
onClick={handleLogout}
className="dropdown-item text-danger"
>
<i className="fas fa-sign-out-alt me-2"></i>
{t('common.logout')}
</button>
</li>
</ul>
</div>
)}
</div>
</div>
</div>
</nav>
)}
{/* отступ, чтобы контент не прятался под фиксированным хедером */}
{!isPublicUserPage && <div className="navbar-spacing" />}
{children}
{/* Подвал не выводим на публичных страницах */}
{!isPublicUserPage && (
<footer className="bg-light footer border-top mt-5">
<div className="container py-4">
<div className="row">
<div className="col-lg-6 text-center text-lg-start mb-2 mb-lg-0">
<ul className="list-inline mb-1">
<li className="list-inline-item"><Link href="#">About</Link></li>
<li className="list-inline-item"></li>
<li className="list-inline-item"><Link href="#">Contact</Link></li>
<li className="list-inline-item"></li>
<li className="list-inline-item"><Link href="#">Terms of Use</Link></li>
<li className="list-inline-item"></li>
<li className="list-inline-item"><Link href="#">Privacy Policy</Link></li>
</ul>
<p className="text-muted small mb-0">© CatLink 2025</p>
</div>
<div className="col-lg-6 text-center text-lg-end">
<ul className="list-inline mb-0">
<li className="list-inline-item"><Link href="#"><i className="fa fa-facebook fa-lg fa-fw"></i></Link></li>
<li className="list-inline-item"><Link href="#"><i className="fa fa-twitter fa-lg fa-fw"></i></Link></li>
<li className="list-inline-item"><Link href="#"><i className="fa fa-instagram fa-lg fa-fw"></i></Link></li>
</ul>
</div>
</div>
</div>
</footer>
)}
{/* Bootstrap JS */}
{/* Bootstrap JS: load after React hydrates to avoid DOM mutations during hydration */}
<Script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
strategy="afterInteractive"
/>
<Script src="/assets/js/bs-init.js" strategy="afterInteractive" />
</>
)
}