Add topnav layout with MainNavigation, SubNavigation, GlobalSearchBar, SplitLayout
Some checks failed
Build Docker Image / build (push) Failing after 1m25s
Some checks failed
Build Docker Image / build (push) Failing after 1m25s
This commit is contained in:
217
app/components/navigation/MainNavigation.vue
Normal file
217
app/components/navigation/MainNavigation.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<header class="sticky top-0 z-40 bg-base-100 border-b border-base-300">
|
||||
<div class="flex items-center justify-between h-16 px-4 lg:px-6">
|
||||
<!-- Left: Logo -->
|
||||
<div class="flex items-center">
|
||||
<NuxtLink :to="localePath('/')" class="flex items-center gap-2">
|
||||
<img src="/logo.svg" alt="Optovia" class="h-8" />
|
||||
<span class="font-bold text-xl hidden sm:inline">Optovia</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- Center: Main tabs -->
|
||||
<nav class="hidden md:flex items-center gap-1">
|
||||
<NuxtLink
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:to="localePath(tab.path)"
|
||||
class="tab-item"
|
||||
:class="{ 'tab-active': isActiveTab(tab.key) }"
|
||||
>
|
||||
<Icon v-if="tab.icon" :name="tab.icon" size="18" class="mr-1" />
|
||||
{{ tab.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Right: Globe + Team + User -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Globe (language/currency) dropdown -->
|
||||
<div class="dropdown dropdown-end">
|
||||
<button tabindex="0" class="btn btn-ghost btn-circle">
|
||||
<Icon name="lucide:globe" size="20" />
|
||||
</button>
|
||||
<div tabindex="0" class="dropdown-content bg-base-100 rounded-box z-50 w-52 p-4 shadow-lg border border-base-300">
|
||||
<div class="font-semibold mb-2">{{ $t('common.language') }}</div>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<NuxtLink
|
||||
v-for="loc in locales"
|
||||
:key="loc.code"
|
||||
:to="switchLocalePath(loc.code)"
|
||||
class="btn btn-sm"
|
||||
:class="locale === loc.code ? 'btn-primary' : 'btn-ghost'"
|
||||
>
|
||||
{{ loc.code.toUpperCase() }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="font-semibold mb-2">{{ $t('common.theme') }}</div>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost w-full justify-start"
|
||||
@click="$emit('toggle-theme')"
|
||||
>
|
||||
<Icon :name="theme === 'night' ? 'lucide:sun' : 'lucide:moon'" size="16" />
|
||||
{{ theme === 'night' ? $t('common.theme_light') : $t('common.theme_dark') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team dropdown -->
|
||||
<template v-if="loggedIn && userData?.teams?.length">
|
||||
<div class="dropdown dropdown-end">
|
||||
<button tabindex="0" class="btn btn-ghost gap-2">
|
||||
<Icon name="lucide:building-2" size="18" />
|
||||
<span class="hidden sm:inline max-w-32 truncate">
|
||||
{{ userData?.activeTeam?.name || $t('common.selectTeam') }}
|
||||
</span>
|
||||
<Icon name="lucide:chevron-down" size="14" />
|
||||
</button>
|
||||
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2 shadow-lg border border-base-300">
|
||||
<li class="menu-title"><span>{{ $t('common.teams') }}</span></li>
|
||||
<li v-for="team in userData?.teams" :key="team.id">
|
||||
<a
|
||||
@click="$emit('switch-team', team)"
|
||||
:class="{ active: team.id === userData?.activeTeamId }"
|
||||
>
|
||||
{{ team.name }}
|
||||
</a>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<li>
|
||||
<NuxtLink :to="localePath('/clientarea/team')">
|
||||
<Icon name="lucide:settings" size="16" />
|
||||
{{ $t('cabinetNav.teamSettings') }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- User menu -->
|
||||
<template v-if="sessionChecked">
|
||||
<template v-if="loggedIn">
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar">
|
||||
<div class="w-10 rounded-full">
|
||||
<div v-if="userAvatarSvg" v-html="userAvatarSvg" class="w-full h-full" />
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full bg-primary flex items-center justify-center text-primary-content font-bold text-sm"
|
||||
>
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"
|
||||
>
|
||||
<li class="menu-title">
|
||||
<span>{{ userName }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink :to="localePath('/clientarea/profile')">
|
||||
<Icon name="lucide:user" size="16" />
|
||||
{{ $t('dashboard.profile') }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<div class="divider my-1"></div>
|
||||
<li>
|
||||
<a @click="$emit('sign-out')" class="text-error">
|
||||
<Icon name="lucide:log-out" size="16" />
|
||||
{{ $t('auth.logout') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button @click="$emit('sign-in')" class="btn btn-primary btn-sm">
|
||||
{{ $t('auth.login') }}
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile tabs (shown below header on small screens) -->
|
||||
<nav class="md:hidden flex items-center justify-center gap-1 py-2 border-t border-base-300 overflow-x-auto">
|
||||
<NuxtLink
|
||||
v-for="tab in visibleTabs"
|
||||
:key="tab.key"
|
||||
:to="localePath(tab.path)"
|
||||
class="tab-item-mobile"
|
||||
:class="{ 'tab-active': isActiveTab(tab.key) }"
|
||||
>
|
||||
<Icon v-if="tab.icon" :name="tab.icon" size="16" />
|
||||
<span class="text-xs">{{ tab.label }}</span>
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
sessionChecked?: boolean
|
||||
loggedIn?: boolean
|
||||
userAvatarSvg?: string
|
||||
userName?: string
|
||||
userInitials?: string
|
||||
theme?: 'default' | 'night'
|
||||
userData?: {
|
||||
id?: string
|
||||
activeTeam?: { name?: string; teamType?: string }
|
||||
activeTeamId?: string
|
||||
teams?: Array<{ id?: string; name?: string; logtoOrgId?: string }>
|
||||
} | null
|
||||
isSeller?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits(['toggle-theme', 'sign-out', 'sign-in', 'switch-team'])
|
||||
|
||||
const localePath = useLocalePath()
|
||||
const { locale, locales } = useI18n()
|
||||
const switchLocalePath = useSwitchLocalePath()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ key: 'catalog', label: t('cabinetNav.catalog'), path: '/catalog/offers', icon: 'lucide:search', auth: false },
|
||||
{ key: 'orders', label: t('cabinetNav.orders'), path: '/clientarea/orders', icon: 'lucide:package', auth: true },
|
||||
{ key: 'seller', label: t('cabinetNav.seller'), path: '/clientarea/offers', icon: 'lucide:store', auth: true, seller: true },
|
||||
])
|
||||
|
||||
const visibleTabs = computed(() => {
|
||||
return tabs.value.filter(tab => {
|
||||
if (tab.auth && !props.loggedIn) return false
|
||||
if (tab.seller && !props.isSeller) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
const isActiveTab = (key: string) => {
|
||||
const path = route.path
|
||||
if (key === 'catalog') return path.startsWith('/catalog') || path === '/'
|
||||
if (key === 'orders') return path.includes('/clientarea/orders') || path.includes('/clientarea/addresses') || path.includes('/clientarea/billing')
|
||||
if (key === 'seller') return path.includes('/clientarea/offers')
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-item {
|
||||
@apply px-4 py-2 rounded-full font-medium text-sm transition-colors;
|
||||
@apply hover:bg-base-200;
|
||||
}
|
||||
|
||||
.tab-item.tab-active {
|
||||
@apply bg-base-200 text-primary;
|
||||
}
|
||||
|
||||
.tab-item-mobile {
|
||||
@apply flex flex-col items-center gap-1 px-4 py-2 rounded-lg;
|
||||
@apply hover:bg-base-200;
|
||||
}
|
||||
|
||||
.tab-item-mobile.tab-active {
|
||||
@apply bg-base-200 text-primary;
|
||||
}
|
||||
</style>
|
||||
63
app/components/navigation/SubNavigation.vue
Normal file
63
app/components/navigation/SubNavigation.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<nav v-if="items.length > 0" class="bg-base-100 border-b border-base-300">
|
||||
<div class="flex items-center justify-center gap-1 py-2 px-4 overflow-x-auto">
|
||||
<NuxtLink
|
||||
v-for="item in items"
|
||||
:key="item.path"
|
||||
:to="localePath(item.path)"
|
||||
class="subnav-item"
|
||||
:class="{ active: isActive(item.path) }"
|
||||
>
|
||||
<Icon v-if="item.icon" :name="item.icon" size="16" class="mr-1.5" />
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
section: 'catalog' | 'orders' | 'seller' | 'settings'
|
||||
}>()
|
||||
|
||||
const localePath = useLocalePath()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const sectionItems = computed(() => ({
|
||||
catalog: [
|
||||
{ label: t('cabinetNav.offers'), path: '/catalog/offers', icon: 'lucide:tag' },
|
||||
{ label: t('cabinetNav.suppliers'), path: '/catalog/suppliers', icon: 'lucide:users' },
|
||||
{ label: t('cabinetNav.hubs'), path: '/catalog/hubs', icon: 'lucide:warehouse' },
|
||||
],
|
||||
orders: [
|
||||
{ label: t('cabinetNav.orders'), path: '/clientarea/orders', icon: 'lucide:package' },
|
||||
{ label: t('cabinetNav.addresses'), path: '/clientarea/addresses', icon: 'lucide:map-pin' },
|
||||
{ label: t('cabinetNav.billing'), path: '/clientarea/billing', icon: 'lucide:credit-card' },
|
||||
],
|
||||
seller: [
|
||||
{ label: t('cabinetNav.myOffers'), path: '/clientarea/offers', icon: 'lucide:tag' },
|
||||
],
|
||||
settings: [
|
||||
{ label: t('cabinetNav.profile'), path: '/clientarea/profile', icon: 'lucide:user' },
|
||||
{ label: t('cabinetNav.team'), path: '/clientarea/team', icon: 'lucide:users' },
|
||||
],
|
||||
}))
|
||||
|
||||
const items = computed(() => sectionItems.value[props.section] || [])
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return route.path === localePath(path) || route.path.startsWith(localePath(path) + '/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.subnav-item {
|
||||
@apply px-4 py-2 rounded-full text-sm font-medium transition-colors whitespace-nowrap;
|
||||
@apply text-base-content/70 hover:text-base-content hover:bg-base-200;
|
||||
}
|
||||
|
||||
.subnav-item.active {
|
||||
@apply text-primary bg-primary/10;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user