Add light/dark mode support with CSS variables

This commit is contained in:
Jennie Robinson Faber 2025-10-06 19:54:20 +01:00
parent 970b185151
commit fb02688166
25 changed files with 1293 additions and 1177 deletions

View file

@ -1,7 +1,7 @@
<template>
<div>
<!-- Page Header -->
<PageHeader
<PageHeader
title="Login"
subtitle="Welcome back! Sign in to access your Ghost Guild account and connect with the cooperative community."
theme="blue"
@ -9,18 +9,20 @@
/>
<!-- Login Form -->
<section class="py-20 bg-white dark:bg-gray-900">
<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-blue-600 dark:text-blue-400 mb-4">
<h2 class="text-3xl font-bold text-primary-500 mb-4">
Passwordless Login
</h2>
<p class="text-gray-600 dark:text-gray-300">
<p class="text-[--ui-text-muted]">
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">
<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>
@ -34,7 +36,7 @@
</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="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" />
@ -46,8 +48,9 @@
<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 class="text-primary-700 text-sm mt-3">
We'll send you a secure login link via email. No password
needed!
</p>
</div>
@ -66,23 +69,31 @@
</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.
<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 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
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-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">
<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>
@ -92,19 +103,25 @@
</section>
<!-- Forgot Password -->
<section id="forgot-password" class="py-20 bg-gray-50 dark:bg-gray-800">
<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-blue-600 dark:text-blue-400 mb-4">
<h2 class="text-3xl font-bold text-primary-500 mb-4">
Forgot Password
</h2>
<p class="text-gray-600 dark:text-gray-300">
<p class="text-[--ui-text-muted]">
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">
<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
@ -117,7 +134,7 @@
</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="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" />
@ -129,8 +146,9 @@
<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 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>
@ -150,41 +168,44 @@
</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">
<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 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
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-white dark:bg-gray-900">
<section class="py-20 bg-[--ui-bg]">
<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>
<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-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 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
<UButton
@click="scrollToLoginForm"
size="xl"
size="xl"
color="primary"
class="px-12"
>
@ -195,13 +216,13 @@
</section>
<!-- Access Your Dashboard -->
<section class="py-20 bg-blue-50 dark:bg-blue-900/20">
<section class="py-20 bg-primary-50">
<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">
<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" />
@ -209,53 +230,59 @@
<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:
<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-gray-700 dark:text-gray-300">Community forums and discussions</span>
<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-gray-700 dark:text-gray-300">Member directory and networking</span>
<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-gray-700 dark:text-gray-300">Educational resources and workshops</span>
<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-gray-700 dark:text-gray-300">Cooperative development tools</span>
<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-gray-700 dark:text-gray-300">Mentorship opportunities</span>
<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-gray-700 dark:text-gray-300">Project collaboration spaces</span>
<span class="text-[--ui-text]"
>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"
>
<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>
@ -266,112 +293,112 @@
</template>
<script setup>
import { reactive, ref, computed } from 'vue'
import { reactive, ref, computed } from "vue";
// Login form state
const loginForm = reactive({
email: ''
})
email: "",
});
// Forgot password form state
const forgotPasswordForm = reactive({
email: ''
})
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('')
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('@')
})
return loginForm.email && loginForm.email.includes("@");
});
// Login handler
const handleLogin = async () => {
if (isLoggingIn.value) return
if (isLoggingIn.value) return;
isLoggingIn.value = true
loginError.value = ''
loginSuccess.value = false
isLoggingIn.value = true;
loginError.value = "";
loginSuccess.value = false;
try {
// Call the passwordless login API
const response = await $fetch('/api/auth/login', {
method: 'POST',
const response = await $fetch("/api/auth/login", {
method: "POST",
body: {
email: loginForm.email
}
})
email: loginForm.email,
},
});
if (response.success) {
loginSuccess.value = true
loginError.value = ''
loginSuccess.value = true;
loginError.value = "";
// Clear the form
loginForm.email = ''
loginForm.email = "";
}
} catch (err) {
console.error('Login error:', 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.'
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.'
loginError.value = "Failed to send login email. Please try again later.";
} else {
loginError.value = err.statusMessage || 'Something went wrong. Please try again.'
loginError.value =
err.statusMessage || "Something went wrong. Please try again.";
}
} finally {
isLoggingIn.value = false
isLoggingIn.value = false;
}
}
};
// Forgot password handler
const handleForgotPassword = async () => {
if (isResettingPassword.value) return
if (isResettingPassword.value) return;
isResettingPassword.value = true
resetError.value = ''
resetSuccess.value = false
isResettingPassword.value = true;
resetError.value = "";
resetSuccess.value = false;
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500))
resetSuccess.value = true
await new Promise((resolve) => setTimeout(resolve, 1500));
resetSuccess.value = true;
// Reset form after success
setTimeout(() => {
forgotPasswordForm.email = ''
resetSuccess.value = false
}, 5000)
forgotPasswordForm.email = "";
resetSuccess.value = false;
}, 5000);
} catch (err) {
console.error('Password reset error:', err)
resetError.value = 'Failed to send reset email. Please try again.'
console.error("Password reset error:", err);
resetError.value = "Failed to send reset email. Please try again.";
} finally {
isResettingPassword.value = false
isResettingPassword.value = false;
}
}
};
// Scroll functions
const scrollToLoginForm = () => {
const formSection = document.querySelector('form')
const formSection = document.querySelector("form");
if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'center' })
formSection.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
};
const scrollToForgotPassword = () => {
const forgotSection = document.getElementById('forgot-password')
const forgotSection = document.getElementById("forgot-password");
if (forgotSection) {
forgotSection.scrollIntoView({ behavior: 'smooth', block: 'start' })
forgotSection.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
</script>
};
</script>