- 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
157 lines
5.3 KiB
Vue
157 lines
5.3 KiB
Vue
<script setup lang="ts">
|
|
const router = useRouter();
|
|
const loginModal = useLoginModal();
|
|
const { user, backendUser, initAuth, isLoading, signOut } = useAuth();
|
|
|
|
onMounted(() => {
|
|
initAuth();
|
|
});
|
|
|
|
const isAuthenticated = computed(() => Boolean(user.value));
|
|
|
|
const displayName = computed(() => {
|
|
return (
|
|
backendUser.value?.name ||
|
|
user.value?.displayName ||
|
|
backendUser.value?.email ||
|
|
user.value?.email ||
|
|
"Slipmat Creator"
|
|
);
|
|
});
|
|
|
|
const displayEmail = computed(() => backendUser.value?.email || user.value?.email || "Unknown");
|
|
|
|
const displayId = computed(() => backendUser.value?.id || user.value?.uid || null);
|
|
|
|
const lastLogin = computed(() => {
|
|
const raw =
|
|
backendUser.value?.lastLogin ||
|
|
backendUser.value?.updatedAt ||
|
|
user.value?.metadata?.lastSignInTime ||
|
|
user.value?.metadata?.creationTime ||
|
|
null;
|
|
|
|
return raw ? new Date(raw) : null;
|
|
});
|
|
|
|
const profileFields = computed(() => {
|
|
const entries: Array<{ label: string; value: string | null }> = [
|
|
{ label: "Display name", value: displayName.value },
|
|
{ label: "Email", value: displayEmail.value },
|
|
];
|
|
|
|
if (displayId.value) {
|
|
entries.push({ label: "User ID", value: displayId.value });
|
|
}
|
|
|
|
if (lastLogin.value) {
|
|
entries.push({ label: "Last login", value: lastLogin.value.toLocaleString() });
|
|
}
|
|
|
|
if (backendUser.value?.role) {
|
|
entries.push({ label: "Role", value: String(backendUser.value.role) });
|
|
}
|
|
|
|
if (backendUser.value?.createdAt) {
|
|
entries.push({ label: "Created", value: new Date(backendUser.value.createdAt).toLocaleString() });
|
|
}
|
|
|
|
return entries;
|
|
});
|
|
|
|
const openLogin = () => {
|
|
loginModal.value = true;
|
|
router.push("/");
|
|
};
|
|
|
|
const handleSignOut = async () => {
|
|
try {
|
|
await signOut();
|
|
router.push("/");
|
|
} catch (error) {
|
|
console.error("Sign out failed", error);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<main class="min-h-screen bg-slate-950 pb-16 text-slate-100">
|
|
<AppNavbar />
|
|
|
|
<section class="mx-auto flex max-w-3xl flex-col gap-8 px-4 pt-16">
|
|
<header class="space-y-3">
|
|
<p class="text-sm uppercase tracking-[0.35em] text-sky-400">Account</p>
|
|
<h1 class="text-3xl font-semibold text-white">Profile</h1>
|
|
<p class="text-sm text-slate-400">
|
|
View your Slipmatz account details and manage sessions. Changes to your profile are controlled through your authentication provider.
|
|
</p>
|
|
</header>
|
|
|
|
<div v-if="isLoading" class="grid gap-4">
|
|
<div class="h-28 animate-pulse rounded-2xl border border-slate-800 bg-slate-900/50" />
|
|
<div class="h-28 animate-pulse rounded-2xl border border-slate-800 bg-slate-900/50" />
|
|
</div>
|
|
|
|
<div v-else-if="!isAuthenticated" class="rounded-2xl border border-slate-800/60 bg-slate-900/80 p-6">
|
|
<h2 class="text-xl font-semibold text-white">You're signed out</h2>
|
|
<p class="mt-2 text-sm text-slate-400">
|
|
Sign in to view your profile information and order history.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
class="mt-4 rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-sky-500"
|
|
@click="openLogin"
|
|
>
|
|
Sign in
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="space-y-6">
|
|
<div class="rounded-2xl border border-slate-800/60 bg-slate-900/80 p-6">
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<h2 class="text-xl font-semibold text-white">{{ displayName }}</h2>
|
|
<p class="text-sm text-slate-400">{{ displayEmail }}</p>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<NuxtLink
|
|
to="/orders"
|
|
class="rounded-md border border-slate-700/70 px-4 py-2 text-sm font-medium text-slate-200 transition hover:border-sky-500 hover:text-white"
|
|
>
|
|
View order history
|
|
</NuxtLink>
|
|
<button
|
|
type="button"
|
|
class="rounded-md border border-rose-500/70 px-4 py-2 text-sm font-semibold text-rose-200 transition hover:bg-rose-500/10"
|
|
@click="handleSignOut"
|
|
>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-2xl border border-slate-800/60 bg-slate-900/80 p-6">
|
|
<h3 class="text-lg font-semibold text-white">Account details</h3>
|
|
<dl class="mt-4 grid gap-4 text-sm text-slate-300 sm:grid-cols-2">
|
|
<div v-for="field in profileFields" :key="field.label" class="space-y-1">
|
|
<dt class="text-xs uppercase tracking-[0.25em] text-slate-500">{{ field.label }}</dt>
|
|
<dd class="text-sm text-slate-200 break-all">{{ field.value }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div v-if="backendUser" class="rounded-2xl border border-slate-800/60 bg-slate-900/80 p-6">
|
|
<h3 class="text-lg font-semibold text-white">Backend session</h3>
|
|
<p class="text-sm text-slate-400">
|
|
The following data is provided by the Slipmatz backend and may include additional metadata used for order fulfillment.
|
|
</p>
|
|
<pre class="mt-4 overflow-x-auto rounded-xl bg-slate-950/80 p-4 text-xs text-slate-300">
|
|
{{ JSON.stringify(backendUser, null, 2) }}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|