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

@ -18,7 +18,7 @@
<div
class="w-8 h-8 border-4 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-stone-300">Loading your dashboard...</p>
<p class="text-ghost-300">Loading your dashboard...</p>
</div>
</div>
@ -36,10 +36,10 @@
<template #header>
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<h1 class="text-2xl font-bold text-stone-100 ethereal-text">
<h1 class="text-2xl font-bold text-ghost-100 ethereal-text">
Welcome to Ghost Guild, {{ memberData?.name }}!
</h1>
<p class="text-stone-300 mt-2">
<p class="text-ghost-300 mt-2">
Your membership is active and you're part of our cooperative
community.
</p>
@ -53,7 +53,7 @@
</div>
<div v-else class="flex-shrink-0">
<div
class="w-16 h-16 bg-ghost-700 border border-ghost-600 flex items-center justify-center text-stone-200 font-bold text-xl"
class="w-16 h-16 bg-ghost-700 border border-ghost-600 flex items-center justify-center text-ghost-200 font-bold text-xl"
>
{{ memberData?.name?.charAt(0)?.toUpperCase() }}
</div>
@ -63,13 +63,13 @@
<div class="flex flex-wrap gap-4 text-sm">
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-stone-400">Circle:</span>
<span class="text-ghost-400">Circle:</span>
<span class="font-medium text-whisper-300 ml-1 capitalize">{{
memberData?.circle
}}</span>
</div>
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2">
<span class="text-stone-400">Contribution:</span>
<span class="text-ghost-400">Contribution:</span>
<span class="font-medium text-whisper-300 ml-1"
>${{ memberData?.contributionTier }} CAD/month</span
>
@ -86,28 +86,16 @@
}"
>
<template #header>
<h2 class="text-xl font-bold text-stone-100 ethereal-text">
<h2 class="text-xl font-bold text-ghost-100 ethereal-text">
Quick Links
</h2>
</template>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<UButton
to="/member/my-updates"
variant="outline"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
<template #leading>
<Icon name="heroicons:pencil-square" class="w-5 h-5" />
</template>
Post an Update
</UButton>
<UButton
disabled
variant="outline"
class="border-ghost-600 text-stone-500 cursor-not-allowed justify-start"
class="border-ghost-600 text-ghost-500 cursor-not-allowed justify-start"
block
title="Coming soon"
>
@ -120,7 +108,7 @@
<UButton
disabled
variant="outline"
class="border-ghost-600 text-stone-500 cursor-not-allowed justify-start"
class="border-ghost-600 text-ghost-500 cursor-not-allowed justify-start"
block
title="Coming soon"
>
@ -133,7 +121,7 @@
<UButton
disabled
variant="outline"
class="border-ghost-600 text-stone-500 cursor-not-allowed justify-start"
class="border-ghost-600 text-ghost-500 cursor-not-allowed justify-start"
block
title="Coming soon"
>
@ -146,7 +134,7 @@
<UButton
to="/member/profile"
variant="outline"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 justify-start"
block
>
<template #leading>
@ -179,10 +167,10 @@
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-stone-100">
<h3 class="text-lg font-semibold mb-2 text-ghost-100">
Upcoming Events
</h3>
<p class="text-stone-300 mb-4">
<p class="text-ghost-300 mb-4">
Discover and register for community events and workshops.
</p>
@ -191,7 +179,7 @@
to="/events"
variant="outline"
size="sm"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
View Events
</UButton>
@ -217,8 +205,8 @@
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-stone-100">Community</h3>
<p class="text-stone-300 mb-4">
<h3 class="text-lg font-semibold mb-2 text-ghost-100">Community</h3>
<p class="text-ghost-300 mb-4">
Connect with other members in your circle and beyond.
</p>
@ -227,7 +215,7 @@
to="/members"
variant="outline"
size="sm"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Browse Members
</UButton>
@ -253,10 +241,10 @@
</div>
</template>
<h3 class="text-lg font-semibold mb-2 text-stone-100">
<h3 class="text-lg font-semibold mb-2 text-ghost-100">
Account Settings
</h3>
<p class="text-stone-300 mb-4">
<p class="text-ghost-300 mb-4">
Manage your profile and membership settings.
</p>
@ -265,7 +253,7 @@
to="/member/profile#account"
variant="outline"
size="sm"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Manage Account
</UButton>
@ -283,14 +271,14 @@
>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-stone-100 ethereal-text">
<h2 class="text-xl font-bold text-ghost-100 ethereal-text">
Your Upcoming Events
</h2>
<UButton
to="/events"
variant="ghost"
size="sm"
class="text-stone-300 hover:text-stone-100"
class="text-ghost-300 hover:text-ghost-100"
>
Browse All Events
</UButton>
@ -334,10 +322,10 @@
/>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-stone-100 mb-1">
<h3 class="font-semibold text-ghost-100 mb-1">
{{ evt.title }}
</h3>
<div class="flex items-center gap-4 text-sm text-stone-400">
<div class="flex items-center gap-4 text-sm text-ghost-400">
<span class="flex items-center gap-1">
<Icon name="heroicons:calendar" class="w-4 h-4" />
{{ formatEventDate(evt.startDate) }}
@ -351,7 +339,7 @@
<div class="flex-shrink-0">
<Icon
name="heroicons:chevron-right"
class="w-5 h-5 text-stone-500"
class="w-5 h-5 text-ghost-500"
/>
</div>
</div>
@ -361,108 +349,20 @@
<div v-else class="text-center py-8">
<Icon
name="heroicons:calendar-days"
class="w-12 h-12 text-stone-600 mx-auto mb-3"
class="w-12 h-12 text-ghost-600 mx-auto mb-3"
/>
<p class="text-stone-400 mb-4">
<p class="text-ghost-400 mb-4">
You haven't registered for any upcoming events
</p>
<UButton
to="/events"
size="sm"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500"
class="border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Browse Events
</UButton>
</div>
</UCard>
<!-- Community Pulse - Recent Updates -->
<UCard
class="sparkle-field"
:ui="{
root: 'bg-ghost-900 border border-ghost-700',
header: 'border-b border-ghost-700 bg-ghost-900',
body: 'bg-ghost-900',
}"
>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-stone-100 ethereal-text">
Community Pulse
</h2>
<UButton
to="/updates"
variant="ghost"
size="sm"
class="text-stone-300 hover:text-stone-100"
>
View All
</UButton>
</div>
</template>
<div v-if="loadingUpdates" class="text-center py-8">
<div
class="w-6 h-6 border-2 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto"
></div>
</div>
<div v-else-if="recentUpdates.length" class="space-y-4">
<div
v-for="update in recentUpdates"
:key="update._id"
class="border-l-2 border-ghost-600 pl-4 py-2"
>
<div class="flex items-start gap-3">
<img
v-if="
update.author?.avatar && isValidAvatar(update.author.avatar)
"
:src="`/ghosties/Ghost-${capitalize(update.author.avatar)}.png`"
:alt="update.author.name"
class="w-8 h-8 flex-shrink-0"
/>
<div
v-else-if="update.author?.name"
class="w-8 h-8 bg-ghost-700 border border-ghost-600 flex items-center justify-center text-stone-200 text-xs font-bold flex-shrink-0"
>
{{ update.author.name.charAt(0).toUpperCase() }}
</div>
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 mb-1">
<span class="font-semibold text-stone-100 text-sm">
{{ update.author?.name }}
</span>
<span class="text-xs text-stone-500">
{{ formatTimeAgo(update.createdAt) }}
</span>
</div>
<p class="text-stone-300 text-sm line-clamp-2">
{{ update.content }}
</p>
<NuxtLink
:to="`/updates/${update._id}`"
class="text-xs text-whisper-400 hover:text-whisper-300 mt-1 inline-block"
>
Read more
</NuxtLink>
</div>
</div>
</div>
</div>
<div v-else class="text-center py-8">
<p class="text-stone-400 mb-4">No community updates yet</p>
<UButton
to="/updates/new"
size="sm"
variant="outline"
class="border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500"
>
Post the First Update
</UButton>
</div>
</UCard>
</div>
</UContainer>
</div>
@ -471,8 +371,6 @@
<script setup>
const { memberData, checkMemberStatus } = useAuth();
const recentUpdates = ref([]);
const loadingUpdates = ref(false);
const registeredEvents = ref([]);
const loadingEvents = ref(false);
@ -506,21 +404,6 @@ const { pending: authPending } = await useLazyAsyncData(
},
);
// Load recent updates
const loadRecentUpdates = async () => {
loadingUpdates.value = true;
try {
const response = await $fetch("/api/updates", {
params: { limit: 5, skip: 0 },
});
recentUpdates.value = response.updates;
} catch (error) {
console.error("Failed to load recent updates:", error);
} finally {
loadingUpdates.value = false;
}
};
// Load registered events
const loadRegisteredEvents = async () => {
console.log(
@ -575,23 +458,6 @@ const capitalize = (str) => {
.join("-");
};
const formatTimeAgo = (date) => {
const now = new Date();
const updateDate = new Date(date);
const diffInSeconds = Math.floor((now - updateDate) / 1000);
if (diffInSeconds < 60) return "just now";
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
if (diffInSeconds < 604800)
return `${Math.floor(diffInSeconds / 86400)}d ago`;
return updateDate.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
};
// Helper functions for event display
const getEventImageUrl = (featureImage) => {
if (!featureImage) return "";
@ -626,7 +492,6 @@ const formatEventTime = (dateString) => {
};
onMounted(() => {
loadRecentUpdates();
loadRegisteredEvents();
});

View file

@ -11,8 +11,8 @@
<UContainer class="px-4">
<!-- Stats -->
<div v-if="!pending" class="mb-8 flex items-center justify-between">
<div class="text-stone-300">
<span class="text-2xl font-bold text-stone-100">{{ total }}</span>
<div class="text-ghost-300">
<span class="text-2xl font-bold text-ghost-100">{{ total }}</span>
{{ total === 1 ? "update" : "updates" }} posted
</div>
<UButton to="/updates/new" icon="i-lucide-plus"> New Update </UButton>
@ -25,9 +25,9 @@
>
<div class="text-center">
<div
class="w-8 h-8 border-4 border-stone-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
class="w-8 h-8 border-4 border-ghost-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-stone-400">Loading your updates...</p>
<p class="text-ghost-400">Loading your updates...</p>
</div>
</div>
@ -62,7 +62,7 @@
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
class="text-stone-600"
class="text-ghost-600"
>
<path
stroke-linecap="round"
@ -72,10 +72,10 @@
/>
</svg>
</div>
<h3 class="text-lg font-medium text-stone-300 mb-2">
<h3 class="text-lg font-medium text-ghost-300 mb-2">
No updates yet
</h3>
<p class="text-stone-400 mb-6">
<p class="text-ghost-400 mb-6">
Share your first update with the community
</p>
<UButton to="/updates/new" icon="i-lucide-plus">

View file

@ -17,7 +17,7 @@
<div
class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-stone-400">Loading your profile...</p>
<p class="text-ghost-400">Loading your profile...</p>
</div>
</div>
@ -32,7 +32,7 @@
<!-- Basic Information -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
>
Basic Information
</h2>
@ -105,7 +105,7 @@
:class="
formData.avatar === ghost.value
? 'border-blue-400 bg-blue-500/20'
: 'border-stone-700 bg-stone-800/50 hover:border-stone-600'
: 'border-ghost-700 bg-ghost-800/50 hover:border-ghost-600'
"
@click="formData.avatar = ghost.value"
>
@ -134,7 +134,7 @@
<!-- Professional Info -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
>
Professional Information
</h2>
@ -203,7 +203,7 @@
<!-- Community Connections -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
>
Community Connections
</h2>
@ -219,7 +219,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Skills & Topics
</label>
@ -251,7 +251,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Details
</label>
@ -281,7 +281,7 @@
<!-- Tags input -->
<div>
<label
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Skills & Topics
</label>
@ -313,7 +313,7 @@
<!-- Description textarea -->
<div>
<label
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Details
</label>
@ -338,7 +338,7 @@
<!-- Peer Support -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
>
Peer Support
</h2>
@ -346,7 +346,7 @@
<div
class="mb-6 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-4"
>
<p class="text-stone-300 text-sm leading-relaxed">
<p class="text-ghost-300 text-sm leading-relaxed">
Offer guidance to fellow members through the
<NuxtLink
to="/peer-support"
@ -362,10 +362,10 @@
<div class="flex items-start gap-4">
<USwitch v-model="formData.peerSupportEnabled" />
<div>
<p class="font-medium text-stone-200">
<p class="font-medium text-ghost-200">
Offer Peer Support
</p>
<p class="text-sm text-stone-400 mt-1">
<p class="text-sm text-ghost-400 mt-1">
Make yourself available to support other members
</p>
</div>
@ -435,20 +435,20 @@
<label
v-for="topic in availableSupportTopics"
:key="topic"
class="flex items-center gap-3 p-3 rounded-lg border border-stone-700 hover:border-purple-500/50 transition-colors cursor-pointer"
class="flex items-center gap-3 p-3 rounded-lg border border-ghost-700 hover:border-purple-500/50 transition-colors cursor-pointer"
:class="
formData.peerSupportSupportTopics.includes(topic)
? 'bg-purple-500/10 border-purple-500/50'
: 'bg-stone-900/50'
: 'bg-ghost-900/50'
"
>
<input
type="checkbox"
:value="topic"
v-model="formData.peerSupportSupportTopics"
class="rounded border-stone-600 text-purple-500 focus:ring-purple-500 focus:ring-offset-0 bg-stone-800"
class="rounded border-ghost-600 text-purple-500 focus:ring-purple-500 focus:ring-offset-0 bg-ghost-800"
/>
<span class="text-stone-200">{{ topic }}</span>
<span class="text-ghost-200">{{ topic }}</span>
</label>
</div>
</UFormField>
@ -484,7 +484,7 @@
class="w-full"
/>
<template #hint>
<span class="text-xs text-stone-500">
<span class="text-xs text-ghost-500">
{{ formData.peerSupportMessage?.length || 0 }}/200
characters
</span>
@ -511,7 +511,7 @@
<!-- Directory Settings -->
<div>
<h2
class="text-2xl font-semibold mb-8 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-8 text-ghost-100 ethereal-text"
>
Directory Visibility
</h2>
@ -519,10 +519,10 @@
<div class="flex items-start gap-4">
<USwitch v-model="formData.showInDirectory" />
<div>
<p class="font-medium text-stone-200">
<p class="font-medium text-ghost-200">
Show in Member Directory
</p>
<p class="text-sm text-stone-400 mt-1">
<p class="text-sm text-ghost-400 mt-1">
Allow other members to discover and connect with you
through the directory
</p>
@ -576,34 +576,34 @@
<!-- Current Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
>
Current Membership
</h2>
<div
class="backdrop-blur-sm bg-stone-800/50 border border-stone-700 rounded-lg p-6 space-y-4"
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6 space-y-4"
>
<div class="flex items-start justify-between">
<div>
<p class="text-sm text-stone-400">Circle</p>
<p class="text-sm text-ghost-400">Circle</p>
<p
class="text-lg font-medium text-stone-100 capitalize"
class="text-lg font-medium text-ghost-100 capitalize"
>
{{ memberData.circle }}
</p>
</div>
<div>
<p class="text-sm text-stone-400">Contribution Level</p>
<p class="text-lg font-medium text-stone-100">
<p class="text-sm text-ghost-400">Contribution Level</p>
<p class="text-lg font-medium text-ghost-100">
${{ contributionTierDetails?.amount }}/month
</p>
</div>
</div>
<div v-if="memberData.subscriptionStartDate">
<p class="text-sm text-stone-400">Member Since</p>
<p class="text-stone-100">
<p class="text-sm text-ghost-400">Member Since</p>
<p class="text-ghost-100">
{{ formatDate(memberData.subscriptionStartDate) }}
</p>
</div>
@ -614,8 +614,8 @@
memberData.contributionTier !== '0'
"
>
<p class="text-sm text-stone-400">Next Billing Date</p>
<p class="text-stone-100">
<p class="text-sm text-ghost-400">Next Billing Date</p>
<p class="text-ghost-100">
{{ formatDate(memberData.nextBillingDate) }}
</p>
</div>
@ -625,15 +625,15 @@
<!-- Change Contribution Level -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
>
Change Contribution Level
</h2>
<div
class="backdrop-blur-sm bg-stone-800/50 border border-stone-700 rounded-lg p-6"
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6"
>
<p class="text-stone-300 mb-6">
<p class="text-ghost-300 mb-6">
Choose a new contribution level that works for you.
Changes will take effect on your next billing cycle.
</p>
@ -647,16 +647,16 @@
'w-full text-left p-4 rounded-lg border-2 transition-all',
selectedContributionTier === tier.value
? 'border-blue-400 bg-blue-500/20'
: 'border-stone-600 bg-stone-900/30 hover:border-stone-500',
: 'border-ghost-600 bg-ghost-900/30 hover:border-ghost-500',
]"
@click="selectedContributionTier = tier.value"
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-stone-100">
<p class="font-medium text-ghost-100">
{{ tier.label }}
</p>
<p class="text-sm text-stone-400 mt-1">
<p class="text-sm text-ghost-400 mt-1">
{{ tier.features[0] }}
</p>
</div>
@ -702,20 +702,20 @@
<!-- Cancel Membership -->
<div>
<h2
class="text-2xl font-semibold mb-6 text-stone-100 ethereal-text"
class="text-2xl font-semibold mb-6 text-ghost-100 ethereal-text"
>
Cancel Membership
</h2>
<div
class="backdrop-blur-sm bg-stone-800/50 border border-stone-700 rounded-lg p-6"
class="backdrop-blur-sm bg-ghost-800/50 border border-ghost-700 rounded-lg p-6"
>
<p class="text-stone-300 mb-4">
<p class="text-ghost-300 mb-4">
We're sorry to see you go. If you cancel, you'll lose
access to member benefits at the end of your current
billing period.
</p>
<p class="text-sm text-stone-400 mb-6">
<p class="text-sm text-ghost-400 mb-6">
Need a break? Consider switching to the free tier instead.
</p>
@ -1325,7 +1325,7 @@ useHead({
<style scoped>
/* Field labels - bright and readable */
:deep(label) {
color: rgb(231 229 228) !important; /* stone-200 */
color: rgb(231 229 228) !important; /* ghost-200 equivalent */
font-weight: 500;
text-align: left !important;
}
@ -1334,7 +1334,7 @@ useHead({
:deep([class*="description"]) {
color: rgb(
168 162 158
) !important; /* stone-400 - lighter than the dark background */
) !important; /* ghost-400 equivalent - lighter than the dark background */
opacity: 0.9;
}
@ -1344,22 +1344,22 @@ useHead({
width: 100% !important;
}
/* Input fields - ensure good contrast */
/* Input fields - respect light/dark mode */
:deep(input),
:deep(textarea) {
background-color: rgb(41 37 36) !important; /* stone-800 */
color: rgb(231 229 228) !important; /* stone-200 */
border-color: rgb(87 83 78) !important; /* stone-600 */
background-color: transparent !important;
color: var(--color-ghost-100) !important;
border-color: var(--color-ghost-600) !important;
}
:deep(input::placeholder),
:deep(textarea::placeholder) {
color: rgb(120 113 108) !important; /* stone-500 */
color: var(--color-ghost-500) !important;
}
:deep(input:focus),
:deep(textarea:focus) {
border-color: rgb(147 197 253) !important; /* blue-300 */
background-color: rgb(44 40 39) !important; /* slightly lighter stone */
border-color: rgb(147 197 253) !important;
background-color: transparent !important;
}
</style>