first commit
This commit is contained in:
commit
4a589825c2
297 changed files with 33019 additions and 0 deletions
170
frontend/src/components/layouts/footer/Footer.astro
Normal file
170
frontend/src/components/layouts/footer/Footer.astro
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
|
||||
import SocialIcons from "@components/base/SocialIcons.astro";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const services = [
|
||||
{ name: "Административные дела", href: "/services/arbitration" },
|
||||
{ name: "Гражданские дела", href: "/services/civil" },
|
||||
{ name: "Семейные дела", href: "/services/family" },
|
||||
{ name: "Защита должников", href: "/services/debt-protection" },
|
||||
];
|
||||
|
||||
const menu = [
|
||||
{ name: "Главная", href: "/" },
|
||||
{ name: "О бюро", href: "/about" },
|
||||
{ name: "Отзывы", href: "/reviews" },
|
||||
{ name: "FAQ", href: "/faq" },
|
||||
];
|
||||
---
|
||||
|
||||
<footer
|
||||
class="bg-[#151b26] text-gray-400 py-16 border-t border-white/5 font-sans"
|
||||
>
|
||||
<div class="container mx-auto px-6 md:px-12 lg:px-16">
|
||||
<!-- Основная сетка: 4 колонки -->
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 lg:gap-8 mb-20"
|
||||
>
|
||||
<!-- Колонка 1: Лого и Инфо -->
|
||||
<div class="flex flex-col items-center md:items-start">
|
||||
<!-- Логотип -->
|
||||
<a href="/" class="flex items-center gap-3 mb-6 group">
|
||||
<div
|
||||
class="w-10 h-10 bg-[#1e2532] border border-white/10 flex items-center justify-center rounded-sm group-hover:border-[#bf9b58] transition-colors duration-300"
|
||||
>
|
||||
<!-- Иконка весов -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[#bf9b58]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"
|
||||
></path><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"
|
||||
></path><path d="M7 21h10"></path><path d="M12 3v18"></path><path
|
||||
d="M3 7h2v2H3z"></path><path d="M19 7h2v2h-2z"></path></svg
|
||||
>
|
||||
</div>
|
||||
<span class="text-xl font-black text-white uppercase tracking-tighter"
|
||||
>ADVOKAT086</span
|
||||
>
|
||||
</a>
|
||||
|
||||
<p
|
||||
class="text-sm leading-relaxed mb-8 max-w-xs text-gray-400 text-center md:text-left"
|
||||
>
|
||||
Ведущая адвокатская практика в Сургуте. Обеспечиваем правовую защиту
|
||||
бизнеса и граждан с 2009 года.
|
||||
</p>
|
||||
|
||||
<!-- Социальные иконки -->
|
||||
<SocialIcons variant="footer" />
|
||||
</div>
|
||||
|
||||
<!-- Колонка 2: Услуги -->
|
||||
<div class="text-center md:text-left">
|
||||
<h3
|
||||
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
|
||||
>
|
||||
Услуги
|
||||
</h3>
|
||||
<ul class="space-y-4">
|
||||
{
|
||||
services.map((item) => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-sm font-bold uppercase text-gray-300 hover:text-[#bf9b58] transition-colors duration-300 tracking-wide"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Колонка 3: Меню -->
|
||||
<div class="hidden md:block">
|
||||
<h3
|
||||
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
|
||||
>
|
||||
Меню
|
||||
</h3>
|
||||
<ul class="space-y-4">
|
||||
{
|
||||
menu.map((item) => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-sm font-bold uppercase text-gray-300 hover:text-[#bf9b58] transition-colors duration-300 tracking-wide"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Колонка 4: Контакты -->
|
||||
<div class="text-center md:text-left">
|
||||
<h3
|
||||
class="text-[#bf9b58] text-xs font-bold uppercase tracking-[0.2em] mb-8"
|
||||
>
|
||||
Контакты
|
||||
</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<p class="text-[10px] uppercase tracking-widest text-gray-500 mb-1">
|
||||
Телефон
|
||||
</p>
|
||||
<a
|
||||
href={CONTACT_CONSTANTS.phoneHref}
|
||||
class="text-white text-lg font-bold hover:text-[#bf9b58] transition-colors"
|
||||
>
|
||||
{CONTACT_CONSTANTS.phone}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-[10px] uppercase tracking-widest text-gray-500 mb-1">
|
||||
Адрес
|
||||
</p>
|
||||
<address class="text-white font-bold not-italic">
|
||||
{CONTACT_CONSTANTS.address}
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Нижняя линия: Copyright -->
|
||||
<div
|
||||
class="pt-8 border-t border-white/5 flex flex-col md:flex-row justify-between items-center gap-4"
|
||||
>
|
||||
<p class="text-[10px] uppercase tracking-[0.1em] text-gray-600">
|
||||
© {currentYear} ADVOKAT086. Все права защищены.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-8">
|
||||
<a
|
||||
href="/privacy-policy"
|
||||
class="text-[10px] uppercase tracking-[0.1em] text-gray-600 hover:text-white transition-colors"
|
||||
>
|
||||
Политика конфиденциальности
|
||||
</a>
|
||||
<a
|
||||
href="/legal-info"
|
||||
class="text-[10px] uppercase tracking-[0.1em] text-gray-600 hover:text-white transition-colors"
|
||||
>
|
||||
Правовая информация
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
444
frontend/src/components/layouts/header/Header.astro
Normal file
444
frontend/src/components/layouts/header/Header.astro
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
---
|
||||
import MobileMenu from "./MobileMenu.astro";
|
||||
import Button from "@components/base/Button.astro";
|
||||
import { CONTACT_CONSTANTS } from "@constants/constants.ts";
|
||||
|
||||
const { pathname } = Astro.url;
|
||||
|
||||
// Проверка авторизации будет на клиенте
|
||||
const navLinks = [
|
||||
{ name: "Главная", href: "/" },
|
||||
{
|
||||
name: "Услуги",
|
||||
href: "/services",
|
||||
children: [
|
||||
{
|
||||
name: "Административные дела",
|
||||
href: "/services/administrative",
|
||||
icon: "⚖️",
|
||||
},
|
||||
{
|
||||
name: "Защита должников",
|
||||
href: "/services/debt-protection",
|
||||
icon: "💳",
|
||||
},
|
||||
{
|
||||
name: "Арбитражные дела",
|
||||
href: "/services/arbitration",
|
||||
icon: "💼",
|
||||
},
|
||||
{
|
||||
name: "Уголовные дела",
|
||||
href: "/services/criminal",
|
||||
icon: "⚖️",
|
||||
},
|
||||
{ name: "Гражданские дела", href: "/services/civil", icon: "📋" },
|
||||
{ name: "Семейные дела", href: "/services/family", icon: "👨👩👧" },
|
||||
{ name: "Дела СВО", href: "/services/svo", icon: "🛡️" },
|
||||
],
|
||||
},
|
||||
{ name: "Кейсы", href: "/cases" },
|
||||
{ name: "Блог", href: "/blog" },
|
||||
{ name: "О Бюро", href: "/about" },
|
||||
{ name: "Контакты", href: "/contacts" },
|
||||
];
|
||||
|
||||
const rawLinks = navLinks.filter((link) => {
|
||||
if (pathname === "/" && link.href === "/") return false;
|
||||
return true;
|
||||
});
|
||||
---
|
||||
|
||||
<header
|
||||
id="main-header"
|
||||
class="sticky top-0 w-full py-4 px-4 md:px-8 lg:px-16 z-[100] bg-[#0a0f1c]/90 backdrop-blur-md border-b border-white/5 transition-all duration-300"
|
||||
>
|
||||
<div class="container px-4 md:px-8 mx-auto flex items-center justify-between">
|
||||
<!-- Логотип -->
|
||||
<a href="/" class="flex items-center group relative py-2">
|
||||
<div
|
||||
class="w-0 opacity-0 overflow-hidden transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)] group-hover:w-10 group-hover:opacity-100"
|
||||
>
|
||||
<div
|
||||
class="w-10 h-10 min-w-10 bg-[#bf9b58] flex items-center justify-center rounded"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-[#151b26]"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12c5.16-1.26 9-6.45 9-12V5zm0 4a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m5.13 12A9.7 9.7 0 0 1 12 20.92A9.7 9.7 0 0 1 6.87 17c-.34-.5-.63-1-.87-1.53c0-1.65 2.71-3 6-3s6 1.32 6 3c-.24.53-.53 1.03-.87 1.53"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)] pl-0 group-hover:pl-3"
|
||||
>
|
||||
<span
|
||||
class="text-xl md:text-2xl font-black tracking-tighter text-white uppercase leading-none"
|
||||
>
|
||||
ADVOKAT<span class="text-[#bf9b58]">086</span>
|
||||
</span>
|
||||
<span
|
||||
class="text-[10px] text-gray-400 tracking-[0.2em] uppercase hidden sm:block"
|
||||
>Юридическая защита</span
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Десктоп меню -->
|
||||
<nav class="hidden lg:flex items-center gap-8 xl:gap-12">
|
||||
{
|
||||
rawLinks.map((link) => (
|
||||
<div class="relative group">
|
||||
{link.children ? (
|
||||
<>
|
||||
<button
|
||||
class={`flex items-center gap-2 text-sm md:text-base font-bold uppercase tracking-wide transition-colors duration-300 py-2 ${link.active ? "text-[#bf9b58]" : "text-gray-300 hover:text-[#bf9b58]"}`}
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
{link.name}
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-300 ease-out group-hover:-rotate-180"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="absolute top-full left-1/2 -translate-x-1/2 pt-4 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 ease-out transform group-hover:translate-y-0 translate-y-2 min-w-[300px]">
|
||||
<div class="bg-[#0f1623]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl shadow-black/50 overflow-hidden p-2 relative">
|
||||
<div class="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 bg-[#0f1623] border-l border-t border-white/10 rotate-45" />
|
||||
<div class="relative space-y-1">
|
||||
{link.children.map((child) => (
|
||||
<a
|
||||
href={child.href}
|
||||
class={`flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all duration-200 ${pathname === child.href ? "bg-[#bf9b58]/10 text-[#bf9b58]" : "text-gray-300 hover:bg-white/5 hover:text-[#bf9b58]"}`}
|
||||
>
|
||||
{child.icon && (
|
||||
<span class="text-lg flex-shrink-0">
|
||||
{child.icon}
|
||||
</span>
|
||||
)}
|
||||
<span>{child.name}</span>
|
||||
{pathname === child.href && (
|
||||
<svg
|
||||
class="w-4 h-4 ml-auto text-[#bf9b58]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-t border-white/5">
|
||||
<a
|
||||
href={link.href}
|
||||
class="flex items-center justify-center gap-2 px-4 py-2 text-xs font-bold uppercase tracking-wider text-[#bf9b58] hover:text-white transition-colors duration-200"
|
||||
>
|
||||
Все услуги
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform group-hover:translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 8l4 4m0 0l-4 4m4-4H3"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
href={link.href}
|
||||
class={`text-sm md:text-base font-bold uppercase tracking-wide transition-colors duration-300 py-2 block ${link.active ? "text-[#bf9b58]" : "text-gray-300 hover:text-[#bf9b58]"}`}
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Блок авторизации - показывается/скрывается через JS -->
|
||||
<div id="auth-block" class="hidden xl:flex items-center gap-3">
|
||||
<!-- Кнопки для авторизованного пользователя -->
|
||||
<div id="auth-user-block" class="hidden items-center gap-3">
|
||||
<a
|
||||
href="/profile"
|
||||
class="w-10 h-10 rounded-full bg-[#bf9b58]/20 border border-[#bf9b58]/50 flex items-center justify-center hover:bg-[#bf9b58]/30 transition-colors group"
|
||||
title="Личный кабинет"
|
||||
>
|
||||
<span id="user-initial" class="text-white font-bold text-lg group-hover:text-[#bf9b58]"></span>
|
||||
</a>
|
||||
<button
|
||||
id="logout-btn"
|
||||
class="w-10 h-10 rounded-full border border-red-500/30 flex items-center justify-center hover:bg-red-500/10 transition-colors group"
|
||||
title="Выход"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 text-red-400 group-hover:text-red-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Блок для неавторизованного -->
|
||||
<div id="auth-guest-block" class="flex flex-col items-end">
|
||||
<button
|
||||
data-consultation-modal
|
||||
class="flex items-center gap-2 text-white font-bold text-lg hover:text-[#bf9b58] transition-colors cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
|
||||
/>
|
||||
</svg>
|
||||
{CONTACT_CONSTANTS.phone}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||
</span>
|
||||
<span class="text-[9px] text-gray-400 font-bold uppercase tracking-widest">
|
||||
Онлайн
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ИЗМЕНЕНИЕ: Кнопка-бургер. Увеличена область (w-12 h-12), добавлен rounded-full -->
|
||||
<button
|
||||
id="menu-toggle"
|
||||
class="lg:hidden relative w-12 h-12 rounded-full border border-transparent flex items-center justify-center z-[1000] cursor-pointer transition-all duration-300"
|
||||
aria-label="Меню"
|
||||
aria-expanded="false"
|
||||
onclick="
|
||||
const body = document.body;
|
||||
const isOpen = body.classList.toggle('menu-open');
|
||||
this.setAttribute('aria-expanded', isOpen);
|
||||
body.style.overflow = isOpen ? 'hidden' : '';
|
||||
"
|
||||
>
|
||||
<span class="burger-line line-1"></span>
|
||||
<span class="burger-line line-2"></span>
|
||||
<span class="burger-line line-3"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="scroll-progress"
|
||||
class="absolute bottom-0 left-0 h-0.5 bg-[#bf9b58] w-0 transition-all duration-75 ease-out z-50"
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<MobileMenu links={rawLinks} />
|
||||
|
||||
<style>
|
||||
/* Шапка становится прозрачной, чтобы просвечивал темный оверлей из меню */
|
||||
:global(body.menu-open) #main-header {
|
||||
background-color: transparent !important;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
/* ИЗМЕНЕНИЕ: Стили для активной кнопки (круглая подложка) */
|
||||
#menu-toggle[aria-expanded="true"] {
|
||||
background-color: #151b26; /* Темный фон под крестиком */
|
||||
border-color: rgba(255, 255, 255, 0.1); /* Легкая граница */
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); /* Тень для объема */
|
||||
}
|
||||
|
||||
.burger-line {
|
||||
position: absolute;
|
||||
width: 26px; /* Немного уменьшили ширину линий, чтобы они лучше вписывались в круг */
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
border-radius: 2px;
|
||||
transition: all 0.4s cubic-bezier(0.68, -0.6, 0.32, 1.6);
|
||||
}
|
||||
|
||||
.line-1 {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.line-2 {
|
||||
width: 18px;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
.line-3 {
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
/* Крестик */
|
||||
#menu-toggle[aria-expanded="true"] .line-1 {
|
||||
transform: translateY(0) rotate(45deg);
|
||||
background-color: #bf9b58; /* Делаем крестик золотым */
|
||||
width: 22px; /* Аккуратный крестик */
|
||||
}
|
||||
|
||||
#menu-toggle[aria-expanded="true"] .line-2 {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
#menu-toggle[aria-expanded="true"] .line-3 {
|
||||
transform: translateY(0) rotate(-45deg);
|
||||
background-color: #bf9b58;
|
||||
width: 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const initApp = () => {
|
||||
const progressBar = document.getElementById("scroll-progress");
|
||||
if (progressBar) {
|
||||
const updateScrollProgress = () => {
|
||||
const scrollTop =
|
||||
document.documentElement.scrollTop || document.body.scrollTop;
|
||||
const scrollHeight =
|
||||
document.documentElement.scrollHeight -
|
||||
document.documentElement.clientHeight;
|
||||
const scrollPercent =
|
||||
scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
|
||||
progressBar.style.width = `${scrollPercent}%`;
|
||||
};
|
||||
window.addEventListener("scroll", updateScrollProgress, {
|
||||
passive: true,
|
||||
});
|
||||
updateScrollProgress();
|
||||
}
|
||||
|
||||
// Проверка авторизации
|
||||
const checkAuth = async () => {
|
||||
const authBlock = document.getElementById("auth-block");
|
||||
const authUserBlock = document.getElementById("auth-user-block");
|
||||
const authGuestBlock = document.getElementById("auth-guest-block");
|
||||
const userInitialEl = document.getElementById("user-initial");
|
||||
|
||||
if (!authBlock || !authUserBlock || !authGuestBlock) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
authBlock.classList.remove("hidden");
|
||||
authUserBlock.classList.remove("hidden");
|
||||
authUserBlock.classList.add("flex");
|
||||
authGuestBlock.classList.add("hidden");
|
||||
authGuestBlock.classList.remove("flex");
|
||||
|
||||
// Отображаем первую букву имени пользователя
|
||||
if (userInitialEl && data.user?.name) {
|
||||
const firstLetter = data.user.name.trim().charAt(0).toUpperCase();
|
||||
userInitialEl.textContent = firstLetter;
|
||||
}
|
||||
} else {
|
||||
authBlock.classList.remove("hidden");
|
||||
authUserBlock.classList.add("hidden");
|
||||
authUserBlock.classList.remove("flex");
|
||||
authGuestBlock.classList.remove("hidden");
|
||||
authGuestBlock.classList.add("flex");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Header] Ошибка проверки авторизации:', error);
|
||||
authBlock.classList.remove("hidden");
|
||||
authUserBlock.classList.add("hidden");
|
||||
authGuestBlock.classList.remove("hidden");
|
||||
authGuestBlock.classList.add("flex");
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
|
||||
// Кнопка выхода
|
||||
const logoutBtn = document.getElementById("logout-btn");
|
||||
if (logoutBtn) {
|
||||
logoutBtn.classList.add("cursor-pointer");
|
||||
logoutBtn.addEventListener("click", async () => {
|
||||
console.log('[Header] Выход из системы');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Удаляем куку на клиенте
|
||||
document.cookie = 'pb_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
// Перенаправляем на главную
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Header] Ошибка при выходе:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("close-mobile-menu", () => {
|
||||
const toggleBtn = document.getElementById("menu-toggle");
|
||||
document.body.classList.remove("menu-open");
|
||||
document.body.style.overflow = "";
|
||||
if (toggleBtn) {
|
||||
toggleBtn.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
});
|
||||
|
||||
initApp();
|
||||
document.addEventListener("astro:page-load", initApp);
|
||||
</script>
|
||||
296
frontend/src/components/layouts/header/MobileMenu.astro
Normal file
296
frontend/src/components/layouts/header/MobileMenu.astro
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
---
|
||||
import Button from "@components/base/Button.astro";
|
||||
|
||||
const { links } = Astro.props;
|
||||
---
|
||||
|
||||
<div
|
||||
id="menu-overlay"
|
||||
class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[90] opacity-0 pointer-events-none transition-opacity duration-500"
|
||||
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- ИЗМЕНЕНИЕ: Ширина уменьшена с 85% до 80% (w-[80%]) -->
|
||||
<aside
|
||||
id="mobile-menu"
|
||||
class="fixed top-0 left-0 h-full w-[80%] max-w-[400px] bg-[#151b26] z-[105] py-8 px-6 flex flex-col overflow-y-auto shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 pointer-events-none opacity-5 bg-gradient-to-br from-white/5 to-transparent"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Логотип -->
|
||||
<div class="mobile-link mb-10 relative z-10" style="--delay: 0.1s">
|
||||
<span
|
||||
class="text-xl md:text-2xl font-black tracking-tighter text-white uppercase leading-none"
|
||||
>
|
||||
ADVOKAT<span class="text-[#bf9b58]">086</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Навигация -->
|
||||
<nav class="relative z-10 flex flex-col items-center space-y-5 w-full flex-1">
|
||||
{
|
||||
links.map((link: any, index: number) =>
|
||||
link.children ? (
|
||||
<div
|
||||
class="mobile-link w-full flex flex-col items-center"
|
||||
style={`--delay: ${0.2 + index * 0.08}s`}
|
||||
>
|
||||
<button
|
||||
class={`mobile-submenu-toggle text-xl md:text-2xl font-black uppercase tracking-wider transition-colors text-center flex items-center gap-2 ${link.active ? "text-[#bf9b58]" : "text-white hover:text-[#bf9b58]"}`}
|
||||
data-index={index}
|
||||
>
|
||||
{link.name}
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform duration-300 arrow-icon"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="mobile-submenu hidden mt-3 w-full space-y-2 overflow-hidden"
|
||||
data-index={index}
|
||||
>
|
||||
{link.children.map((child: any) => (
|
||||
<a
|
||||
href={child.href}
|
||||
class="block text-center text-base font-bold uppercase tracking-wider text-gray-300 hover:text-[#bf9b58] transition-colors py-1"
|
||||
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
|
||||
>
|
||||
<span class="mr-2">{child.icon}</span>
|
||||
{child.name}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={link.href}
|
||||
class="block text-center text-sm font-bold uppercase tracking-wider text-[#bf9b58] hover:text-white transition-colors mt-2 py-1"
|
||||
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
|
||||
>
|
||||
Все услуги →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
href={link.href}
|
||||
class="mobile-link text-xl md:text-2xl font-black uppercase tracking-wider transition-colors text-center"
|
||||
class:list={[
|
||||
link.active
|
||||
? "text-[#bf9b58]"
|
||||
: "text-white hover:text-[#bf9b58]",
|
||||
]}
|
||||
style={`--delay: ${0.2 + index * 0.08}s`}
|
||||
onclick="document.dispatchEvent(new CustomEvent('close-mobile-menu'))"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
),
|
||||
)
|
||||
}
|
||||
</nav>
|
||||
|
||||
<!-- Авторизация - управляется через JS -->
|
||||
<div
|
||||
id="mobile-auth-block"
|
||||
class="mobile-link w-full mt-8 flex justify-center gap-6 relative z-10 hidden"
|
||||
style="--delay: 0.6s"
|
||||
>
|
||||
<a
|
||||
href="/profile"
|
||||
class="w-10 h-10 rounded-full bg-[#bf9b58]/20 border border-[#bf9b58]/50 flex items-center justify-center hover:bg-[#bf9b58]/30 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 text-[#bf9b58]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
id="mobile-logout-btn"
|
||||
class="w-10 h-10 rounded-full border border-red-500/30 flex items-center justify-center hover:bg-red-500/10 transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 text-red-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Нижний блок -->
|
||||
<div
|
||||
class="relative z-10 w-full mt-auto pt-6 mobile-link"
|
||||
style="--delay: 0.7s"
|
||||
>
|
||||
<div class="h-[1px] bg-white/10 w-full mb-4"></div>
|
||||
<div id="mobile-guest-block" class="space-y-0.5 text-center hidden">
|
||||
<p class="text-xs font-bold text-[#bf9b58] uppercase tracking-[0.2em]">
|
||||
Срочная связь
|
||||
</p>
|
||||
<a
|
||||
href="tel:+79222538375"
|
||||
class="block text-lg font-bold text-white hover:text-[#bf9b58] transition-colors"
|
||||
>
|
||||
+7 (922) 253-83-75
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
#mobile-menu {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
:global(body.menu-open) #mobile-menu {
|
||||
transform: translateX(0);
|
||||
}
|
||||
:global(body.menu-open) #menu-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.mobile-link {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition:
|
||||
opacity 0.4s ease-out,
|
||||
transform 0.4s ease-out;
|
||||
transition-delay: var(--delay);
|
||||
}
|
||||
:global(body.menu-open) .mobile-link {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.arrow-icon.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const setupSubmenus = () => {
|
||||
const toggles = document.querySelectorAll(".mobile-submenu-toggle");
|
||||
toggles.forEach((toggle) => {
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const index = toggle.getAttribute("data-index");
|
||||
const submenu = document.querySelector(
|
||||
`.mobile-submenu[data-index="${index}"]`,
|
||||
);
|
||||
const arrow = toggle.querySelector(".arrow-icon");
|
||||
|
||||
if (submenu) {
|
||||
const isHidden = submenu.classList.contains("hidden");
|
||||
document
|
||||
.querySelectorAll(".mobile-submenu")
|
||||
.forEach((el) => el.classList.add("hidden"));
|
||||
document
|
||||
.querySelectorAll(".arrow-icon")
|
||||
.forEach((el) => el.classList.remove("rotated"));
|
||||
|
||||
if (isHidden) {
|
||||
submenu.classList.remove("hidden");
|
||||
arrow?.classList.add("rotated");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const setupLogout = () => {
|
||||
const btn = document.getElementById("mobile-logout-btn");
|
||||
if (btn) {
|
||||
btn.classList.add("cursor-pointer");
|
||||
btn.addEventListener("click", async () => {
|
||||
console.log('[MobileMenu] Выход из системы');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Удаляем куку на клиенте
|
||||
document.cookie = 'pb_auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
// Перенаправляем на главную
|
||||
window.location.href = '/';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MobileMenu] Ошибка при выходе:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Проверка авторизации
|
||||
const checkAuth = async () => {
|
||||
const mobileAuthBlock = document.getElementById("mobile-auth-block");
|
||||
const mobileGuestBlock = document.getElementById("mobile-guest-block");
|
||||
|
||||
if (!mobileAuthBlock || !mobileGuestBlock) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/me', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
// Пользователь авторизован
|
||||
mobileAuthBlock.classList.remove("hidden");
|
||||
mobileAuthBlock.classList.add("flex");
|
||||
mobileGuestBlock.classList.add("hidden");
|
||||
mobileGuestBlock.classList.remove("flex");
|
||||
} else {
|
||||
// Пользователь не авторизован
|
||||
mobileAuthBlock.classList.add("hidden");
|
||||
mobileAuthBlock.classList.remove("flex");
|
||||
mobileGuestBlock.classList.remove("hidden");
|
||||
mobileGuestBlock.classList.add("flex");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MobileMenu] Ошибка проверки авторизации:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Вызываем проверку авторизации
|
||||
checkAuth();
|
||||
|
||||
setupSubmenus();
|
||||
setupLogout();
|
||||
document.addEventListener("astro:after-swap", () => {
|
||||
setupSubmenus();
|
||||
setupLogout();
|
||||
checkAuth();
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue