377 lines
No EOL
13 KiB
Vue
377 lines
No EOL
13 KiB
Vue
<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-white dark:bg-gray-900">
|
|
<UContainer class="max-w-md">
|
|
<div class="text-center mb-12">
|
|
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-4">
|
|
Passwordless Login
|
|
</h2>
|
|
<p class="text-gray-600 dark:text-gray-300">
|
|
Enter your email to receive a secure login link
|
|
</p>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
|
<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-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<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-blue-700 dark:text-blue-300 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 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
|
<p class="text-green-700 dark:text-green-300 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 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
|
<p class="text-red-700 dark:text-red-300 text-center">
|
|
❌ {{ loginError }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Sign Up Link -->
|
|
<div class="mt-6 text-center">
|
|
<p class="text-gray-600 dark:text-gray-400">
|
|
Don't have an account?
|
|
<NuxtLink to="/join" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
|
|
Join Ghost Guild
|
|
</NuxtLink>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</section>
|
|
|
|
<!-- Forgot Password -->
|
|
<section id="forgot-password" class="py-20 bg-gray-50 dark:bg-gray-800">
|
|
<UContainer class="max-w-md">
|
|
<div class="text-center mb-12">
|
|
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-4">
|
|
Forgot Password
|
|
</h2>
|
|
<p class="text-gray-600 dark:text-gray-300">
|
|
Enter your email to receive a password reset link
|
|
</p>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
|
|
<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-blue-50 dark:bg-blue-900/20 p-4 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<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-blue-700 dark:text-blue-300 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 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
|
<p class="text-green-700 dark:text-green-300 text-center">
|
|
✅ Password reset link sent! Check your email.
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="resetError" class="mt-6 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
|
|
<p class="text-red-700 dark:text-red-300 text-center">
|
|
❌ {{ resetError }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UContainer>
|
|
</section>
|
|
|
|
<!-- Sign In CTA -->
|
|
<section class="py-20 bg-white dark:bg-gray-900">
|
|
<UContainer>
|
|
<div class="text-center max-w-2xl mx-auto">
|
|
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 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-gray-600 dark:text-gray-300 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-blue-50 dark:bg-blue-900/20">
|
|
<UContainer>
|
|
<div class="text-center max-w-3xl mx-auto">
|
|
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 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-white dark:bg-gray-800 rounded-2xl p-8 shadow-lg border border-blue-200 dark:border-blue-800 mb-8">
|
|
<p class="text-lg text-gray-600 dark:text-gray-300 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-gray-700 dark:text-gray-300">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-gray-700 dark:text-gray-300">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-gray-700 dark:text-gray-300">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-gray-700 dark:text-gray-300">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-gray-700 dark:text-gray-300">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-gray-700 dark:text-gray-300">Project collaboration spaces</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center">
|
|
<p class="text-gray-600 dark:text-gray-300 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> |