- 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
187 lines
5.6 KiB
Vue
187 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
const router = useRouter();
|
|
const { user, registerWithEmail, signInWithGoogle, isLoading, error } = useAuth();
|
|
const loginModal = useLoginModal();
|
|
|
|
const email = ref("");
|
|
const password = ref("");
|
|
const confirmPassword = ref("");
|
|
const isSubmitting = ref(false);
|
|
const localError = ref<string | null>(null);
|
|
|
|
const isProcessing = computed(() => isSubmitting.value || isLoading.value);
|
|
const combinedError = computed(() => localError.value || error.value || null);
|
|
|
|
const redirectIfAuthenticated = (maybeUser: unknown) => {
|
|
if (!process.client) {
|
|
return;
|
|
}
|
|
if (maybeUser) {
|
|
router.replace("/");
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => user.value,
|
|
(currentUser) => {
|
|
if (currentUser) {
|
|
redirectIfAuthenticated(currentUser);
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const handleRegister = async () => {
|
|
if (!process.client) {
|
|
return;
|
|
}
|
|
|
|
localError.value = null;
|
|
|
|
if (!email.value.trim()) {
|
|
localError.value = "Email is required.";
|
|
return;
|
|
}
|
|
|
|
if (password.value.length < 6) {
|
|
localError.value = "Password must be at least 6 characters.";
|
|
return;
|
|
}
|
|
|
|
if (password.value !== confirmPassword.value) {
|
|
localError.value = "Passwords do not match.";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isSubmitting.value = true;
|
|
await registerWithEmail(email.value.trim(), password.value);
|
|
await router.replace("/");
|
|
} catch (err: any) {
|
|
localError.value = err?.message ?? "Registration failed. Please try again.";
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
}
|
|
};
|
|
|
|
const handleGoogleRegister = async () => {
|
|
if (!process.client) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isSubmitting.value = true;
|
|
await signInWithGoogle();
|
|
await router.replace("/");
|
|
} catch (err: any) {
|
|
localError.value = err?.message ?? "Google sign-in failed. Please try again.";
|
|
} finally {
|
|
isSubmitting.value = false;
|
|
}
|
|
};
|
|
|
|
const goToSignIn = async () => {
|
|
if (!process.client) {
|
|
return;
|
|
}
|
|
|
|
loginModal.value = true;
|
|
await router.push("/");
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<main class="min-h-screen bg-slate-950 pb-16 text-slate-100">
|
|
<AppNavbar />
|
|
|
|
<section class="mx-auto flex max-w-md flex-col gap-8 px-4 pt-16">
|
|
<header class="space-y-3 text-center">
|
|
<p class="text-sm uppercase tracking-[0.35em] text-sky-400">Create Account</p>
|
|
<h1 class="text-3xl font-semibold text-white">Join Slipmatz</h1>
|
|
<p class="text-sm text-slate-400">
|
|
Sign up with email and password to save your designs and return to them anytime.
|
|
</p>
|
|
</header>
|
|
|
|
<form class="space-y-5 rounded-2xl border border-slate-800/60 bg-slate-900/80 p-6" @submit.prevent="handleRegister">
|
|
<div class="space-y-1">
|
|
<label for="email" class="block text-sm font-medium text-slate-300">Email</label>
|
|
<input
|
|
id="email"
|
|
v-model="email"
|
|
type="email"
|
|
required
|
|
autocomplete="email"
|
|
class="w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-white focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-500/40"
|
|
/>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="password" class="block text-sm font-medium text-slate-300">Password</label>
|
|
<input
|
|
id="password"
|
|
v-model="password"
|
|
type="password"
|
|
required
|
|
autocomplete="new-password"
|
|
minlength="6"
|
|
class="w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-white focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-500/40"
|
|
/>
|
|
<p class="text-xs text-slate-500">Minimum 6 characters.</p>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<label for="confirm-password" class="block text-sm font-medium text-slate-300">Confirm Password</label>
|
|
<input
|
|
id="confirm-password"
|
|
v-model="confirmPassword"
|
|
type="password"
|
|
required
|
|
autocomplete="new-password"
|
|
minlength="6"
|
|
class="w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-white focus:border-sky-400 focus:outline-none focus:ring-2 focus:ring-sky-500/40"
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
:disabled="isProcessing"
|
|
class="w-full rounded-md bg-emerald-500 px-4 py-2 text-sm font-semibold text-emerald-950 transition hover:bg-emerald-400 disabled:opacity-60"
|
|
>
|
|
{{ isProcessing ? "Creating account..." : "Create account" }}
|
|
</button>
|
|
|
|
<div class="my-2 flex items-center gap-3 text-xs uppercase tracking-[0.2em] text-slate-500">
|
|
<div class="flex-1 border-t border-slate-800"></div>
|
|
<span>or</span>
|
|
<div class="flex-1 border-t border-slate-800"></div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
:disabled="isProcessing"
|
|
class="w-full rounded-md border border-slate-700 bg-slate-900 px-4 py-2 text-sm font-semibold text-slate-200 transition hover:bg-slate-800 disabled:opacity-60"
|
|
@click="handleGoogleRegister"
|
|
>
|
|
Continue with Google
|
|
</button>
|
|
|
|
<p v-if="combinedError" class="rounded-md border border-rose-500/40 bg-rose-500/10 px-3 py-2 text-sm text-rose-200">
|
|
{{ combinedError }}
|
|
</p>
|
|
</form>
|
|
|
|
<p class="text-center text-sm text-slate-400">
|
|
Already have an account?
|
|
<NuxtLink
|
|
to="/"
|
|
class="text-sky-400 hover:text-sky-300"
|
|
@click.prevent="goToSignIn"
|
|
>
|
|
Sign in instead
|
|
</NuxtLink>
|
|
</p>
|
|
</section>
|
|
</main>
|
|
</template>
|