Files
webapp/app/components/Sidebar.vue
2026-01-07 09:10:35 +07:00

396 lines
14 KiB
Vue

<template>
<aside
class="bg-base-100 h-screen flex flex-col border-r border-base-300 transition-all duration-300"
:class="collapsed ? 'w-16' : 'w-64'"
>
<!-- Logo + Collapse button -->
<div class="p-4 border-b border-base-300 flex items-center justify-between">
<NuxtLink v-if="!collapsed" :to="localePath('/')" class="text-2xl font-bold text-primary">
Optovia
</NuxtLink>
<NuxtLink v-else :to="localePath('/')" class="text-xl font-bold text-primary">
O
</NuxtLink>
<button
@click="collapsed = !collapsed"
class="btn btn-ghost btn-xs btn-square"
:title="collapsed ? 'Expand' : 'Collapse'"
>
<Icon :name="collapsed ? 'lucide:chevron-right' : 'lucide:chevron-left'" size="16" />
</button>
</div>
<!-- Navigation -->
<nav class="flex-1 overflow-y-auto py-2">
<!-- Collapsed view: show only menu item icons -->
<template v-if="collapsed">
<ul class="menu menu-vertical menu-compact rounded-none w-full px-2 py-1">
<!-- Exchange items -->
<li>
<NuxtLink
:to="localePath('/')"
:class="{ active: isExactActive('/') }"
class="tooltip tooltip-right"
:data-tip="t('nav.search')"
>
<Icon name="lucide:search" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/catalog/offers')"
:class="{ active: isActive('/catalog/offers') }"
class="tooltip tooltip-right"
:data-tip="t('nav.offers')"
>
<Icon name="lucide:tag" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/catalog/suppliers')"
:class="{ active: isActive('/catalog/suppliers') }"
class="tooltip tooltip-right"
:data-tip="t('nav.suppliers')"
>
<Icon name="lucide:building-2" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/catalog/hubs')"
:class="{ active: isActive('/catalog/hubs') }"
class="tooltip tooltip-right"
:data-tip="t('nav.hubs')"
>
<Icon name="lucide:warehouse" size="18" />
</NuxtLink>
</li>
<!-- Logged in items -->
<template v-if="loggedIn">
<li class="my-2"><div class="divider my-0 h-px"></div></li>
<li>
<NuxtLink
:to="localePath('/clientarea/orders')"
:class="{ active: isActive('/clientarea/orders') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.orders')"
>
<Icon name="lucide:package" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/clientarea/addresses')"
:class="{ active: isActive('/clientarea/addresses') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.addresses')"
>
<Icon name="lucide:map-pin" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/clientarea/billing')"
:class="{ active: isActive('/clientarea/billing') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.billing')"
>
<Icon name="lucide:wallet" size="18" />
</NuxtLink>
</li>
<template v-if="isSeller">
<li>
<NuxtLink
:to="localePath('/clientarea/offers')"
:class="{ active: isActive('/clientarea/offers') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.offers')"
>
<Icon name="lucide:tag" size="18" />
</NuxtLink>
</li>
</template>
<li class="my-2"><div class="divider my-0 h-px"></div></li>
<li>
<NuxtLink
:to="localePath('/clientarea/ai')"
:class="{ active: isActive('/clientarea/ai') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.ai')"
>
<Icon name="lucide:sparkles" size="18" />
</NuxtLink>
</li>
<li class="my-2"><div class="divider my-0 h-px"></div></li>
<li>
<NuxtLink
:to="localePath('/clientarea/profile')"
:class="{ active: isActive('/clientarea/profile') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.profile')"
>
<Icon name="lucide:user" size="18" />
</NuxtLink>
</li>
<li>
<NuxtLink
:to="localePath('/clientarea/team')"
:class="{ active: isActive('/clientarea/team') }"
class="tooltip tooltip-right"
:data-tip="t('cabinetNav.team')"
>
<Icon name="lucide:users" size="18" />
</NuxtLink>
</li>
</template>
</ul>
</template>
<!-- Expanded view -->
<template v-else>
<ul class="menu rounded-none w-full gap-1 px-2 py-3">
<!-- Exchange section -->
<li>
<details :open="sections.exchange">
<summary @click.prevent="toggleSection('exchange')" class="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold text-base-content/70">
<Icon name="lucide:layers" size="16" />
<span>{{ t('sidebar.exchange') }}</span>
</summary>
<ul>
<li>
<NuxtLink :to="localePath('/')" :class="{ active: isExactActive('/') }">
<Icon name="lucide:search" size="18" />
{{ t('nav.search') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/catalog/offers')" :class="{ active: isActive('/catalog/offers') }">
<Icon name="lucide:tag" size="18" />
{{ t('nav.offers') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/catalog/suppliers')" :class="{ active: isActive('/catalog/suppliers') }">
<Icon name="lucide:building-2" size="18" />
{{ t('nav.suppliers') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/catalog/hubs')" :class="{ active: isActive('/catalog/hubs') }">
<Icon name="lucide:warehouse" size="18" />
{{ t('nav.hubs') }}
</NuxtLink>
</li>
</ul>
</details>
</li>
<!-- Cabinet section - only for logged in users -->
<template v-if="loggedIn">
<!-- Orders & Logistics -->
<li>
<details :open="sections.orders">
<summary @click.prevent="toggleSection('orders')" class="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold text-base-content/70">
<Icon name="lucide:truck" size="16" />
<span>{{ t('sidebar.ordersLogistics') }}</span>
</summary>
<ul>
<li>
<NuxtLink :to="localePath('/clientarea/orders')" :class="{ active: isActive('/clientarea/orders') }">
<Icon name="lucide:package" size="18" />
{{ t('cabinetNav.orders') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/clientarea/addresses')" :class="{ active: isActive('/clientarea/addresses') }">
<Icon name="lucide:map-pin" size="18" />
{{ t('cabinetNav.addresses') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/clientarea/billing')" :class="{ active: isActive('/clientarea/billing') }">
<Icon name="lucide:wallet" size="18" />
{{ t('cabinetNav.billing') }}
</NuxtLink>
</li>
</ul>
</details>
</li>
<!-- Seller (only for sellers) -->
<li v-if="isSeller">
<details :open="sections.seller">
<summary @click.prevent="toggleSection('seller')" class="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold text-base-content/70">
<Icon name="lucide:badge-dollar-sign" size="16" />
<span>{{ t('sidebar.seller') }}</span>
</summary>
<ul>
<li>
<NuxtLink :to="localePath('/clientarea/offers')" :class="{ active: isActive('/clientarea/offers') }">
<Icon name="lucide:tag" size="18" />
{{ t('cabinetNav.offers') }}
</NuxtLink>
</li>
</ul>
</details>
</li>
<!-- AI & Tools -->
<li>
<details :open="sections.ai">
<summary @click.prevent="toggleSection('ai')" class="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold text-base-content/70">
<Icon name="lucide:sparkles" size="16" />
<span>{{ t('sidebar.aiTools') }}</span>
</summary>
<ul>
<li>
<NuxtLink :to="localePath('/clientarea/ai')" :class="{ active: isActive('/clientarea/ai') }">
<Icon name="lucide:sparkles" size="18" />
{{ t('cabinetNav.ai') }}
</NuxtLink>
</li>
</ul>
</details>
</li>
<!-- Settings -->
<li>
<details :open="sections.settings">
<summary @click.prevent="toggleSection('settings')" class="flex items-center gap-2 text-xs uppercase tracking-wide font-semibold text-base-content/70">
<Icon name="lucide:settings" size="16" />
<span>{{ t('sidebar.settings') }}</span>
</summary>
<ul>
<li>
<NuxtLink :to="localePath('/clientarea/profile')" :class="{ active: isActive('/clientarea/profile') }">
<Icon name="lucide:user" size="18" />
{{ t('cabinetNav.profile') }}
</NuxtLink>
</li>
<li>
<NuxtLink :to="localePath('/clientarea/team')" :class="{ active: isActive('/clientarea/team') }">
<Icon name="lucide:users" size="18" />
{{ t('cabinetNav.team') }}
</NuxtLink>
</li>
</ul>
</details>
</li>
</template>
</ul>
</template>
</nav>
<!-- Company switcher (bottom) - only for logged in users -->
<div v-if="!collapsed" class="p-3 border-t border-base-300">
<template v-if="userData?.teams?.length">
<div class="dropdown dropdown-top w-full">
<div
tabindex="0"
role="button"
class="btn btn-ghost btn-sm w-full justify-start gap-2"
>
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded w-8 h-8 flex items-center justify-center">
<span class="text-xs">{{ getTeamInitials(userData.activeTeam?.name) }}</span>
</div>
</div>
<div class="flex-1 text-left truncate">
<div class="text-sm font-medium truncate">{{ userData.activeTeam?.name || 'Select company' }}</div>
</div>
<Icon name="lucide:chevrons-up-down" size="16" class="opacity-50" />
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-full p-2 shadow-lg border border-base-300 mb-2"
>
<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:plus" size="16" />
{{ t('sidebar.createCompany') }}
</NuxtLink>
</li>
</ul>
</div>
</template>
<template v-else-if="loggedIn">
<NuxtLink
:to="localePath('/clientarea/team')"
class="btn btn-ghost btn-sm w-full justify-start gap-2"
>
<Icon name="lucide:building-2" size="18" />
<span>{{ t('sidebar.joinCompany') }}</span>
</NuxtLink>
</template>
</div>
</aside>
</template>
<script setup lang="ts">
defineEmits(['switch-team'])
defineProps<{
userData?: {
activeTeam?: { name?: string }
activeTeamId?: string
teams?: Array<{ id?: string; name?: string }>
} | null
isSeller?: boolean
loggedIn?: boolean
}>()
const localePath = useLocalePath()
const route = useRoute()
const { t } = useI18n()
// Sidebar collapse state
const collapsed = ref(false)
// Section open states
const sections = reactive({
exchange: true,
orders: true,
seller: true,
ai: true,
settings: true
})
const toggleSection = (section: keyof typeof sections) => {
sections[section] = !sections[section]
}
const isActive = (path: string) => {
const current = route.path
if (current === localePath(path)) return true
return current.startsWith(localePath(path) + '/')
}
const isExactActive = (path: string) => {
return route.path === localePath(path)
}
const getTeamInitials = (name?: string) => {
if (!name) return '?'
return name
.split(' ')
.map(w => w[0])
.join('')
.slice(0, 2)
.toUpperCase()
}
</script>