Many an update!
This commit is contained in:
parent
85195d6c7a
commit
d588c49946
35 changed files with 3528 additions and 1142 deletions
|
|
@ -1,404 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- Page Header -->
|
||||
<PageHeader
|
||||
title="Login"
|
||||
subtitle="Welcome back! Sign in to access your Ghost Guild account and connect with the cooperative community."
|
||||
theme="blue"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<!-- Login Form -->
|
||||
<section class="py-20 bg-[--ui-bg]">
|
||||
<UContainer class="max-w-md">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-primary-500 mb-4">
|
||||
Passwordless Login
|
||||
</h2>
|
||||
<p class="text-[--ui-text-muted]">
|
||||
Enter your email to receive a secure login link
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-[--ui-bg-elevated] rounded-2xl p-8 shadow-xl border border-primary-200"
|
||||
>
|
||||
<UForm :state="loginForm" class="space-y-6" @submit="handleLogin">
|
||||
<!-- Email Field -->
|
||||
<UFormField label="Email Address" name="email" required>
|
||||
<UInput
|
||||
v-model="loginForm.email"
|
||||
type="email"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<!-- Passwordless Info -->
|
||||
<div class="bg-primary-50 p-4 rounded-lg border border-primary-200">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="space-y-1 flex-shrink-0 mt-1">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<div class="w-2 h-2 bg-blue-400 rounded-full" />
|
||||
</div>
|
||||
<div class="space-y-2 flex-1">
|
||||
<div class="h-1 bg-blue-500 rounded-full w-full" />
|
||||
<div class="h-1 bg-blue-300 rounded-full w-3/4" />
|
||||
<div class="h-1 bg-blue-200 rounded-full w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-primary-700 text-sm mt-3">
|
||||
We'll send you a secure login link via email. No password
|
||||
needed!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<div class="flex justify-center pt-4">
|
||||
<UButton
|
||||
type="submit"
|
||||
:loading="isLoggingIn"
|
||||
:disabled="!isLoginFormValid"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
>
|
||||
Send Magic Link
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
<div
|
||||
v-if="loginSuccess"
|
||||
class="mt-6 p-4 bg-green-50 rounded-lg border border-green-200"
|
||||
>
|
||||
<p class="text-green-700 text-center">
|
||||
✅ Magic link sent! Check your email and click the link to sign
|
||||
in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loginError"
|
||||
class="mt-6 p-4 bg-red-50 rounded-lg border border-red-200"
|
||||
>
|
||||
<p class="text-red-700 text-center">❌ {{ loginError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Sign Up Link -->
|
||||
<div class="mt-6 text-center">
|
||||
<p class="text-[--ui-text-muted]">
|
||||
Don't have an account?
|
||||
<NuxtLink
|
||||
to="/join"
|
||||
class="text-primary-500 hover:underline font-medium"
|
||||
>
|
||||
Join Ghost Guild
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Forgot Password -->
|
||||
<section id="forgot-password" class="py-20 bg-neutral-50">
|
||||
<UContainer class="max-w-md">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl font-bold text-primary-500 mb-4">
|
||||
Forgot Password
|
||||
</h2>
|
||||
<p class="text-[--ui-text-muted]">
|
||||
Enter your email to receive a password reset link
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-[--ui-bg-elevated] rounded-2xl p-8 shadow-xl border border-primary-200"
|
||||
>
|
||||
<UForm
|
||||
:state="forgotPasswordForm"
|
||||
class="space-y-6"
|
||||
@submit="handleForgotPassword"
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<UFormField label="Email Address" name="email" required>
|
||||
<UInput
|
||||
v-model="forgotPasswordForm.email"
|
||||
type="email"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<!-- Reset Instructions -->
|
||||
<div class="bg-primary-50 p-4 rounded-lg border border-primary-200">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="space-y-1 flex-shrink-0 mt-1">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<div class="w-2 h-2 bg-blue-400 rounded-full" />
|
||||
</div>
|
||||
<div class="space-y-2 flex-1">
|
||||
<div class="h-1 bg-blue-500 rounded-full w-full" />
|
||||
<div class="h-1 bg-blue-300 rounded-full w-3/4" />
|
||||
<div class="h-1 bg-blue-200 rounded-full w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-primary-700 text-sm mt-3">
|
||||
We'll send you a secure link to reset your password. Check your
|
||||
email inbox and spam folder.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Send Reset Link Button -->
|
||||
<div class="flex justify-center pt-4">
|
||||
<UButton
|
||||
type="submit"
|
||||
:loading="isResettingPassword"
|
||||
:disabled="!forgotPasswordForm.email"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
>
|
||||
Send Reset Link
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
<div
|
||||
v-if="resetSuccess"
|
||||
class="mt-6 p-4 bg-green-50 rounded-lg border border-green-200"
|
||||
>
|
||||
<p class="text-green-700 text-center">
|
||||
✅ Password reset link sent! Check your email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="resetError"
|
||||
class="mt-6 p-4 bg-red-50 rounded-lg border border-red-200"
|
||||
>
|
||||
<p class="text-red-700 text-center">❌ {{ resetError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Sign In CTA -->
|
||||
<section class="py-20 bg-[--ui-bg]">
|
||||
<UContainer>
|
||||
<div class="text-center max-w-2xl mx-auto">
|
||||
<h2 class="text-3xl font-bold text-primary-500 mb-8">Sign In</h2>
|
||||
|
||||
<div class="space-y-4 mb-8">
|
||||
<div class="h-2 bg-blue-500 rounded-full w-64 mx-auto" />
|
||||
<div class="h-2 bg-blue-300 rounded-full w-48 mx-auto" />
|
||||
</div>
|
||||
|
||||
<p class="text-lg text-[--ui-text-muted] mb-8">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ready to
|
||||
access your account and connect with the community?
|
||||
</p>
|
||||
|
||||
<UButton
|
||||
@click="scrollToLoginForm"
|
||||
size="xl"
|
||||
color="primary"
|
||||
class="px-12"
|
||||
>
|
||||
Login Now
|
||||
</UButton>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
|
||||
<!-- Access Your Dashboard -->
|
||||
<section class="py-20 bg-primary-50">
|
||||
<UContainer>
|
||||
<div class="text-center max-w-3xl mx-auto">
|
||||
<h2 class="text-3xl font-bold text-primary-500 mb-8">
|
||||
Access Your Dashboard
|
||||
</h2>
|
||||
|
||||
<div class="space-y-3 mb-8">
|
||||
<div class="h-2 bg-blue-500 rounded-full w-full max-w-lg mx-auto" />
|
||||
<div class="h-2 bg-blue-400 rounded-full w-full max-w-md mx-auto" />
|
||||
<div class="h-2 bg-blue-300 rounded-full w-full max-w-sm mx-auto" />
|
||||
<div class="h-2 bg-blue-200 rounded-full w-full max-w-xs mx-auto" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-[--ui-bg-elevated] rounded-2xl p-8 shadow-lg border border-primary-200 mb-8"
|
||||
>
|
||||
<p class="text-lg text-[--ui-text-muted] mb-6">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Once
|
||||
you're logged in, you'll have access to:
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-left">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<span class="text-[--ui-text]"
|
||||
>Community forums and discussions</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-blue-400 rounded-full" />
|
||||
<span class="text-[--ui-text]"
|
||||
>Member directory and networking</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-blue-300 rounded-full" />
|
||||
<span class="text-[--ui-text]"
|
||||
>Educational resources and workshops</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-emerald-500 rounded-full" />
|
||||
<span class="text-[--ui-text]"
|
||||
>Cooperative development tools</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full" />
|
||||
<span class="text-[--ui-text]">Mentorship opportunities</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-emerald-300 rounded-full" />
|
||||
<span class="text-[--ui-text]"
|
||||
>Project collaboration spaces</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="text-[--ui-text-muted] mb-4">New to Ghost Guild?</p>
|
||||
<UButton to="/join" variant="outline" size="lg" class="px-8">
|
||||
Create Your Account
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UContainer>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed } from "vue";
|
||||
|
||||
// Login form state
|
||||
const loginForm = reactive({
|
||||
email: "",
|
||||
});
|
||||
|
||||
// Forgot password form state
|
||||
const forgotPasswordForm = reactive({
|
||||
email: "",
|
||||
});
|
||||
|
||||
// UI state
|
||||
const isLoggingIn = ref(false);
|
||||
const isResettingPassword = ref(false);
|
||||
const loginSuccess = ref(false);
|
||||
const loginError = ref("");
|
||||
const resetSuccess = ref(false);
|
||||
const resetError = ref("");
|
||||
|
||||
// Form validation
|
||||
const isLoginFormValid = computed(() => {
|
||||
return loginForm.email && loginForm.email.includes("@");
|
||||
});
|
||||
|
||||
// Login handler
|
||||
const handleLogin = async () => {
|
||||
if (isLoggingIn.value) return;
|
||||
|
||||
isLoggingIn.value = true;
|
||||
loginError.value = "";
|
||||
loginSuccess.value = false;
|
||||
|
||||
try {
|
||||
// Call the passwordless login API
|
||||
const response = await $fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: {
|
||||
email: loginForm.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loginSuccess.value = true;
|
||||
loginError.value = "";
|
||||
|
||||
// Clear the form
|
||||
loginForm.email = "";
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
|
||||
// Handle different error types
|
||||
if (err.statusCode === 404) {
|
||||
loginError.value =
|
||||
"No account found with that email address. Please check your email or create an account.";
|
||||
} else if (err.statusCode === 500) {
|
||||
loginError.value = "Failed to send login email. Please try again later.";
|
||||
} else {
|
||||
loginError.value =
|
||||
err.statusMessage || "Something went wrong. Please try again.";
|
||||
}
|
||||
} finally {
|
||||
isLoggingIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Forgot password handler
|
||||
const handleForgotPassword = async () => {
|
||||
if (isResettingPassword.value) return;
|
||||
|
||||
isResettingPassword.value = true;
|
||||
resetError.value = "";
|
||||
resetSuccess.value = false;
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
resetSuccess.value = true;
|
||||
|
||||
// Reset form after success
|
||||
setTimeout(() => {
|
||||
forgotPasswordForm.email = "";
|
||||
resetSuccess.value = false;
|
||||
}, 5000);
|
||||
} catch (err) {
|
||||
console.error("Password reset error:", err);
|
||||
resetError.value = "Failed to send reset email. Please try again.";
|
||||
} finally {
|
||||
isResettingPassword.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Scroll functions
|
||||
const scrollToLoginForm = () => {
|
||||
const formSection = document.querySelector("form");
|
||||
if (formSection) {
|
||||
formSection.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToForgotPassword = () => {
|
||||
const forgotSection = document.getElementById("forgot-password");
|
||||
if (forgotSection) {
|
||||
forgotSection.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue