Files
social-media/frontend/src/static/components/LandingSiteMenu.vue

437 lines
13 KiB
Vue

<template>
<header class="site-menu">
<div class="site-menu-inner">
<router-link
class="site-brand"
to="/"
>
<span class="site-brand-mark">S</span>
<span class="site-brand-heading">
<span class="site-brand-text-wrap">
<span class="site-brand-text">Socialize</span>
<span
class="site-brand-stage-badge"
:aria-label="t('nav.brandStageLabel')"
>
<span>{{ t('nav.brandStage') }}</span>
</span>
</span>
</span>
</router-link>
<nav
class="site-nav"
:aria-label="t('public.nav.ariaLabel')"
>
<div
ref="productMenuRef"
class="site-nav-group site-nav-product"
@pointerenter="openProductMenu"
@pointerleave="scheduleProductMenuClose"
@focusin="openProductMenu"
@focusout="scheduleProductMenuClose"
>
<button
type="button"
:aria-expanded="isProductMenuOpen"
aria-haspopup="true"
>
{{ t('public.nav.product') }}
</button>
<div
class="site-product-panel"
:class="{ open: isProductMenuOpen }"
>
<router-link
v-for="feature in productFeatureItems"
:key="feature.slug"
class="site-product-link"
:to="`/product/${feature.slug}`"
@click="closeProductMenu"
>
<span class="site-product-icon">
<svg
viewBox="0 0 24 24"
aria-hidden="true"
>
<path :d="feature.icon" />
</svg>
</span>
<span>
<strong>{{ t(`public.features.${feature.slug}.title`) }}</strong>
<small>{{ t(`public.features.${feature.slug}.description`) }}</small>
</span>
</router-link>
</div>
</div>
<router-link to="/pricing">{{ t('public.nav.pricing') }}</router-link>
<div
ref="resourcesMenuRef"
class="site-nav-group"
@pointerenter="openResourcesMenu"
@pointerleave="scheduleResourcesMenuClose"
@focusin="openResourcesMenu"
@focusout="scheduleResourcesMenuClose"
>
<button
type="button"
:aria-expanded="isResourcesMenuOpen"
aria-haspopup="true"
@click="toggleResourcesMenu"
>
{{ t('public.nav.resources') }}
</button>
<div
class="site-nav-menu"
:class="{ open: isResourcesMenuOpen }"
>
<router-link
to="/blogs"
@click="closeResourcesMenu"
>
{{ t('public.nav.blogs') }}
</router-link>
<router-link
to="/guides"
@click="closeResourcesMenu"
>
{{ t('public.nav.guides') }}
</router-link>
</div>
</div>
</nav>
<div
class="site-language-toggle"
:aria-label="t('public.nav.language')"
>
<button
type="button"
:class="{ active: currentLocale === 'en' }"
@click="setLocale('en')"
>
En
</button>
<button
type="button"
:class="{ active: currentLocale === 'fr' }"
@click="setLocale('fr')"
>
Fr
</button>
</div>
<router-link
class="site-login"
:to="authLink"
>
{{ authLabel }}
</router-link>
</div>
</header>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { productFeatureItems } from '@/static/productFeatures.js';
const allowedLocales = ['en', 'fr'];
const localeStorageKey = 'user-locale';
const authStore = useAuthStore();
const { locale, t } = useI18n();
const isProductMenuOpen = ref(false);
const isResourcesMenuOpen = ref(false);
const productMenuRef = ref(null);
const resourcesMenuRef = ref(null);
let productMenuCloseTimer = null;
let resourcesMenuCloseTimer = null;
const authLink = computed(() =>
authStore.isAuthenticated
? '/app/dashboard'
: '/login'
);
const authLabel = computed(() =>
authStore.isAuthenticated
? t('public.nav.openApp')
: t('public.nav.login')
);
const currentLocale = computed(() =>
allowedLocales.includes(locale.value) ? locale.value : 'en'
);
function setLocale(nextLocale) {
if (!allowedLocales.includes(nextLocale)) {
return;
}
locale.value = nextLocale;
if (typeof window !== 'undefined') {
window.sessionStorage.setItem(localeStorageKey, nextLocale);
}
}
function clearProductMenuCloseTimer() {
if (productMenuCloseTimer === null) {
return;
}
window.clearTimeout(productMenuCloseTimer);
productMenuCloseTimer = null;
}
function clearResourcesMenuCloseTimer() {
if (resourcesMenuCloseTimer === null) {
return;
}
window.clearTimeout(resourcesMenuCloseTimer);
resourcesMenuCloseTimer = null;
}
function openProductMenu() {
clearProductMenuCloseTimer();
isProductMenuOpen.value = true;
}
function openResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = true;
}
function closeProductMenu() {
clearProductMenuCloseTimer();
isProductMenuOpen.value = false;
}
function closeResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = false;
}
function toggleResourcesMenu() {
clearResourcesMenuCloseTimer();
isResourcesMenuOpen.value = !isResourcesMenuOpen.value;
}
function scheduleProductMenuClose() {
clearProductMenuCloseTimer();
productMenuCloseTimer = window.setTimeout(() => {
const activeElement = document.activeElement;
if (productMenuRef.value?.contains(activeElement)) {
return;
}
closeProductMenu();
}, 120);
}
function scheduleResourcesMenuClose() {
clearResourcesMenuCloseTimer();
resourcesMenuCloseTimer = window.setTimeout(() => {
const activeElement = document.activeElement;
if (resourcesMenuRef.value?.contains(activeElement)) {
return;
}
closeResourcesMenu();
}, 120);
}
onMounted(() => {
const storedLocale = window.sessionStorage.getItem(localeStorageKey);
if (allowedLocales.includes(storedLocale)) {
locale.value = storedLocale;
}
});
onBeforeUnmount(() => {
clearProductMenuCloseTimer();
clearResourcesMenuCloseTimer();
});
</script>
<style scoped>
.site-menu {
@apply sticky top-0 z-30 w-full;
background: rgba(255, 250, 242, 0.9);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
}
.site-menu-inner {
@apply mx-auto flex w-full max-w-7xl items-center justify-between gap-4 px-5 py-4 md:px-8;
}
.site-brand {
@apply flex min-w-0 items-start gap-3 no-underline;
color: #172033;
}
.site-brand-mark {
@apply flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-2xl text-base font-black;
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
color: #fffaf2;
}
.site-brand-heading {
@apply flex min-w-0 items-center;
}
.site-brand-text-wrap {
@apply relative inline-flex min-w-0 items-center;
}
.site-brand-text {
@apply truncate text-lg font-black uppercase tracking-[0.18em];
line-height: 2.5rem;
}
.site-brand-stage-badge {
@apply absolute inline-flex h-4 items-center rounded-sm border px-1.5 text-[0.55rem] font-black uppercase tracking-[0.08em];
background: rgba(255, 231, 199, 0.46);
border-color: rgba(242, 179, 107, 0.38);
color: #925000;
left: 0;
line-height: 1;
top: calc(100% - 0.45rem);
}
.site-nav {
@apply hidden items-center justify-center gap-2 sm:flex;
}
.site-nav a {
@apply rounded-[0.65rem] px-4 py-2 text-sm font-semibold no-underline transition-colors;
color: #44516a;
}
.site-nav a:hover,
.site-nav-group:hover > button {
background: rgba(23, 32, 51, 0.06);
color: #172033;
}
.site-nav-group {
@apply relative;
}
.site-nav-group > button {
@apply rounded-[0.65rem] px-4 py-2 text-sm font-semibold transition-colors;
color: #44516a;
}
.site-nav-menu {
@apply invisible absolute left-0 top-[calc(100%+0.5rem)] flex min-w-36 flex-col gap-1 rounded-[1rem] border p-2 opacity-0 transition-opacity;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
}
.site-nav-group:hover .site-nav-menu,
.site-nav-group:focus-within .site-nav-menu,
.site-nav-menu.open {
@apply visible opacity-100;
}
.site-nav-menu a {
@apply rounded-[0.75rem] px-3 py-2;
}
.site-product-panel {
@apply invisible absolute left-1/2 top-full grid w-[min(56rem,calc(100vw-2rem))] -translate-x-1/2 grid-cols-3 gap-2 rounded-[1.25rem] border p-3 opacity-0 transition-opacity;
background: rgba(255, 255, 255, 0.98);
border-color: rgba(23, 32, 51, 0.08);
box-shadow: 0 24px 60px rgba(23, 32, 51, 0.14);
}
.site-nav-product:hover .site-product-panel,
.site-nav-product:focus-within .site-product-panel,
.site-product-panel.open {
@apply visible opacity-100;
}
.site-product-link {
@apply grid grid-cols-[2.5rem_1fr] gap-3 rounded-[0.9rem] p-3 no-underline transition-colors;
}
.site-product-link:hover {
background: rgba(23, 32, 51, 0.05);
}
.site-product-icon {
@apply flex h-10 w-10 items-center justify-center rounded-[0.75rem];
background: rgba(15, 118, 110, 0.1);
color: #0f766e;
}
.site-product-icon svg {
@apply h-5 w-5;
fill: currentColor;
}
.site-product-link strong {
@apply block text-sm font-black leading-5;
color: #172033;
}
.site-product-link small {
@apply mt-1 block text-xs font-medium leading-5;
color: #44516a;
}
.site-language-toggle {
@apply flex items-center rounded-full border p-1;
background: rgba(255, 255, 255, 0.64);
border-color: rgba(23, 32, 51, 0.1);
}
.site-language-toggle button {
@apply flex h-8 min-w-9 items-center justify-center rounded-full px-3 text-xs font-black uppercase transition-colors;
color: #44516a;
}
.site-language-toggle button:hover {
color: #172033;
}
.site-language-toggle button.active {
background: #172033;
color: #fffaf2;
}
.site-login {
@apply flex h-10 min-w-[7.75rem] items-center justify-center rounded-full px-4 text-sm font-bold no-underline transition-colors;
background: #172033;
color: #fffaf2;
}
.site-login:hover {
background: #0f766e;
}
@media (max-width: 420px) {
.site-brand-heading {
@apply hidden;
}
.site-menu-inner {
@apply gap-3;
}
}
@media (max-width: 820px) {
.site-product-panel {
@apply w-[min(30rem,calc(100vw-2rem))] grid-cols-1;
}
}
</style>