refactor: align main layout shell
This commit is contained in:
@@ -1,15 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<div class="shell-container">
|
<div class="shell-container">
|
||||||
<app-bar
|
|
||||||
:show-brand="true"
|
|
||||||
:collapse-brand="showsAppSidebar && !isSidebarExpanded"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="shell-main"
|
|
||||||
:class="{ 'shell-main-app': showsAppSidebar }"
|
|
||||||
>
|
|
||||||
<template v-if="showsAppSidebar">
|
<template v-if="showsAppSidebar">
|
||||||
<div class="shell-sidebar-wrap">
|
<div class="shell-sidebar-wrap">
|
||||||
<app-sidebar :is-expanded="isSidebarExpanded" />
|
<app-sidebar :is-expanded="isSidebarExpanded" />
|
||||||
@@ -24,6 +15,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="shell-main"
|
||||||
|
:class="{ 'shell-main-app': showsAppSidebar }"
|
||||||
|
>
|
||||||
|
<app-bar v-if="showsAppSidebar" />
|
||||||
|
|
||||||
<div class="shell-view">
|
<div class="shell-view">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +60,7 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shell-container {
|
.shell-container {
|
||||||
@apply min-h-screen flex flex-col;
|
@apply min-h-screen flex flex-row;
|
||||||
@apply w-full font-sans;
|
@apply w-full font-sans;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(255, 174, 94, 0.18), transparent 28%),
|
radial-gradient(circle at top left, rgba(255, 174, 94, 0.18), transparent 28%),
|
||||||
@@ -73,19 +70,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shell-main {
|
.shell-main {
|
||||||
@apply relative flex flex-1 flex-col;
|
@apply relative flex min-w-0 flex-1 flex-col;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-main-app {
|
.shell-main-app {
|
||||||
@apply md:flex-row md:items-start;
|
@apply min-h-screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell-sidebar-wrap {
|
.shell-sidebar-wrap {
|
||||||
@apply relative flex-shrink-0;
|
@apply sticky top-0 z-30 h-screen flex-shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-boundary-toggle {
|
.sidebar-boundary-toggle {
|
||||||
@apply absolute left-full top-8 z-10 flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-full border transition-colors;
|
@apply absolute left-full top-8 z-40 flex h-10 w-10 -translate-x-1/2 items-center justify-center rounded-full border transition-colors;
|
||||||
background: rgba(255, 250, 242, 0.98);
|
background: rgba(255, 250, 242, 0.98);
|
||||||
border-color: rgba(23, 32, 51, 0.12);
|
border-color: rgba(23, 32, 51, 0.12);
|
||||||
color: #44516a;
|
color: #44516a;
|
||||||
|
|||||||
@@ -10,17 +10,6 @@
|
|||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
showBrand: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
collapseBrand: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -73,21 +62,6 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="side-container">
|
<nav class="side-container">
|
||||||
<div class="brand-block">
|
|
||||||
<router-link
|
|
||||||
v-if="showBrand"
|
|
||||||
class="brand-link"
|
|
||||||
:class="{ 'brand-link-collapsed': collapseBrand }"
|
|
||||||
to="/"
|
|
||||||
>
|
|
||||||
<span class="brand-mark">S</span>
|
|
||||||
<div v-if="!collapseBrand">
|
|
||||||
<div class="brand-name">Socialize</div>
|
|
||||||
<div class="brand-caption">{{ t('nav.brandCaption') }}</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="side-menu">
|
<div class="side-menu">
|
||||||
<div class="side-menu-items side-menu-left">
|
<div class="side-menu-items side-menu-left">
|
||||||
<WorkspaceSelector
|
<WorkspaceSelector
|
||||||
@@ -123,44 +97,15 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.side-container {
|
.side-container {
|
||||||
@apply sticky top-0 z-10 flex flex-col gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between;
|
@apply sticky top-0 z-20 flex flex-col gap-4 px-5 py-4 md:flex-row md:items-center md:justify-between;
|
||||||
background: rgba(255, 250, 242, 0.82);
|
background: rgba(255, 250, 242, 0.82);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
|
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-block {
|
|
||||||
@apply flex items-center gap-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-link {
|
|
||||||
@apply flex items-center gap-3 no-underline;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-link-collapsed {
|
|
||||||
@apply gap-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-mark {
|
|
||||||
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
|
|
||||||
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
|
||||||
color: #fffaf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
@apply text-lg font-black uppercase tracking-[0.18em];
|
|
||||||
color: #172033;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-caption {
|
|
||||||
@apply text-xs uppercase tracking-[0.24em];
|
|
||||||
color: #5d6b82;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-menu {
|
.side-menu {
|
||||||
@apply flex flex-1 items-center justify-between gap-3;
|
@apply flex w-full flex-1 items-center justify-between gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-menu-items {
|
.side-menu-items {
|
||||||
|
|||||||
@@ -2,15 +2,13 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import AppAvatar from '@/components/AppAvatar.vue';
|
|
||||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
|
import { useChannelsStore } from '@/features/channels/stores/channelsStore.js';
|
||||||
import { useLanguageStore } from '@/stores/languageStore.js';
|
|
||||||
import { useNotificationsStore } from '@/features/notifications/stores/notificationsStore.js';
|
import { useNotificationsStore } from '@/features/notifications/stores/notificationsStore.js';
|
||||||
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
|
import { getNotificationRoute } from '@/features/notifications/notificationRoutes.js';
|
||||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||||
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
|
||||||
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
import SidebarUserMenu from './SidebarUserMenu.vue';
|
||||||
import {
|
import {
|
||||||
mdiBellOutline,
|
mdiBellOutline,
|
||||||
mdiCalendarMonthOutline,
|
mdiCalendarMonthOutline,
|
||||||
@@ -39,16 +37,12 @@
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const channelsStore = useChannelsStore();
|
const channelsStore = useChannelsStore();
|
||||||
const contentItemsStore = useContentItemsStore();
|
const contentItemsStore = useContentItemsStore();
|
||||||
const languageStore = useLanguageStore();
|
|
||||||
const notificationsStore = useNotificationsStore();
|
const notificationsStore = useNotificationsStore();
|
||||||
const campaignsStore = useCampaignsStore();
|
const campaignsStore = useCampaignsStore();
|
||||||
const userProfileStore = useUserProfileStore();
|
|
||||||
const isUserMenuOpen = ref(false);
|
|
||||||
const isNotificationsOpen = ref(false);
|
const isNotificationsOpen = ref(false);
|
||||||
const isSearchFocused = ref(false);
|
const isSearchFocused = ref(false);
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
|
|
||||||
const userMenuRef = ref(null);
|
|
||||||
const notificationsRef = ref(null);
|
const notificationsRef = ref(null);
|
||||||
const searchRef = ref(null);
|
const searchRef = ref(null);
|
||||||
|
|
||||||
@@ -133,25 +127,6 @@
|
|||||||
isNotificationsOpen.value = !isNotificationsOpen.value;
|
isNotificationsOpen.value = !isNotificationsOpen.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleUserMenu() {
|
|
||||||
if (!props.isExpanded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isUserMenuOpen.value = !isUserMenuOpen.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLanguage() {
|
|
||||||
const nextLocale = languageStore.locale === 'en' ? 'fr' : 'en';
|
|
||||||
languageStore.setLocale(nextLocale);
|
|
||||||
isUserMenuOpen.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openProfile() {
|
|
||||||
isUserMenuOpen.value = false;
|
|
||||||
await router.push({ name: 'settings-user-information' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNotificationTitle(notification) {
|
function formatNotificationTitle(notification) {
|
||||||
return notificationTitleMap.value[notification.eventType] ?? notification.message;
|
return notificationTitleMap.value[notification.eventType] ?? notification.message;
|
||||||
}
|
}
|
||||||
@@ -182,11 +157,6 @@
|
|||||||
await router.push(result.route);
|
await router.push(result.route);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
|
||||||
isUserMenuOpen.value = false;
|
|
||||||
authStore.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDocumentClick(event) {
|
function handleDocumentClick(event) {
|
||||||
if (searchRef.value && !searchRef.value.contains(event.target)) {
|
if (searchRef.value && !searchRef.value.contains(event.target)) {
|
||||||
isSearchFocused.value = false;
|
isSearchFocused.value = false;
|
||||||
@@ -195,10 +165,6 @@
|
|||||||
if (isNotificationsOpen.value && notificationsRef.value && !notificationsRef.value.contains(event.target)) {
|
if (isNotificationsOpen.value && notificationsRef.value && !notificationsRef.value.contains(event.target)) {
|
||||||
isNotificationsOpen.value = false;
|
isNotificationsOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserMenuOpen.value && userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
|
||||||
isUserMenuOpen.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -215,15 +181,6 @@
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.isExpanded,
|
|
||||||
isExpanded => {
|
|
||||||
if (!isExpanded) {
|
|
||||||
isUserMenuOpen.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleDocumentClick);
|
document.addEventListener('click', handleDocumentClick);
|
||||||
});
|
});
|
||||||
@@ -238,7 +195,22 @@
|
|||||||
class="app-sidebar"
|
class="app-sidebar"
|
||||||
:class="{ 'app-sidebar-collapsed': !isExpanded }"
|
:class="{ 'app-sidebar-collapsed': !isExpanded }"
|
||||||
>
|
>
|
||||||
<div class="app-sidebar-inner">
|
|
||||||
|
<div class="brand-block">
|
||||||
|
<router-link
|
||||||
|
class="brand-link"
|
||||||
|
:class="{ 'brand-link-collapsed': !isExpanded }"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<span class="brand-mark">S</span>
|
||||||
|
<div v-if="isExpanded">
|
||||||
|
<div class="brand-name">Socialize</div>
|
||||||
|
<div class="brand-caption">{{ t('nav.brandCaption') }}</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app-sidebar-scroll">
|
||||||
<div
|
<div
|
||||||
v-if="authStore.isAuthenticated"
|
v-if="authStore.isAuthenticated"
|
||||||
class="sidebar-section sidebar-utilities"
|
class="sidebar-section sidebar-utilities"
|
||||||
@@ -578,75 +550,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
</div>
|
||||||
v-if="authStore.isAuthenticated"
|
|
||||||
ref="userMenuRef"
|
|
||||||
class="sidebar-workspace sidebar-workspace-bottom"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="sidebar-workspace-trigger"
|
|
||||||
type="button"
|
|
||||||
:title="!isExpanded ? userProfileStore.alias : null"
|
|
||||||
@click.stop="toggleUserMenu"
|
|
||||||
>
|
|
||||||
<AppAvatar
|
|
||||||
:name="userProfileStore.alias"
|
|
||||||
:src="userProfileStore.portraitUrl"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="isExpanded"
|
|
||||||
class="sidebar-workspace-label"
|
|
||||||
>
|
|
||||||
{{ userProfileStore.alias }}
|
|
||||||
</span>
|
|
||||||
<v-icon
|
|
||||||
v-if="isExpanded"
|
|
||||||
:icon="mdiChevronDown"
|
|
||||||
class="sidebar-workspace-icon"
|
|
||||||
:class="{ 'sidebar-workspace-icon-open': isUserMenuOpen }"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
<SidebarUserMenu
|
||||||
v-if="isExpanded && isUserMenuOpen"
|
v-if="authStore.isAuthenticated"
|
||||||
class="sidebar-workspace-menu"
|
:is-expanded="isExpanded"
|
||||||
>
|
/>
|
||||||
<button
|
|
||||||
class="sidebar-workspace-option"
|
|
||||||
type="button"
|
|
||||||
@click="openProfile"
|
|
||||||
>
|
|
||||||
{{ t('nav.profile') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="sidebar-workspace-option"
|
|
||||||
type="button"
|
|
||||||
@click="toggleLanguage"
|
|
||||||
>
|
|
||||||
{{ t('nav.language') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="sidebar-workspace-option sidebar-workspace-option-danger"
|
|
||||||
type="button"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
{{ t('nav.signOut') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-sidebar {
|
.app-sidebar {
|
||||||
@apply w-[19rem] flex-shrink-0 px-4 pb-4 pt-4 transition-[width,padding] duration-200 md:sticky md:top-24 md:h-[calc(100vh-6rem)] md:pt-0;
|
@apply flex h-full w-[19rem] flex-shrink-0 flex-col px-4 pt-4 transition-[width,padding] duration-200;
|
||||||
border-right: 1px solid rgba(23, 32, 51, 0.08);
|
border-right: 1px solid rgba(23, 32, 51, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-sidebar-inner {
|
.app-sidebar-scroll {
|
||||||
@apply flex h-full flex-col gap-4 overflow-y-auto py-3 pr-3;
|
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-3 pr-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
@apply flex items-center gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-link {
|
||||||
|
@apply flex items-center gap-3 no-underline;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
|
||||||
|
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
|
||||||
|
color: #fffaf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
@apply text-lg font-black uppercase tracking-[0.18em];
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-caption {
|
||||||
|
@apply text-xs uppercase tracking-[0.24em];
|
||||||
|
color: #5d6b82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
@apply flex flex-1 items-center justify-between gap-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-utilities {
|
.sidebar-utilities {
|
||||||
@@ -776,63 +725,6 @@
|
|||||||
color: #172033;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-workspace {
|
|
||||||
@apply relative flex flex-col gap-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-bottom {
|
|
||||||
@apply mt-auto pt-4;
|
|
||||||
border-top: 1px solid rgba(23, 32, 51, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-kicker {
|
|
||||||
@apply px-4 text-[10px] font-bold uppercase tracking-[0.22em];
|
|
||||||
color: #7a8799;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-trigger {
|
|
||||||
@apply flex w-full items-center gap-3 rounded-[1.1rem] px-4 py-3 text-left transition-colors;
|
|
||||||
background: rgba(23, 32, 51, 0.04);
|
|
||||||
color: #172033;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-trigger:hover {
|
|
||||||
background: rgba(23, 32, 51, 0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-label {
|
|
||||||
@apply flex-1 truncate text-sm font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-icon {
|
|
||||||
@apply text-base transition-transform;
|
|
||||||
color: #5d6b82;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-icon-open {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-menu {
|
|
||||||
@apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-option {
|
|
||||||
@apply rounded-[0.95rem] px-4 py-3 text-left text-sm font-semibold transition-colors;
|
|
||||||
color: #172033;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-option:hover {
|
|
||||||
background: rgba(23, 32, 51, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-workspace-option-danger {
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section {
|
.sidebar-section {
|
||||||
@apply flex flex-col gap-2;
|
@apply flex flex-col gap-2;
|
||||||
}
|
}
|
||||||
|
|||||||
185
frontend/src/layouts/main/SidebarUserMenu.vue
Normal file
185
frontend/src/layouts/main/SidebarUserMenu.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import AppAvatar from '@/components/AppAvatar.vue';
|
||||||
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
|
import { useLanguageStore } from '@/stores/languageStore.js';
|
||||||
|
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
||||||
|
import { mdiChevronDown } from '@mdi/js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isExpanded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useI18n();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const languageStore = useLanguageStore();
|
||||||
|
const userProfileStore = useUserProfileStore();
|
||||||
|
const isUserMenuOpen = ref(false);
|
||||||
|
const userMenuRef = ref(null);
|
||||||
|
|
||||||
|
function toggleUserMenu() {
|
||||||
|
if (!props.isExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserMenuOpen.value = !isUserMenuOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLanguage() {
|
||||||
|
const nextLocale = languageStore.locale === 'en' ? 'fr' : 'en';
|
||||||
|
languageStore.setLocale(nextLocale);
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openProfile() {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
await router.push({ name: 'settings-user-information' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
authStore.logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentClick(event) {
|
||||||
|
if (isUserMenuOpen.value && userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.isExpanded,
|
||||||
|
isExpanded => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleDocumentClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="userMenuRef"
|
||||||
|
class="sidebar-workspace sidebar-workspace-bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="sidebar-workspace-trigger"
|
||||||
|
type="button"
|
||||||
|
:title="!isExpanded ? userProfileStore.alias : null"
|
||||||
|
@click.stop="toggleUserMenu"
|
||||||
|
>
|
||||||
|
<AppAvatar
|
||||||
|
:name="userProfileStore.alias"
|
||||||
|
:src="userProfileStore.portraitUrl"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="isExpanded"
|
||||||
|
class="sidebar-workspace-label"
|
||||||
|
>
|
||||||
|
{{ userProfileStore.alias }}
|
||||||
|
</span>
|
||||||
|
<v-icon
|
||||||
|
v-if="isExpanded"
|
||||||
|
:icon="mdiChevronDown"
|
||||||
|
class="sidebar-workspace-icon"
|
||||||
|
:class="{ 'sidebar-workspace-icon-open': isUserMenuOpen }"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isExpanded && isUserMenuOpen"
|
||||||
|
class="sidebar-workspace-menu"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="sidebar-workspace-option"
|
||||||
|
type="button"
|
||||||
|
@click="openProfile"
|
||||||
|
>
|
||||||
|
{{ t('nav.profile') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="sidebar-workspace-option"
|
||||||
|
type="button"
|
||||||
|
@click="toggleLanguage"
|
||||||
|
>
|
||||||
|
{{ t('nav.language') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="sidebar-workspace-option sidebar-workspace-option-danger"
|
||||||
|
type="button"
|
||||||
|
@click="handleLogout"
|
||||||
|
>
|
||||||
|
{{ t('nav.signOut') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-workspace {
|
||||||
|
@apply relative flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-bottom {
|
||||||
|
@apply py-4;
|
||||||
|
border-top: 1px solid rgba(23, 32, 51, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-trigger {
|
||||||
|
@apply flex w-full items-center gap-3 rounded-[1.1rem] px-4 py-3 text-left transition-colors;
|
||||||
|
background: rgba(23, 32, 51, 0.04);
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-trigger:hover {
|
||||||
|
background: rgba(23, 32, 51, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-label {
|
||||||
|
@apply flex-1 truncate text-sm font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-icon {
|
||||||
|
@apply text-base transition-transform;
|
||||||
|
color: #5d6b82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-icon-open {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-menu {
|
||||||
|
@apply absolute bottom-[calc(100%+0.5rem)] left-0 right-0 z-30 flex flex-col gap-1 rounded-[1.25rem] border p-2;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-option {
|
||||||
|
@apply rounded-[0.95rem] px-4 py-3 text-left text-sm font-semibold transition-colors;
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-option:hover {
|
||||||
|
background: rgba(23, 32, 51, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-workspace-option-danger {
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user