refactor: organize frontend by feature
This commit is contained in:
356
frontend/src/layouts/main/AppBar.vue
Normal file
356
frontend/src/layouts/main/AppBar.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||
import {
|
||||
mdiChevronDown,
|
||||
mdiCogOutline,
|
||||
mdiLogin,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
|
||||
const props = defineProps({
|
||||
showBrand: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
collapseBrand: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const workspaceStore = useWorkspaceStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const isWorkspaceMenuOpen = ref(false);
|
||||
const workspaceMenuRef = ref(null);
|
||||
|
||||
const canSwitchWorkspaces = computed(() => workspaceStore.workspaces.length > 1);
|
||||
const canManageWorkspaces = computed(() => authStore.isManager);
|
||||
const canOpenWorkspaceMenu = computed(() => canSwitchWorkspaces.value || canManageWorkspaces.value);
|
||||
const activeWorkspaceName = computed(() =>
|
||||
workspaceStore.activeWorkspace?.name || t('nav.noWorkspace')
|
||||
);
|
||||
const appBarActions = computed(() => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
return [];
|
||||
}
|
||||
|
||||
switch (route.name) {
|
||||
case 'workspace-dashboard':
|
||||
case 'content-items':
|
||||
return authStore.isManager || authStore.isProvider
|
||||
? [{
|
||||
key: 'create-content',
|
||||
label: t('contentItems.newItem'),
|
||||
icon: mdiPlus,
|
||||
route: { name: 'content-item-create' },
|
||||
}]
|
||||
: [];
|
||||
case 'campaigns':
|
||||
return [{
|
||||
key: 'create-campaign',
|
||||
label: t('projects.newProject'),
|
||||
icon: mdiPlus,
|
||||
route: { name: 'campaigns', query: { create: 'true' } },
|
||||
}];
|
||||
case 'channels':
|
||||
return [{
|
||||
key: 'create-channel',
|
||||
label: t('channels.createTitle'),
|
||||
icon: mdiPlus,
|
||||
route: { name: 'channels', query: { create: 'true' } },
|
||||
}];
|
||||
case 'workspace-settings':
|
||||
case 'settings-user-information':
|
||||
case 'settings-workspaces':
|
||||
case 'settings-integrations':
|
||||
return [{
|
||||
key: 'settings',
|
||||
label: t('nav.settings'),
|
||||
icon: mdiCogOutline,
|
||||
route: { name: 'settings-user-information' },
|
||||
}];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
function toggleWorkspaceMenu() {
|
||||
if (!canOpenWorkspaceMenu.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isWorkspaceMenuOpen.value = !isWorkspaceMenuOpen.value;
|
||||
}
|
||||
|
||||
function chooseWorkspace(workspaceId) {
|
||||
workspaceStore.setActiveWorkspace(workspaceId);
|
||||
isWorkspaceMenuOpen.value = false;
|
||||
}
|
||||
|
||||
async function openCreateWorkspace() {
|
||||
isWorkspaceMenuOpen.value = false;
|
||||
await router.push({ name: 'workspace-create' });
|
||||
}
|
||||
|
||||
function handleDocumentClick(event) {
|
||||
if (workspaceMenuRef.value && !workspaceMenuRef.value.contains(event.target)) {
|
||||
isWorkspaceMenuOpen.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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-items side-menu-left">
|
||||
<div
|
||||
v-if="authStore.isAuthenticated"
|
||||
ref="workspaceMenuRef"
|
||||
class="user-menu-wrap"
|
||||
>
|
||||
<button
|
||||
class="menu-item-action workspace-trigger"
|
||||
:class="{ 'workspace-trigger-static': !canOpenWorkspaceMenu }"
|
||||
@click.stop="toggleWorkspaceMenu"
|
||||
>
|
||||
<span class="workspace-trigger-mark">W</span>
|
||||
<span class="label workspace-trigger-label">{{ activeWorkspaceName }}</span>
|
||||
<v-icon
|
||||
v-if="canOpenWorkspaceMenu"
|
||||
:icon="mdiChevronDown"
|
||||
class="user-trigger-icon"
|
||||
:class="{ 'user-trigger-icon-open': isWorkspaceMenuOpen }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="isWorkspaceMenuOpen"
|
||||
class="user-menu"
|
||||
>
|
||||
<button
|
||||
v-for="workspace in workspaceStore.workspaces"
|
||||
:key="workspace.id"
|
||||
class="user-menu-item"
|
||||
:class="{ 'user-menu-item-active': workspace.id === workspaceStore.activeWorkspaceId }"
|
||||
@click="chooseWorkspace(workspace.id)"
|
||||
>
|
||||
<span>{{ workspace.name }}</span>
|
||||
<small>{{ workspace.timeZone }}</small>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="canManageWorkspaces"
|
||||
class="user-menu-item user-menu-item-create"
|
||||
type="button"
|
||||
@click="openCreateWorkspace"
|
||||
>
|
||||
<span>{{ t('workspaceSelector.createAction') }}</span>
|
||||
<v-icon :icon="mdiPlus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-menu-items side-menu-right">
|
||||
<template v-if="!authStore.isAuthenticated">
|
||||
<router-link to="/login">
|
||||
<button class="menu-item-action">
|
||||
<v-icon :icon="mdiLogin" />
|
||||
<span class="label">{{ t('nav.signIn') }}</span>
|
||||
</button>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<router-link
|
||||
v-for="action in appBarActions"
|
||||
:key="action.key"
|
||||
:to="action.route"
|
||||
class="menu-action-link"
|
||||
>
|
||||
<button class="menu-item-action">
|
||||
<v-icon :icon="action.icon" />
|
||||
<span class="label">{{ action.label }}</span>
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
background: rgba(255, 250, 242, 0.82);
|
||||
backdrop-filter: blur(18px);
|
||||
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
|
||||
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 {
|
||||
@apply flex flex-1 items-center justify-between gap-3;
|
||||
}
|
||||
|
||||
.side-menu-items {
|
||||
@apply flex flex-wrap items-center justify-end gap-2;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.side-menu-left {
|
||||
@apply justify-start;
|
||||
}
|
||||
|
||||
.side-menu-right {
|
||||
@apply justify-end;
|
||||
}
|
||||
|
||||
.label {
|
||||
@apply hidden text-nowrap md:inline;
|
||||
}
|
||||
|
||||
.menu-item-action {
|
||||
@apply flex h-11 items-center gap-3 rounded-full px-4 transition-colors;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
color: #172033;
|
||||
border: 1px solid rgba(23, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.menu-item-action:hover {
|
||||
background: #172033;
|
||||
color: #fffaf2;
|
||||
}
|
||||
|
||||
.menu-item-action i {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
.user-menu-wrap {
|
||||
@apply relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.workspace-trigger {
|
||||
@apply max-w-[18rem] pl-2 pr-3;
|
||||
}
|
||||
|
||||
.user-trigger-icon {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
.user-trigger-icon-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.workspace-trigger-static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.workspace-trigger-mark {
|
||||
@apply flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-xl text-xs font-black uppercase;
|
||||
background: linear-gradient(135deg, rgba(255, 138, 61, 0.16), rgba(239, 68, 68, 0.14));
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.workspace-trigger-label {
|
||||
@apply max-w-[11rem] truncate;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
@apply absolute right-0 top-[calc(100%+0.75rem)] flex min-w-[14rem] flex-col gap-1 rounded-[1.25rem] border p-2;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
@apply flex items-center gap-3 rounded-[0.9rem] px-3 py-3 text-left text-sm font-semibold transition-colors;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.user-menu-item:hover {
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
}
|
||||
|
||||
.user-menu-item-danger {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.user-menu-item-active {
|
||||
background: rgba(255, 138, 61, 0.12);
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.user-menu-item small {
|
||||
@apply ml-auto text-xs font-medium;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.user-menu-item-create {
|
||||
@apply justify-between border border-dashed;
|
||||
border-color: rgba(23, 32, 51, 0.12);
|
||||
}
|
||||
|
||||
.menu-action-link {
|
||||
@apply no-underline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user