- Storage Integration: * Remove Firebase Storage dependency and useFirebaseStorage composable * Implement direct MinIO uploads via POST /storage/upload with multipart/form-data * Upload canvas JSON, preview PNG, and production PNG as separate objects * Store public URLs and metadata in design records - Authentication & Registration: * Add email/password registration page with validation * Integrate backend user session via /auth/login endpoint * Store backendUser.id as ownerId in design records * Auto-sync backend session on Firebase auth state changes - User Account Pages: * Create profile page showing user details and backend session info * Create orders page with transaction history filtered by customerEmail * Add server proxy /api/orders to forward GET /transactions queries - Navigation Improvements: * Replace inline auth buttons with avatar dropdown menu * Add Profile, Orders, and Logout options to dropdown * Implement outside-click and route-change handlers for dropdown * Display user initials in avatar badge - API Updates: * Update transactions endpoint to accept amount as string * Format amount with .toFixed(2) in checkout success flow * Query orders by customerEmail instead of ownerId for consistency
158 lines
5.5 KiB
Vue
158 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
const { user, backendUser, signOut, initAuth, isLoading } = useAuth()
|
|
const loginModal = useLoginModal()
|
|
const route = useRoute()
|
|
|
|
const dropdownRef = ref<HTMLElement | null>(null)
|
|
const showMenu = ref(false)
|
|
|
|
const displayName = computed(() => backendUser.value?.name || user.value?.displayName || backendUser.value?.email || user.value?.email || "Account")
|
|
|
|
const avatarInitials = computed(() => {
|
|
const source = displayName.value
|
|
if (!source) {
|
|
return "S"
|
|
}
|
|
const parts = source.trim().split(/\s+/)
|
|
if (parts.length === 1) {
|
|
return parts[0].charAt(0).toUpperCase()
|
|
}
|
|
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
|
|
})
|
|
|
|
const openLoginModal = () => {
|
|
loginModal.value = true
|
|
}
|
|
|
|
const handleSignOut = async () => {
|
|
try {
|
|
await signOut()
|
|
showMenu.value = false
|
|
} catch (error) {
|
|
console.error('Sign out failed:', error)
|
|
}
|
|
}
|
|
|
|
const toggleMenu = () => {
|
|
showMenu.value = !showMenu.value
|
|
}
|
|
|
|
const closeMenu = () => {
|
|
if (showMenu.value) {
|
|
showMenu.value = false
|
|
}
|
|
}
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (!dropdownRef.value) {
|
|
return
|
|
}
|
|
if (!dropdownRef.value.contains(event.target as Node)) {
|
|
closeMenu()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
initAuth()
|
|
document.addEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
document.removeEventListener('click', handleClickOutside)
|
|
})
|
|
|
|
watch(
|
|
() => route.fullPath,
|
|
() => {
|
|
closeMenu()
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<nav class="sticky top-0 z-30 border-b border-slate-800/60 bg-slate-950/80 backdrop-blur">
|
|
<div class="mx-auto max-w-6xl px-4 py-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center justify-between gap-6">
|
|
<NuxtLink to="/" class="text-lg font-semibold text-white transition hover:text-sky-300 sm:text-xl">
|
|
Slipmatz
|
|
</NuxtLink>
|
|
<div class="flex items-center gap-3">
|
|
<!-- Show user info and logout when authenticated -->
|
|
<div v-if="user && !isLoading" ref="dropdownRef" class="relative flex items-center gap-3">
|
|
<span class="hidden text-sm text-slate-300 sm:inline">{{ backendUser?.email || user.email }}</span>
|
|
<button
|
|
type="button"
|
|
class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-800 text-sm font-semibold uppercase text-slate-200 transition hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
|
|
@click.stop="toggleMenu"
|
|
aria-haspopup="menu"
|
|
:aria-expanded="showMenu"
|
|
>
|
|
{{ avatarInitials }}
|
|
</button>
|
|
|
|
<transition
|
|
enter-active-class="transition ease-out duration-100"
|
|
enter-from-class="transform opacity-0 scale-95"
|
|
enter-to-class="transform opacity-100 scale-100"
|
|
leave-active-class="transition ease-in duration-75"
|
|
leave-from-class="transform opacity-100 scale-100"
|
|
leave-to-class="transform opacity-0 scale-95"
|
|
>
|
|
<div
|
|
v-if="showMenu"
|
|
class="absolute right-0 top-12 w-56 rounded-2xl border border-slate-800/70 bg-slate-900/95 p-3 shadow-xl shadow-slate-950/50 backdrop-blur"
|
|
role="menu"
|
|
>
|
|
<p class="px-3 text-xs uppercase tracking-[0.25em] text-slate-500">Signed in as</p>
|
|
<p class="px-3 text-sm font-medium text-slate-200">{{ displayName }}</p>
|
|
<p class="px-3 text-xs text-slate-500">{{ backendUser?.email || user.email }}</p>
|
|
<div class="my-3 h-px bg-slate-800"></div>
|
|
<NuxtLink
|
|
to="/profile"
|
|
class="flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-slate-200 transition hover:bg-slate-800"
|
|
role="menuitem"
|
|
>
|
|
Profile
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
to="/orders"
|
|
class="flex items-center gap-2 rounded-xl px-3 py-2 text-sm font-medium text-slate-200 transition hover:bg-slate-800"
|
|
role="menuitem"
|
|
>
|
|
Orders
|
|
</NuxtLink>
|
|
<button
|
|
type="button"
|
|
class="mt-2 flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm font-semibold text-rose-300 transition hover:bg-rose-500/10"
|
|
@click="handleSignOut"
|
|
role="menuitem"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
|
|
<!-- Show login button when not authenticated -->
|
|
<div v-else-if="!isLoading" class="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
@click="openLoginModal"
|
|
class="rounded-full border border-slate-700/80 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-slate-500 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
|
|
>
|
|
Login
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-else class="flex items-center gap-3">
|
|
<div class="h-8 w-16 animate-pulse rounded bg-slate-700"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<LoginModal />
|
|
</nav>
|
|
</template>
|