Benard John
feat: implement full-stack architecture with database models, authentication services, and web dashboard components
37b5223 | <template> | |
| <n-dropdown | |
| trigger="click" | |
| placement="bottom-end" | |
| :options="menuOptions" | |
| @select="onSelect" | |
| > | |
| <button class="user-menu-trigger" :aria-label="`Account menu for ${userLabel}`"> | |
| <span class="avatar"> | |
| {{ userInitial }} | |
| </span> | |
| <span class="user-meta"> | |
| <span class="user-name">{{ userLabel }}</span> | |
| <span class="user-tier">{{ tierLabel }}</span> | |
| </span> | |
| <n-icon size="14" class="chevron"> | |
| <chevron-down-outline /> | |
| </n-icon> | |
| </button> | |
| </n-dropdown> | |
| </template> | |
| <script setup lang="ts"> | |
| import { computed, h } from 'vue' | |
| import { useRouter } from 'vue-router' | |
| import { useAuthStore } from '@/stores/auth' | |
| import { NIcon } from 'naive-ui' | |
| import { | |
| ChevronDownOutline, | |
| PersonOutline, | |
| KeyOutline, | |
| LogOutOutline, | |
| MoonOutline, | |
| SunnyOutline, | |
| } from '@vicons/ionicons5' | |
| import { useThemeStore } from '@/stores/theme' | |
| const authStore = useAuthStore() | |
| const themeStore = useThemeStore() | |
| const router = useRouter() | |
| const userLabel = computed(() => { | |
| if (authStore.user?.display_name) return authStore.user.display_name | |
| if (authStore.user?.email) { | |
| const [local] = authStore.user.email.split('@') | |
| return local | |
| } | |
| return 'Account' | |
| }) | |
| const userInitial = computed(() => | |
| userLabel.value.charAt(0).toUpperCase() | |
| ) | |
| const tierLabel = computed(() => { | |
| switch (authStore.tier) { | |
| case 'registered': return 'REGISTERED' | |
| case 'api': return 'API' | |
| case 'enterprise': return 'ENTERPRISE' | |
| default: return 'PUBLIC' | |
| } | |
| }) | |
| function renderIcon(component: any) { | |
| return () => h(NIcon, null, { default: () => h(component) }) | |
| } | |
| const menuOptions = computed(() => [ | |
| { | |
| key: 'header', | |
| type: 'render', | |
| render: () => | |
| h('div', { class: 'menu-header' }, [ | |
| h('div', { class: 'menu-header-email' }, authStore.user?.email || ''), | |
| h('div', { class: 'menu-header-tier' }, tierLabel.value), | |
| ]), | |
| }, | |
| { type: 'divider', key: 'd1' }, | |
| { | |
| label: 'Account', | |
| key: 'account', | |
| icon: renderIcon(PersonOutline), | |
| }, | |
| { | |
| label: 'API keys', | |
| key: 'api-keys', | |
| icon: renderIcon(KeyOutline), | |
| }, | |
| { | |
| label: themeStore.theme === 'dark' ? 'Light mode' : 'Dark mode', | |
| key: 'theme', | |
| icon: renderIcon(themeStore.theme === 'dark' ? SunnyOutline : MoonOutline), | |
| }, | |
| { type: 'divider', key: 'd2' }, | |
| { | |
| label: 'Sign out', | |
| key: 'logout', | |
| icon: renderIcon(LogOutOutline), | |
| }, | |
| ]) | |
| function onSelect(key: string) { | |
| switch (key) { | |
| case 'account': | |
| // No dedicated page yet — open modal at user info step | |
| authStore.openAuthModal() | |
| break | |
| case 'api-keys': | |
| router.push('/api-keys') | |
| break | |
| case 'theme': | |
| themeStore.toggle() | |
| break | |
| case 'logout': | |
| authStore.logout() | |
| break | |
| } | |
| } | |
| </script> | |
| <style scoped lang="scss"> | |
| .user-menu-trigger { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 4px 10px 4px 4px; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border); | |
| border-radius: 999px; | |
| cursor: pointer; | |
| transition: all 0.2s var(--ease-out); | |
| color: var(--text-primary); | |
| &:hover { | |
| border-color: var(--primary); | |
| background: var(--bg-card); | |
| } | |
| } | |
| .avatar { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); | |
| color: white; | |
| font-weight: 700; | |
| font-size: 12px; | |
| font-family: var(--font-display); | |
| } | |
| .user-meta { | |
| display: flex; | |
| flex-direction: column; | |
| line-height: 1.1; | |
| text-align: left; | |
| } | |
| .user-name { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| max-width: 110px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .user-tier { | |
| font-size: 9px; | |
| font-weight: 700; | |
| letter-spacing: 0.1em; | |
| color: var(--primary); | |
| font-family: var(--font-mono); | |
| } | |
| .chevron { | |
| color: var(--text-muted); | |
| } | |
| @media (max-width: 768px) { | |
| .user-meta { display: none; } | |
| .chevron { display: none; } | |
| } | |
| </style> | |
| <style lang="scss"> | |
| // Global: dropdown menu header styling | |
| .menu-header { | |
| padding: 4px 12px 8px; | |
| .menu-header-email { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| word-break: break-all; | |
| } | |
| .menu-header-tier { | |
| font-size: 10px; | |
| color: var(--primary); | |
| font-weight: 700; | |
| letter-spacing: 0.08em; | |
| margin-top: 2px; | |
| } | |
| } | |
| </style> | |