Enhance application structure: Add runtime configuration for environment variables, integrate new dependencies for Cloudinary and UI components, and refactor member management features including improved forms and member dashboard. Update styles and layout for better user experience.

This commit is contained in:
Jennie Robinson Faber 2025-08-27 16:49:51 +01:00
parent 6e7e27ac4e
commit e4a0a9ab0f
61 changed files with 7902 additions and 950 deletions

377
app/pages/login.vue Normal file
View file

@ -0,0 +1,377 @@
<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>