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

@ -6,8 +6,13 @@ export default defineAppConfig({
},
formField: {
slots: {
label: "block font-medium text-stone-200",
label: "block font-medium text-[--ui-text-dimmed]",
},
},
},
colorMode: {
preference: "system", // default value of $colorMode.preference
fallback: "dark", // fallback value if not system preference found
classSuffix: "", // default is '', set to '-mode' to have 'dark-mode' and 'light-mode'
},
});

View file

@ -2,14 +2,52 @@
@import "tailwindcss";
@import "@nuxt/ui";
@theme static {
@theme {
/* Font families */
--font-sans: "Inter", sans-serif;
--font-body: "Inter", sans-serif;
--font-mono: "Ubuntu Mono", monospace;
--font-display: "NB Television Pro", monospace;
/* Ethereal color palette - grays, blacks, minimal color */
/* Ethereal color palette - light mode (inverted for light backgrounds) */
--color-ghost-50: #0a0a0a;
--color-ghost-100: #1a1a1a;
--color-ghost-200: #2a2a2a;
--color-ghost-300: #3a3a3a;
--color-ghost-400: #4a4a4a;
--color-ghost-500: #6a6a6a;
--color-ghost-600: #8a8a8a;
--color-ghost-700: #b0b0b0;
--color-ghost-800: #d0d0d0;
--color-ghost-900: #f0f0f0;
/* Subtle accent - barely visible blue-gray (light mode) */
--color-whisper-50: #0f1419;
--color-whisper-100: #1a1f2e;
--color-whisper-200: #252d40;
--color-whisper-300: #2f3b52;
--color-whisper-400: #3a4964;
--color-whisper-500: #4f5d7a;
--color-whisper-600: #687291;
--color-whisper-700: #8491a8;
--color-whisper-800: #a8b3c7;
--color-whisper-900: #d4dae6;
/* Sparkle accent (light mode) */
--color-sparkle-50: #202020;
--color-sparkle-100: #404040;
--color-sparkle-200: #606060;
--color-sparkle-300: #808080;
--color-sparkle-400: #a0a0a0;
--color-sparkle-500: #c0c0c0;
--color-sparkle-600: #d0d0d0;
--color-sparkle-700: #e8e8e8;
--color-sparkle-800: #f0f0f0;
--color-sparkle-900: #fafafa;
}
.dark {
/* Ethereal color palette - dark mode (original values) */
--color-ghost-50: #f0f0f0;
--color-ghost-100: #d0d0d0;
--color-ghost-200: #b0b0b0;
@ -21,7 +59,7 @@
--color-ghost-800: #1a1a1a;
--color-ghost-900: #0a0a0a;
/* Subtle accent - barely visible blue-gray */
/* Subtle accent - barely visible blue-gray (dark mode) */
--color-whisper-50: #d4dae6;
--color-whisper-100: #a8b3c7;
--color-whisper-200: #8491a8;
@ -33,7 +71,7 @@
--color-whisper-800: #1a1f2e;
--color-whisper-900: #0f1419;
/* Sparkle accent */
/* Sparkle accent (dark mode) */
--color-sparkle-50: #fafafa;
--color-sparkle-100: #f0f0f0;
--color-sparkle-200: #e8e8e8;
@ -46,33 +84,79 @@
--color-sparkle-900: #202020;
}
/* Global ethereal background */
/* Global ethereal background - light mode */
:root {
--ethereal-bg: radial-gradient(circle at 20% 80%, rgba(232, 232, 232, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(232, 232, 232, 0.02) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(232, 232, 232, 0.01) 0%, transparent 50%);
--ethereal-bg:
radial-gradient(
circle at 20% 80%,
rgba(40, 40, 40, 0.03) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
rgba(40, 40, 40, 0.02) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 40%,
rgba(40, 40, 40, 0.01) 0%,
transparent 50%
);
--halftone-pattern: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px);
--halftone-pattern: radial-gradient(
circle,
rgba(0, 0, 0, 0.1) 1px,
transparent 1px
);
--halftone-size: 8px 8px;
}
/* Dark mode background */
.dark:root {
--ethereal-bg:
radial-gradient(
circle at 20% 80%,
rgba(232, 232, 232, 0.03) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 20%,
rgba(232, 232, 232, 0.02) 0%,
transparent 50%
),
radial-gradient(
circle at 40% 40%,
rgba(232, 232, 232, 0.01) 0%,
transparent 50%
);
--halftone-pattern: radial-gradient(
circle,
rgba(255, 255, 255, 0.1) 1px,
transparent 1px
);
}
html {
background: var(--color-ghost-900);
color: var(--color-ghost-200);
@apply text-[--ui-text];
}
body {
background: var(--ethereal-bg), var(--color-ghost-900);
background: var(--ethereal-bg), #f0f0f0;
background-attachment: fixed;
}
.dark body {
background: var(--ethereal-bg), #0a0a0a;
}
/* Halftone texture overlay */
.halftone-texture {
position: relative;
}
.halftone-texture::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -86,14 +170,28 @@ body {
/* Sparkle effects */
@keyframes sparkle {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
0%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
@keyframes twinkle {
0%, 100% { opacity: 0.2; }
25% { opacity: 0.8; }
75% { opacity: 0.4; }
0%,
100% {
opacity: 0.2;
}
25% {
opacity: 0.8;
}
75% {
opacity: 0.4;
}
}
.sparkle-field {
@ -102,20 +200,50 @@ body {
}
.sparkle-field::after {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 10% 20%, var(--color-sparkle-200) 1px, transparent 1px),
radial-gradient(circle at 90% 80%, var(--color-sparkle-400) 1px, transparent 1px),
radial-gradient(circle at 30% 70%, var(--color-sparkle-200) 0.5px, transparent 0.5px),
radial-gradient(circle at 70% 30%, var(--color-sparkle-400) 0.5px, transparent 0.5px),
radial-gradient(circle at 50% 10%, var(--color-sparkle-200) 1px, transparent 1px),
radial-gradient(circle at 20% 90%, var(--color-sparkle-400) 0.5px, transparent 0.5px);
background-size: 200px 200px, 300px 300px, 150px 150px, 250px 250px, 180px 180px, 220px 220px;
radial-gradient(
circle at 10% 20%,
var(--color-sparkle-200) 1px,
transparent 1px
),
radial-gradient(
circle at 90% 80%,
var(--color-sparkle-400) 1px,
transparent 1px
),
radial-gradient(
circle at 30% 70%,
var(--color-sparkle-200) 0.5px,
transparent 0.5px
),
radial-gradient(
circle at 70% 30%,
var(--color-sparkle-400) 0.5px,
transparent 0.5px
),
radial-gradient(
circle at 50% 10%,
var(--color-sparkle-200) 1px,
transparent 1px
),
radial-gradient(
circle at 20% 90%,
var(--color-sparkle-400) 0.5px,
transparent 0.5px
);
background-size:
200px 200px,
300px 300px,
150px 150px,
250px 250px,
180px 180px,
220px 220px;
animation: twinkle 4s infinite ease-in-out;
pointer-events: none;
opacity: 0.6;
@ -141,6 +269,22 @@ body {
linear-gradient(45deg, transparent 75%, var(--color-ghost-700) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-ghost-700) 75%);
background-size: 4px 4px;
background-position: 0 0, 0 2px, 2px -2px, -2px 0px;
background-position:
0 0,
0 2px,
2px -2px,
-2px 0px;
}
/* Mobile responsive utilities */
@media (max-width: 1023px) {
/* Prevent horizontal scroll on mobile */
body {
overflow-x: hidden;
}
/* Adjust halftone pattern for mobile */
.halftone-texture::before {
background-size: 6px 6px;
}
}

View file

@ -7,7 +7,7 @@
>
<!-- Left: Copyright and minimal info -->
<div>
<p class="text-stone-500 text-xs mb-2">
<p class="text-ghost-500 text-xs mb-2">
© {{ currentYear }} Ghost Guild
</p>
</div>
@ -16,7 +16,7 @@
<div class="flex flex-wrap gap-6 text-xs">
<a
href="mailto:hello@ghostguild.org"
class="text-stone-500 hover:text-stone-300 transition-colors"
class="text-ghost-500 hover:text-ghost-300 transition-colors"
>
Contact
</a>

View file

@ -1,27 +1,38 @@
<template>
<nav
class="w-64 lg:w-80 backdrop-blur-sm h-screen sticky top-0 flex flex-col"
:class="[
isMobile
? 'w-full flex flex-col bg-transparent'
: 'w-64 lg:w-80 backdrop-blur-sm h-screen sticky top-0 flex flex-col bg-ghost-900 border-r border-ghost-700',
]"
>
<!-- Logo/Brand at top -->
<div class="p-8 border-b border-ghost-800 bg-blue-400">
<!-- Logo/Brand at top (desktop only) -->
<div v-if="!isMobile" class="p-8 border-b border-ghost-700 bg-primary-500">
<NuxtLink to="/" class="flex flex-col items-center gap-3 group">
<span
class="text-xl font-bold text-stone-100 ethereal-text tracking-wider"
<span class="text-xl font-bold text-white ethereal-text tracking-wider"
>Ghost Guild Logo</span
>
</NuxtLink>
</div>
<!-- Vertical Navigation -->
<div class="flex-1 p-8 overflow-y-auto">
<ul class="space-y-6">
<div
:class="
isMobile ? 'flex-1 p-6 overflow-y-auto' : 'flex-1 p-8 overflow-y-auto'
"
>
<ul :class="isMobile ? 'space-y-4' : 'space-y-6'">
<li v-for="item in navigationItems" :key="item.path">
<NuxtLink :to="item.path" class="block group relative">
<NuxtLink
:to="item.path"
class="block group relative"
@click="handleNavigate"
>
<!-- Hover indicator -->
<span
class="text-stone-200 hover:text-stone-100 transition-all duration-300 text-lg tracking-wide block py-2 hover:ethereal-text"
active-class="text-stone-100 ethereal-text translate-x-2"
class="text-ghost-200 hover:text-ghost-100 transition-all duration-300 text-lg tracking-wide block py-2 hover:ethereal-text"
active-class="text-ghost-100 ethereal-text translate-x-2"
>
{{ item.label }}
</span>
@ -29,12 +40,24 @@
</li>
</ul>
<!-- Color Mode Switcher -->
<div class="mb-6">
<UColorModeButton size="md" class="w-full" />
</div>
<!-- Auth section -->
<div class="mt-12 pt-8 border-t border-ghost-800/50">
<div
:class="
isMobile
? 'mt-8 pt-6 border-t border-ghost-800/50'
: 'mt-12 pt-8 border-t border-ghost-800/50'
"
>
<div v-if="isAuthenticated" class="space-y-4">
<NuxtLink
to="/member/dashboard"
class="block text-stone-300 hover:text-stone-100 hover:ethereal-text transition-all duration-300 py-2"
class="block text-ghost-300 hover:text-ghost-100 hover:ethereal-text transition-all duration-300 py-2"
@click="handleNavigate"
>
<span class="block text-sm text-whisper-400 mb-1">{{
memberData?.name || "Member"
@ -42,14 +65,14 @@
Dashboard
</NuxtLink>
<button
@click="logout"
class="text-stone-500 hover:text-stone-300 transition-all duration-300 text-sm"
@click="handleLogout"
class="text-ghost-500 hover:text-ghost-300 transition-all duration-300 text-sm"
>
Logout
</button>
</div>
<div v-else class="space-y-4">
<p class="text-stone-400 text-sm mb-4">
<p class="text-ghost-400 text-sm mb-4">
Enter your email to receive a login link
</p>
@ -97,8 +120,30 @@
<script setup>
import { reactive, ref, computed } from "vue";
const props = defineProps({
isMobile: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["navigate"]);
const { isAuthenticated, logout, memberData } = useAuth();
const handleNavigate = () => {
if (props.isMobile) {
emit("navigate");
}
};
const handleLogout = async () => {
await logout();
if (props.isMobile) {
emit("navigate");
}
};
const publicNavigationItems = [
{ label: "Home", path: "/", accent: "entry point" },
{ label: "About", path: "/about", accent: "who we are" },

View file

@ -5,7 +5,7 @@
<img
:src="transformedImageUrl"
:alt="modelValue.alt || 'Event image'"
class="w-full h-48 object-cover rounded-lg border border-gray-300"
class="w-full h-48 object-cover rounded-lg border border-neutral-200"
@error="console.log('Image failed to load:', transformedImageUrl)"
@load="console.log('Image loaded successfully:', transformedImageUrl)"
/>
@ -62,7 +62,7 @@
:value="modelValue.alt || ''"
@input="updateAltText($event.target.value)"
placeholder="Describe this image..."
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full border border-neutral-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
@ -91,111 +91,113 @@
const props = defineProps({
modelValue: {
type: Object,
default: () => null
}
})
default: () => null,
},
});
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits(["update:modelValue"]);
const isDragging = ref(false)
const isUploading = ref(false)
const uploadProgress = ref(0)
const errorMessage = ref('')
const fileInput = ref()
const isDragging = ref(false);
const isUploading = ref(false);
const uploadProgress = ref(0);
const errorMessage = ref("");
const fileInput = ref();
// Transform image URL for preview (smaller size)
const transformedImageUrl = computed(() => {
console.log('modelValue in computed:', props.modelValue)
console.log("modelValue in computed:", props.modelValue);
// If we have the direct URL, use it
if (props.modelValue?.url) {
console.log('Using direct URL:', props.modelValue.url)
return props.modelValue.url
console.log("Using direct URL:", props.modelValue.url);
return props.modelValue.url;
}
// Otherwise try to construct from publicId
if (props.modelValue?.publicId) {
const config = useRuntimeConfig()
const constructedUrl = `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/w_400,h_200,c_fill,f_auto,q_auto/${props.modelValue.publicId}`
console.log('Constructed URL:', constructedUrl)
return constructedUrl
const config = useRuntimeConfig();
const constructedUrl = `https://res.cloudinary.com/${config.public.cloudinaryCloudName}/image/upload/w_400,h_200,c_fill,f_auto,q_auto/${props.modelValue.publicId}`;
console.log("Constructed URL:", constructedUrl);
return constructedUrl;
}
console.log('No URL or publicId found')
return ''
})
console.log("No URL or publicId found");
return "";
});
const handleFileSelect = (event) => {
const file = event.target.files[0]
const file = event.target.files[0];
if (file) {
uploadFile(file)
uploadFile(file);
}
}
};
const handleDrop = (event) => {
isDragging.value = false
const files = event.dataTransfer.files
isDragging.value = false;
const files = event.dataTransfer.files;
if (files.length > 0) {
uploadFile(files[0])
uploadFile(files[0]);
}
}
};
const uploadFile = async (file) => {
// Validate file
if (!file.type.startsWith('image/')) {
errorMessage.value = 'Please select an image file'
return
if (!file.type.startsWith("image/")) {
errorMessage.value = "Please select an image file";
return;
}
if (file.size > 10 * 1024 * 1024) { // 10MB
errorMessage.value = 'File size must be less than 10MB'
return
if (file.size > 10 * 1024 * 1024) {
// 10MB
errorMessage.value = "File size must be less than 10MB";
return;
}
errorMessage.value = ''
isUploading.value = true
uploadProgress.value = 0
errorMessage.value = "";
isUploading.value = true;
uploadProgress.value = 0;
try {
// Create form data for upload
const formData = new FormData()
formData.append('file', file)
const formData = new FormData();
formData.append("file", file);
// Upload to Cloudinary
const response = await $fetch(`/api/upload/image`, {
method: 'POST',
method: "POST",
body: formData,
onUploadProgress: (progress) => {
uploadProgress.value = Math.round((progress.loaded / progress.total) * 100)
}
})
uploadProgress.value = Math.round(
(progress.loaded / progress.total) * 100,
);
},
});
console.log('Upload response:', response)
console.log("Upload response:", response);
// Update the model value
emit('update:modelValue', {
emit("update:modelValue", {
url: response.secure_url,
publicId: response.public_id,
alt: ''
})
alt: "",
});
} catch (error) {
console.error('Upload failed:', error)
errorMessage.value = 'Upload failed. Please try again.'
console.error("Upload failed:", error);
errorMessage.value = "Upload failed. Please try again.";
} finally {
isUploading.value = false
uploadProgress.value = 0
isUploading.value = false;
uploadProgress.value = 0;
}
}
};
const removeImage = () => {
emit('update:modelValue', null)
}
emit("update:modelValue", null);
};
const updateAltText = (altText) => {
emit('update:modelValue', {
emit("update:modelValue", {
...props.modelValue,
alt: altText
})
}
alt: altText,
});
};
</script>

View file

@ -28,21 +28,21 @@
'rounded-2xl p-6 md:p-8 mb-12 backdrop-blur-sm',
props.theme === 'ethereal'
? 'bg-ghost-800/60 border border-ghost-700 ethereal-glow halftone-texture'
: 'bg-white dark:bg-gray-800 shadow-xl border border-blue-200 dark:border-blue-800',
: 'bg-[--ui-bg-elevated] shadow-xl border border-blue-200',
]"
>
<div class="flex items-center justify-between">
<div class="flex items-center justify-between gap-3 md:gap-4">
<button
:class="[
'p-3 rounded-full transition-all duration-300',
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
props.theme === 'ethereal'
? 'bg-whisper-600/80 text-stone-100 hover:bg-whisper-500 ethereal-glow'
? 'bg-whisper-600/80 text-ghost-100 hover:bg-whisper-500 ethereal-glow'
: 'bg-blue-500 text-white hover:bg-blue-600',
]"
@click="$emit('prev')"
>
<svg
class="w-6 h-6"
class="w-5 h-5 md:w-6 md:h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -56,14 +56,14 @@
</svg>
</button>
<div class="text-center flex-1">
<div class="text-center flex-1 min-w-0">
<slot name="interactive-content">
<p
:class="[
'text-lg',
'text-base md:text-lg',
props.theme === 'ethereal'
? 'text-stone-200'
: 'text-gray-600 dark:text-gray-300',
? 'text-ghost-200'
: 'text-[--ui-text-muted]',
]"
>
{{
@ -76,15 +76,15 @@
<button
:class="[
'p-3 rounded-full transition-all duration-300',
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
props.theme === 'ethereal'
? 'bg-whisper-600/80 text-stone-100 hover:bg-whisper-500 ethereal-glow'
? 'bg-whisper-600/80 text-ghost-100 hover:bg-whisper-500 ethereal-glow'
: 'bg-blue-500 text-white hover:bg-blue-600',
]"
@click="$emit('next')"
>
<svg
class="w-6 h-6"
class="w-5 h-5 md:w-6 md:h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
@ -178,12 +178,10 @@ defineEmits(["prev", "next"]);
const backgroundClass = computed(() => {
const themes = {
blue: "bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800",
purple:
"bg-gradient-to-br from-purple-50 to-violet-100 dark:from-gray-900 dark:to-purple-900/20",
emerald:
"bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-emerald-900/20",
gray: "bg-gray-50 dark:bg-gray-900",
blue: "bg-gradient-to-br from-blue-50 to-indigo-100",
purple: "bg-gradient-to-br from-purple-50 to-violet-100",
emerald: "bg-gradient-to-br from-emerald-50 to-teal-100",
gray: "bg-neutral-100",
ethereal:
"bg-gradient-to-br from-ghost-900 via-ghost-800 to-whisper-900 halftone-texture",
};

View file

@ -1,6 +1,6 @@
<template>
<div class="flex items-center gap-3 text-sm">
<span class="text-stone-300 font-medium">{{ label }}:</span>
<span class="text-ghost-300 font-medium">{{ label }}:</span>
<UButtonGroup size="sm" class="privacy-toggle-group">
<UButton
:variant="modelValue === 'members' ? 'solid' : 'outline'"
@ -46,14 +46,14 @@ const updateValue = (value) => {
<style scoped>
/* Unselected buttons - lighter background for visibility */
:deep(.privacy-toggle-btn:not(.is-selected)) {
background-color: rgb(68 64 60) !important; /* stone-700 */
border-color: rgb(87 83 78) !important; /* stone-600 */
color: rgb(214 211 209) !important; /* stone-300 */
background-color: rgb(68 64 60) !important; /* ghost-700 equivalent */
border-color: rgb(87 83 78) !important; /* ghost-600 equivalent */
color: rgb(214 211 209) !important; /* ghost-300 equivalent */
}
:deep(.privacy-toggle-btn:not(.is-selected):hover) {
background-color: rgb(87 83 78) !important; /* stone-600 */
border-color: rgb(120 113 108) !important; /* stone-500 */
background-color: rgb(87 83 78) !important; /* ghost-600 equivalent */
border-color: rgb(120 113 108) !important; /* ghost-500 equivalent */
}
/* Selected buttons - bright blue to stand out */

View file

@ -12,7 +12,7 @@
/>
<div
v-else
class="w-12 h-12 rounded-full bg-stone-700 flex items-center justify-center text-stone-300 font-bold"
class="w-12 h-12 rounded-full bg-ghost-700 flex items-center justify-center text-ghost-300 font-bold"
>
{{ update.author?.name?.charAt(0)?.toUpperCase() || "?" }}
</div>
@ -23,30 +23,30 @@
<!-- Header -->
<div class="flex items-start justify-between gap-4 mb-2">
<div>
<h3 class="font-semibold text-stone-100">
<h3 class="font-semibold text-ghost-100">
<NuxtLink
v-if="update.author?._id"
:to="`/updates/user/${update.author._id}`"
class="hover:text-stone-300 transition-colors"
class="hover:text-ghost-300 transition-colors"
>
{{ update.author.name }}
</NuxtLink>
<span v-else>Unknown Member</span>
</h3>
<div class="flex items-center gap-2 text-sm text-stone-400">
<div class="flex items-center gap-2 text-sm text-ghost-400">
<time :datetime="update.createdAt">
{{ formatDate(update.createdAt) }}
</time>
<span v-if="isEdited" class="text-stone-500">(edited)</span>
<span v-if="isEdited" class="text-ghost-500">(edited)</span>
<span
v-if="update.privacy === 'private'"
class="px-2 py-0.5 bg-stone-700 text-stone-300 rounded text-xs"
class="px-2 py-0.5 bg-ghost-700 text-ghost-300 rounded text-xs"
>
Private
</span>
<span
v-if="update.privacy === 'public'"
class="px-2 py-0.5 bg-stone-700 text-stone-300 rounded text-xs"
class="px-2 py-0.5 bg-ghost-700 text-ghost-300 rounded text-xs"
>
Public
</span>
@ -73,12 +73,12 @@
</div>
<!-- Content -->
<div class="text-stone-200 whitespace-pre-wrap break-words mb-3">
<div class="text-ghost-200 whitespace-pre-wrap break-words mb-3">
<template v-if="showPreview && update.content.length > 300">
{{ update.content.substring(0, 300) }}...
<NuxtLink
:to="`/updates/${update._id}`"
class="text-stone-400 hover:text-stone-300 ml-1"
class="text-ghost-400 hover:text-ghost-300 ml-1"
>
Read more
</NuxtLink>
@ -100,14 +100,14 @@
</div>
<!-- Footer actions -->
<div class="flex items-center gap-4 text-sm text-stone-400">
<div class="flex items-center gap-4 text-sm text-ghost-400">
<NuxtLink
:to="`/updates/${update._id}`"
class="hover:text-stone-300 transition-colors"
class="hover:text-ghost-300 transition-colors"
>
View full update
</NuxtLink>
<span v-if="update.commentsEnabled" class="text-stone-500">
<span v-if="update.commentsEnabled" class="text-ghost-500">
Comments (coming soon)
</span>
</div>

View file

@ -11,19 +11,19 @@
</UFormField>
<!-- Privacy Settings -->
<div class="border border-stone-700 rounded-lg p-4 bg-stone-800/30">
<h3 class="text-sm font-medium text-stone-200 mb-4">Privacy Settings</h3>
<div class="border border-ghost-700 rounded-lg p-4 bg-ghost-800/30">
<h3 class="text-sm font-medium text-ghost-200 mb-4">Privacy Settings</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer">
<input
v-model="formData.privacy"
type="radio"
value="public"
class="w-4 h-4 text-stone-400"
class="w-4 h-4 text-ghost-400"
/>
<div>
<div class="text-stone-200 font-medium">Public</div>
<div class="text-sm text-stone-400">
<div class="text-ghost-200 font-medium">Public</div>
<div class="text-sm text-ghost-400">
Visible to everyone, including non-members
</div>
</div>
@ -34,11 +34,11 @@
v-model="formData.privacy"
type="radio"
value="members"
class="w-4 h-4 text-stone-400"
class="w-4 h-4 text-ghost-400"
/>
<div>
<div class="text-stone-200 font-medium">Members Only</div>
<div class="text-sm text-stone-400">
<div class="text-ghost-200 font-medium">Members Only</div>
<div class="text-sm text-ghost-400">
Only visible to Ghost Guild members
</div>
</div>
@ -49,11 +49,11 @@
v-model="formData.privacy"
type="radio"
value="private"
class="w-4 h-4 text-stone-400"
class="w-4 h-4 text-ghost-400"
/>
<div>
<div class="text-stone-200 font-medium">Private</div>
<div class="text-sm text-stone-400">Only visible to you</div>
<div class="text-ghost-200 font-medium">Private</div>
<div class="text-sm text-ghost-400">Only visible to you</div>
</div>
</label>
</div>
@ -66,15 +66,17 @@
<div class="flex items-center gap-3">
<USwitch v-model="formData.commentsEnabled" />
<div>
<div class="text-stone-200 font-medium">Enable Comments</div>
<div class="text-sm text-stone-400">
<div class="text-ghost-200 font-medium">Enable Comments</div>
<div class="text-sm text-ghost-400">
Allow members to comment on this update
</div>
</div>
</div>
<!-- Actions -->
<div class="flex justify-between items-center pt-4 border-t border-stone-700">
<div
class="flex justify-between items-center pt-4 border-t border-ghost-700"
>
<UButton variant="ghost" color="neutral" @click="$emit('cancel')">
Cancel
</UButton>

View file

@ -1,5 +1,5 @@
<template>
<div class="min-h-screen bg-stone-800 flex relative">
<div class="min-h-screen bg-ghost-900 flex relative">
<!-- Background image at top - full page width -->
<div
class="absolute inset-x-0 pointer-events-none z-0"
@ -21,15 +21,48 @@
"
/>
<!-- Mobile Header -->
<div
class="lg:hidden fixed top-0 left-0 right-0 z-50 bg-ghost-900/95 backdrop-blur-md border-b border-ghost-700"
>
<div class="flex items-center justify-between p-4">
<NuxtLink
to="/"
class="text-lg font-bold text-white ethereal-text tracking-wider"
>
Ghost Guild
</NuxtLink>
<UButton
icon="i-lucide-menu"
color="neutral"
variant="ghost"
size="lg"
@click="isMobileMenuOpen = true"
aria-label="Open menu"
/>
</div>
</div>
<!-- Main Content Column - Left -->
<div class="flex-1 overflow-y-auto relative z-[5]">
<div class="p-8 md:p-12 lg:p-16 max-w-4xl relative">
<div class="p-4 pt-20 md:p-8 md:pt-8 lg:p-16 max-w-4xl relative">
<slot />
</div>
<AppFooter />
</div>
<!-- Navigation Column - Right -->
<AppNavigation class="relative z-20" />
<!-- Desktop Navigation Column - Right -->
<AppNavigation class="hidden lg:block relative z-20" />
<!-- Mobile Navigation Drawer -->
<USlideover v-model:open="isMobileMenuOpen" side="right">
<template #body>
<AppNavigation :is-mobile="true" @navigate="isMobileMenuOpen = false" />
</template>
</USlideover>
</div>
</template>
<script setup>
const isMobileMenuOpen = ref(false);
</script>

View file

@ -9,22 +9,22 @@
/>
<!-- How Ghost Guild Works -->
<section class="py-20 bg-white dark:bg-gray-900">
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-6">
<h2 class="text-3xl font-bold text-[--ui-text] mb-6">
How Ghost Guild Works
</h2>
<div class="prose prose-lg dark:prose-invert max-w-none">
<p class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
<p class="text-xl font-semibold text-[--ui-text] mb-6">
Everyone gets everything. Your circle reflects where you are in
your cooperative journey. Your financial contribution reflects
what you can afford. These are completely separate choices.
</p>
<ul
class="text-lg leading-relaxed text-gray-700 dark:text-gray-300 space-y-3 mb-12"
class="text-lg leading-relaxed text-[--ui-text-muted] space-y-3 mb-12"
>
<li>
<strong>Equal access:</strong> The entire knowledge commons, all
@ -49,13 +49,13 @@
</section>
<!-- Find Your Circle -->
<section class="py-20 bg-gray-50 dark:bg-gray-800">
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
Find Your Circle
</h2>
<p class="text-lg text-gray-700 dark:text-gray-300 mb-12">
<p class="text-lg text-[--ui-text-muted] mb-12">
Circles help us provide relevant guidance and connect you with
others at similar stages. Choose based on where you are, not what
you want to access.
@ -63,21 +63,19 @@
<div class="space-y-12">
<!-- Community Circle -->
<div class="bg-white dark:bg-gray-900 rounded-xl p-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
<div class="bg-[--ui-bg] rounded-xl p-8">
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
Community Circle
</h3>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-6">
<p class="text-lg text-[--ui-text-muted] mb-6">
You're exploring what cooperatives could mean for your work
</p>
<div class="mb-6">
<h4
class="text-lg font-semibold text-gray-900 dark:text-white mb-3"
>
<h4 class="text-lg font-semibold text-[--ui-text] mb-3">
Where you might be:
</h4>
<ul class="text-gray-700 dark:text-gray-300 space-y-2">
<ul class="text-[--ui-text-muted] space-y-2">
<li>
Curious about alternatives to traditional studio structures
</li>
@ -88,12 +86,10 @@
</div>
<div class="mb-6">
<h4
class="text-lg font-semibold text-gray-900 dark:text-white mb-3"
>
<h4 class="text-lg font-semibold text-[--ui-text] mb-3">
We'll help you navigate:
</h4>
<ul class="text-gray-700 dark:text-gray-300 space-y-2">
<ul class="text-[--ui-text-muted] space-y-2">
<li>Understanding cooperative basics</li>
<li>Connecting with others asking similar questions</li>
<li>Exploring real examples from game studios</li>
@ -102,12 +98,10 @@
</div>
<div>
<h4
class="text-lg font-semibold text-gray-900 dark:text-white mb-3"
>
<h4 class="text-lg font-semibold text-[--ui-text] mb-3">
You might be:
</h4>
<ul class="text-gray-700 dark:text-gray-300 space-y-2">
<ul class="text-[--ui-text-muted] space-y-2">
<li>Individual game workers</li>
<li>Researchers and students</li>
<li>Industry allies and supporters</li>
@ -117,11 +111,11 @@
</div>
<!-- Founder Circle -->
<div class="bg-white dark:bg-gray-900 rounded-xl p-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
<div class="bg-[--ui-bg] rounded-xl p-8">
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
Founder Circle
</h3>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-6">
<p class="text-lg text-[--ui-text-muted] mb-6">
You're actively building or transitioning to a cooperative model
</p>
@ -188,11 +182,11 @@
</div>
<!-- Practitioner Circle -->
<div class="bg-white dark:bg-gray-900 rounded-xl p-8">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
<div class="bg-[--ui-bg] rounded-xl p-8">
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
Practitioner Circle
</h3>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-6">
<p class="text-lg text-[--ui-text-muted] mb-6">
You're operating a cooperative and contributing to the field
</p>
@ -246,14 +240,14 @@
</section>
<!-- Important Notes -->
<section class="py-20 bg-white dark:bg-gray-900">
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-8">
<h2 class="text-3xl font-bold text-[--ui-text] mb-8">
Important Notes
</h2>
<div class="space-y-6 text-lg text-gray-700 dark:text-gray-300">
<div class="space-y-6 text-lg text-[--ui-text-muted]">
<p>
<strong>Movement between circles is fluid.</strong> As you move
along in your journey, you can shift circles anytime. Just let us

View file

@ -4,13 +4,13 @@
<section class="mb-24">
<div class="relative">
<h1
class="text-6xl md:text-8xl font-bold text-stone-100 ethereal-text leading-tight mb-8"
class="text-6xl md:text-8xl font-bold text-ghost-100 ethereal-text leading-tight mb-8"
>
Get in Touch
</h1>
<div class="mb-16">
<p class="text-stone-100 text-lg max-w-md">
<p class="text-ghost-100 text-lg max-w-md">
We'd be happy to answer any questions<br />
you might have about Ghost Guild
</p>
@ -21,7 +21,7 @@
<!-- Contact Form -->
<section class="mb-32 relative">
<div class="mb-12">
<h2 class="text-3xl font-light text-stone-200 mb-4">
<h2 class="text-3xl font-light text-ghost-200 mb-4">
Send us a message (or email hello@ghostguild.org)
</h2>
</div>
@ -79,7 +79,7 @@
:loading="isSubmitting"
:disabled="!isFormValid"
size="xl"
class="px-12 border border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500 hover:ethereal-text transition-all duration-500"
class="px-12 border border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 hover:ethereal-text transition-all duration-500"
>
Send Message
</UButton>
@ -98,7 +98,7 @@
</div>
<div v-if="error" class="mt-6 p-4 border border-ghost-700 bg-ghost-900">
<p class="text-stone-300 text-center">
<p class="text-ghost-300 text-center">
{{ error }}
</p>
</div>

View file

@ -1,27 +1,27 @@
<template>
<div
v-if="pending"
class="min-h-screen bg-stone-900 flex items-center justify-center"
class="min-h-screen bg-ghost-900 flex items-center justify-center"
>
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"
></div>
<p class="text-stone-200">Loading event details...</p>
<p class="text-ghost-200">Loading event details...</p>
</div>
</div>
<div
v-else-if="error"
class="min-h-screen bg-stone-900 flex items-center justify-center"
class="min-h-screen bg-ghost-900 flex items-center justify-center"
>
<div class="text-center">
<Icon
name="heroicons:exclamation-triangle"
class="w-16 h-16 text-red-500 mx-auto mb-4"
/>
<h2 class="text-2xl font-bold text-stone-100 mb-2">Event Not Found</h2>
<p class="text-stone-300 mb-6">
<h2 class="text-2xl font-bold text-ghost-100 mb-2">Event Not Found</h2>
<p class="text-ghost-300 mb-6">
The event you're looking for doesn't exist.
</p>
<NuxtLink to="/events" class="text-blue-400 hover:underline">
@ -64,11 +64,11 @@
<PageHeader v-else :title="event.title" theme="blue" size="medium" />
<!-- Event Details Section -->
<section class="py-16 bg-stone-900">
<section class="py-16 bg-ghost-900">
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Event Meta Info -->
<div class="bg-stone-800 rounded-xl p-6 mb-8 border border-stone-700">
<div class="bg-ghost-800 rounded-xl p-6 mb-8 border border-ghost-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="flex items-center space-x-3">
<Icon
@ -76,8 +76,8 @@
class="w-6 h-6 text-blue-400"
/>
<div>
<p class="text-sm text-stone-400">Date</p>
<p class="font-semibold text-stone-100">
<p class="text-sm text-ghost-400">Date</p>
<p class="font-semibold text-ghost-100">
{{ formatDate(event.startDate) }}
</p>
</div>
@ -86,8 +86,8 @@
<div class="flex items-center space-x-3">
<Icon name="heroicons:clock" class="w-6 h-6 text-blue-400" />
<div>
<p class="text-sm text-stone-400">Time</p>
<p class="font-semibold text-stone-100">
<p class="text-sm text-ghost-400">Time</p>
<p class="font-semibold text-ghost-100">
{{ formatTime(event.startDate, event.endDate) }}
</p>
</div>
@ -96,8 +96,8 @@
<div class="flex items-center space-x-3">
<Icon name="heroicons:map-pin" class="w-6 h-6 text-blue-400" />
<div>
<p class="text-sm text-stone-400">Location</p>
<p class="font-semibold text-stone-100">
<p class="text-sm text-ghost-400">Location</p>
<p class="font-semibold text-ghost-100">
{{ event.location }}
</p>
</div>
@ -153,7 +153,7 @@
>
<div class="flex items-center space-x-2">
<Icon name="heroicons:user-group" class="w-5 h-5 text-blue-400" />
<span class="text-sm font-medium text-stone-200"
<span class="text-sm font-medium text-ghost-200"
>Recommended for:</span
>
<div class="flex flex-wrap gap-2">
@ -170,15 +170,15 @@
<!-- Event Description -->
<div class="prose prose-lg dark:prose-invert max-w-none mb-12">
<h2 class="text-2xl font-bold text-stone-100 mb-4">
<h2 class="text-2xl font-bold text-ghost-100 mb-4">
About This Event
</h2>
<p class="text-stone-200">
<p class="text-ghost-200">
{{ event.description }}
</p>
<div v-if="event.agenda && event.agenda.length > 0" class="mt-8">
<h3 class="text-xl font-semibold text-stone-100 mb-4">
<h3 class="text-xl font-semibold text-ghost-100 mb-4">
Event Agenda
</h3>
<ul class="space-y-3">
@ -192,7 +192,7 @@
>
{{ index + 1 }}
</span>
<span class="text-stone-200">{{ item }}</span>
<span class="text-ghost-200">{{ item }}</span>
</li>
</ul>
</div>
@ -201,7 +201,7 @@
v-if="event.speakers && event.speakers.length > 0"
class="mt-8"
>
<h3 class="text-xl font-semibold text-stone-100 mb-4">
<h3 class="text-xl font-semibold text-ghost-100 mb-4">
Speakers
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -211,21 +211,21 @@
class="flex items-start space-x-4"
>
<div
class="w-16 h-16 bg-stone-700 rounded-full flex items-center justify-center"
class="w-16 h-16 bg-ghost-700 rounded-full flex items-center justify-center"
>
<Icon
name="heroicons:user"
class="w-8 h-8 text-stone-500"
class="w-8 h-8 text-ghost-500"
/>
</div>
<div>
<p class="font-semibold text-stone-100">
<p class="font-semibold text-ghost-100">
{{ speaker.name }}
</p>
<p class="text-sm text-stone-300">
<p class="text-sm text-ghost-300">
{{ speaker.role }}
</p>
<p class="text-sm text-stone-400 mt-1">
<p class="text-sm text-ghost-400 mt-1">
{{ speaker.bio }}
</p>
</div>
@ -237,9 +237,9 @@
<!-- Registration Section -->
<div
v-if="!event.isCancelled"
class="bg-stone-800 rounded-xl p-8 border border-stone-700"
class="bg-ghost-800 rounded-xl p-8 border border-ghost-700"
>
<h3 class="text-xl font-bold text-stone-100 mb-6">
<h3 class="text-xl font-bold text-ghost-100 mb-6">
Register for This Event
</h3>
@ -322,7 +322,7 @@
<div>
<label
for="name"
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Full Name
</label>
@ -338,7 +338,7 @@
<div>
<label
for="email"
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Email Address
</label>
@ -354,7 +354,7 @@
<div>
<label
for="membershipLevel"
class="block text-sm font-medium text-stone-200 mb-2"
class="block text-sm font-medium text-ghost-200 mb-2"
>
Membership Status
</label>
@ -388,16 +388,16 @@
<!-- Event Capacity -->
<div
v-if="event.maxAttendees"
class="mt-6 pt-6 border-t border-stone-700"
class="mt-6 pt-6 border-t border-ghost-700"
>
<div class="flex items-center justify-between">
<span class="text-sm text-stone-300">Event Capacity</span>
<span class="text-sm text-ghost-300">Event Capacity</span>
<div class="flex items-center space-x-2">
<span class="text-sm font-semibold text-stone-100">
<span class="text-sm font-semibold text-ghost-100">
{{ event.registeredCount || 0 }} / {{ event.maxAttendees }}
</span>
<div
class="w-24 h-2 bg-stone-700 rounded-full overflow-hidden"
class="w-24 h-2 bg-ghost-700 rounded-full overflow-hidden"
>
<div
class="h-full bg-blue-500 rounded-full"
@ -410,9 +410,9 @@
</div>
<!-- Additional Information -->
<div class="mt-8 p-6 bg-stone-800 rounded-xl border border-stone-700">
<h4 class="font-semibold text-stone-100 mb-3">Questions?</h4>
<p class="text-sm text-stone-200 mb-3">
<div class="mt-8 p-6 bg-ghost-800 rounded-xl border border-ghost-700">
<h4 class="font-semibold text-ghost-100 mb-3">Questions?</h4>
<p class="text-sm text-ghost-200 mb-3">
If you have any questions about this event please drop us a line.
</p>
<a

View file

@ -8,7 +8,7 @@
/>
<!-- Events Section with Tabs -->
<section class="py-20 bg-stone-900 dark:bg-stone-950">
<section class="py-20 bg-ghost-900 dark:bg-ghost-950">
<UContainer>
<UTabs
v-model="activeTab"
@ -27,10 +27,10 @@
class="group flex items-start gap-4 py-2 hover:opacity-80 transition-opacity"
>
<div class="flex-shrink-0 text-center">
<div class="text-2xl font-bold text-stone-100">
<div class="text-2xl font-bold text-ghost-100">
{{ event.start.getDate() }}
</div>
<div class="text-xs text-stone-400 uppercase">
<div class="text-xs text-ghost-400 uppercase">
{{
event.start.toLocaleDateString("en-US", {
month: "short",
@ -42,7 +42,7 @@
<div class="flex-1 min-w-0">
<div class="flex items-start gap-2 mb-1">
<h3
class="text-lg font-semibold text-stone-100 group-hover:text-blue-400 transition-colors"
class="text-lg font-semibold text-ghost-100 group-hover:text-blue-400 transition-colors"
>
{{ event.title }}
</h3>
@ -53,7 +53,7 @@
/>
</div>
<p class="text-sm text-stone-300 mb-2 line-clamp-2">
<p class="text-sm text-ghost-300 mb-2 line-clamp-2">
{{ event.content }}
</p>
@ -72,7 +72,7 @@
<Icon
name="heroicons:arrow-right"
class="w-5 h-5 text-stone-400 group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 mt-1"
class="w-5 h-5 text-ghost-400 group-hover:text-blue-400 group-hover:translate-x-1 transition-all flex-shrink-0 mt-1"
/>
</NuxtLink>
</div>
@ -83,10 +83,10 @@
<ClientOnly>
<div
v-if="pending"
class="min-h-[400px] bg-stone-700 rounded-xl flex items-center justify-center"
class="min-h-[400px] bg-ghost-700 rounded-xl flex items-center justify-center"
>
<div class="text-center">
<p class="text-stone-200">Loading events...</p>
<p class="text-ghost-200">Loading events...</p>
</div>
</div>
<VueCal
@ -110,13 +110,13 @@
/>
<template #fallback>
<div
class="min-h-[400px] bg-stone-700 rounded-xl flex items-center justify-center"
class="min-h-[400px] bg-ghost-700 rounded-xl flex items-center justify-center"
>
<div class="text-center">
<div
class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"
></div>
<p class="text-stone-200">Loading calendar...</p>
<p class="text-ghost-200">Loading calendar...</p>
</div>
</div>
</template>
@ -130,14 +130,14 @@
<!-- Event Series -->
<section
v-if="activeSeries.length > 0"
class="py-20 bg-stone-800 dark:bg-stone-900"
class="py-20 bg-ghost-800 dark:bg-ghost-900"
>
<UContainer>
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-stone-100 mb-8">
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
Active Event Series
</h2>
<p class="text-stone-300 max-w-2xl mx-auto">
<p class="text-ghost-300 max-w-2xl mx-auto">
Multi-part workshops and recurring events designed to deepen your
knowledge and build community connections.
</p>
@ -149,7 +149,7 @@
<div
v-for="series in activeSeries.slice(0, 6)"
:key="series.id"
class="bg-stone-900 rounded-xl p-6 shadow-lg border border-stone-700"
class="bg-ghost-900 rounded-xl p-6 shadow-lg border border-ghost-700"
>
<div class="flex items-start justify-between mb-4">
<div
@ -160,17 +160,17 @@
>
{{ formatSeriesType(series.type) }}
</div>
<div class="flex items-center gap-1 text-xs text-stone-400">
<div class="flex items-center gap-1 text-xs text-ghost-400">
<Icon name="heroicons:calendar-days" class="w-4 h-4" />
<span>{{ series.eventCount }} events</span>
</div>
</div>
<h3 class="text-lg font-semibold text-stone-100 mb-2">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
{{ series.title }}
</h3>
<p class="text-sm text-stone-300 mb-4 line-clamp-2">
<p class="text-sm text-ghost-300 mb-4 line-clamp-2">
{{ series.description }}
</p>
@ -186,22 +186,22 @@
>
{{ event.series?.position || "?" }}
</div>
<span class="text-stone-300 truncate">{{ event.title }}</span>
<span class="text-ghost-300 truncate">{{ event.title }}</span>
</div>
<span class="text-stone-400">
<span class="text-ghost-400">
{{ formatEventDate(event.startDate) }}
</span>
</div>
<div
v-if="series.events.length > 3"
class="text-xs text-stone-400 text-center pt-1"
class="text-xs text-ghost-400 text-center pt-1"
>
+{{ series.events.length - 3 }} more events
</div>
</div>
<div class="flex items-center justify-between text-sm">
<div class="text-stone-400">
<div class="text-ghost-400">
{{ formatDateRange(series.startDate, series.endDate) }}
</div>
<span
@ -223,20 +223,20 @@
</section>
<!-- Attend Our Events -->
<section class="py-20 bg-stone-800 dark:bg-stone-900">
<section class="py-20 bg-ghost-800 dark:bg-ghost-900">
<UContainer>
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-stone-100 mb-8">
<h2 class="text-3xl font-bold text-ghost-100 mb-8">
Attend Our Events
</h2>
</div>
<div class="max-w-4xl mx-auto">
<div
class="bg-stone-900 rounded-2xl p-8 border border-stone-700 mb-12"
class="bg-ghost-900 rounded-2xl p-8 border border-ghost-700 mb-12"
>
<div class="prose prose-lg dark:prose-invert max-w-none">
<p class="text-lg leading-relaxed text-stone-200 mb-6">
<p class="text-lg leading-relaxed text-ghost-200 mb-6">
Our events are ,Lorem ipsum, dolor sit amet consectetur
adipisicing elit. Quibusdam exercitationem delectus ab
voluptates aspernatur, quia deleniti aut maxime, veniam
@ -244,7 +244,7 @@
dolorum alias nulla!
</p>
<p class="text-lg leading-relaxed text-stone-200 mb-6">
<p class="text-lg leading-relaxed text-ghost-200 mb-6">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
@ -265,31 +265,31 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="text-center">
<h3 class="text-lg font-semibold text-stone-100 mb-2">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
Monthly Meetups
</h3>
<p class="text-sm text-stone-300">
<p class="text-sm text-ghost-300">
Casual knowledge sharing sessions
</p>
</div>
<div class="text-center">
<h3 class="text-lg font-semibold text-stone-100 mb-2">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
Workshops
</h3>
<p class="text-sm text-stone-300">
<p class="text-sm text-ghost-300">
Hands-on learning about cooperative and worker-centric business
models
</p>
</div>
<div class="text-center">
<h3 class="text-lg font-semibold text-stone-100 mb-2">
<h3 class="text-lg font-semibold text-ghost-100 mb-2">
Social Events
</h3>
<p class="text-sm text-stone-300">
<p class="text-sm text-ghost-300">
Game nights, socials, and more
</p>
</div>

View file

@ -5,14 +5,14 @@
<div class="relative">
<!-- Large artistic title -->
<h1
class="text-6xl md:text-8xl font-bold text-stone-100 ethereal-text leading-tight mb-8"
class="text-6xl md:text-8xl font-bold text-ghost-100 ethereal-text leading-tight mb-8"
>
Become a Ghostie
</h1>
<!-- Floating subtitle -->
<div class="mb-16">
<p class="text-stone-100 text-lg max-w-md">
<p class="text-ghost-100 text-lg max-w-md">
A community for creatives and game devs<br />
exploring cooperative models
</p>
@ -33,7 +33,7 @@
<div>
<NuxtLink
to="/join"
class="inline-block px-8 py-3 border border-ghost-600 text-stone-200 hover:bg-ghost-800 hover:border-whisper-500 hover:ethereal-text transition-all duration-500"
class="inline-block px-8 py-3 border border-ghost-600 text-ghost-200 hover:bg-ghost-800 hover:border-whisper-500 hover:ethereal-text transition-all duration-500"
>
Join Us Today
</NuxtLink>
@ -55,13 +55,13 @@
>
<!-- Content -->
<div class="flex-1 max-w-lg">
<h3 class="text-xl text-stone-100 mb-3">{{ circle.label }}</h3>
<p class="text-stone-200 text-sm leading-relaxed mb-4">
<h3 class="text-xl text-ghost-100 mb-3">{{ circle.label }}</h3>
<p class="text-ghost-200 text-sm leading-relaxed mb-4">
{{ circle.description }}
</p>
<!-- Features as inline text -->
<div class="text-sm text-stone-400">
<div class="text-sm text-ghost-400">
<span v-for="(feature, i) in circle.features" :key="feature">
{{ feature
}}<span v-if="i < circle.features.length - 1"> </span>
@ -77,7 +77,7 @@
<!-- Why Join? - Diagonal Layout -->
<section class="mb-32 relative">
<div class="transform -rotate-1">
<h2 class="text-3xl font-light text-stone-200 mb-12">Why Join?</h2>
<h2 class="text-3xl font-light text-ghost-200 mb-12">Why Join?</h2>
</div>
<div class="ml-12 relative">
@ -86,11 +86,11 @@
/>
<div class="max-w-2xl">
<p class="text-stone-300 leading-loose text-lg mb-8">
<p class="text-ghost-300 leading-loose text-lg mb-8">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<p class="text-stone-400 leading-relaxed ml-8">
<p class="text-ghost-400 leading-relaxed ml-8">
Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.<br />
Ut enim ad minim veniam, quis nostrud exercitation.
@ -98,7 +98,7 @@
</div>
<div
class="absolute -bottom-8 right-0 text-6xl text-stone-800 opacity-20 font-bold"
class="absolute -bottom-8 right-0 text-6xl text-ghost-800 opacity-20 font-bold"
>
?
</div>

View file

@ -9,13 +9,13 @@
/>
<!-- Membership Sign Up Form -->
<section class="py-20 bg-white dark:bg-gray-900">
<section class="py-20 bg-[--ui-bg]">
<UContainer class="max-w-4xl">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
Membership Sign Up
</h2>
<p class="text-lg text-gray-700 dark:text-gray-300">
<p class="text-lg text-[--ui-text]">
Choose your circle to connect with others at your stage. Choose your
contribution based on what you can afford. Everyone gets full
access.
@ -30,8 +30,8 @@
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 1
? 'bg-gray-900 dark:bg-white text-white dark:text-gray-900'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500',
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
1
@ -39,21 +39,16 @@
<span
class="ml-2 font-medium"
:class="
currentStep === 1
? 'text-gray-900 dark:text-white'
: 'text-gray-500'
currentStep === 1 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Information
</span>
</div>
<div
v-if="needsPayment"
class="w-16 h-1 bg-gray-200 dark:bg-gray-700"
>
<div v-if="needsPayment" class="w-16 h-1 bg-neutral-200">
<div
class="h-full bg-gray-900 dark:bg-white transition-all"
class="h-full bg-neutral-900 transition-all"
:style="{ width: currentStep >= 2 ? '100%' : '0%' }"
/>
</div>
@ -63,8 +58,8 @@
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 2
? 'bg-gray-900 dark:bg-white text-white dark:text-gray-900'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500',
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
2
@ -72,18 +67,16 @@
<span
class="ml-2 font-medium"
:class="
currentStep === 2
? 'text-gray-900 dark:text-white'
: 'text-gray-500'
currentStep === 2 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Payment
</span>
</div>
<div class="w-16 h-1 bg-gray-200 dark:bg-gray-700">
<div class="w-16 h-1 bg-neutral-200">
<div
class="h-full bg-gray-900 dark:bg-white transition-all"
class="h-full bg-neutral-900 transition-all"
:style="{ width: currentStep >= 3 ? '100%' : '0%' }"
/>
</div>
@ -93,8 +86,8 @@
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 3
? 'bg-gray-900 dark:bg-white text-white dark:text-gray-900'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500',
? 'bg-neutral-900 text-neutral-50'
: 'bg-neutral-200 text-neutral-500',
]"
>
<span v-if="needsPayment">3</span>
@ -103,9 +96,7 @@
<span
class="ml-2 font-medium"
:class="
currentStep === 3
? 'text-gray-900 dark:text-white'
: 'text-gray-500'
currentStep === 3 ? 'text-[--ui-text]' : 'text-neutral-500'
"
>
Confirmation
@ -120,7 +111,7 @@
</div>
<!-- Step 1: Information -->
<div v-if="currentStep === 1" class="bg-white dark:bg-gray-800">
<div v-if="currentStep === 1" class="bg-[--ui-bg-elevated]">
<UForm :state="form" class="space-y-8" @submit="handleSubmit">
<!-- Personal Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -154,8 +145,8 @@
class="flex flex-col p-6 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md"
:class="
form.circle === option.value
? 'border-gray-900 dark:border-white bg-gray-50 dark:bg-gray-800'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-500'
? 'border-neutral-900 bg-[--ui-bg]'
: 'border-neutral-200 hover:border-neutral-400'
"
>
<input
@ -166,12 +157,12 @@
class="mb-3"
/>
<div class="font-medium text-lg mb-2">{{ option.label }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
<div class="text-sm text-[--ui-text-muted]">
{{ option.description }}
</div>
</label>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-3 italic">
<p class="text-sm text-[--ui-text-muted] mt-3 italic">
Not sure? Start with Community - you can always move.
</p>
</div>
@ -213,23 +204,23 @@
<!-- Step 2: Payment -->
<div
v-if="currentStep === 2"
class="bg-white dark:bg-gray-800 rounded-xl p-8"
class="bg-[--ui-bg-elevated] rounded-xl p-8"
>
<div class="mb-6">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
Payment Information
</h3>
<p class="text-gray-600 dark:text-gray-400">
<p class="text-[--ui-text-muted]">
You're signing up for the {{ selectedTier.label }} plan
</p>
<p class="text-lg font-semibold text-gray-900 dark:text-white mt-2">
<p class="text-lg font-semibold text-[--ui-text] mt-2">
${{ selectedTier.amount }} CAD / month
</p>
</div>
<!-- Payment Instructions -->
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6">
<p class="text-gray-700 dark:text-gray-300">
<div class="bg-[--ui-bg] rounded-lg p-6 mb-6">
<p class="text-[--ui-text]">
Click "Complete Payment" below to open the secure payment modal
and verify your payment method.
</p>
@ -254,11 +245,11 @@
<!-- Step 3: Confirmation -->
<div
v-if="currentStep === 3"
class="bg-white dark:bg-gray-800 rounded-xl p-8"
class="bg-[--ui-bg-elevated] rounded-xl p-8"
>
<div class="text-center">
<div
class="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6"
class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6"
>
<svg
class="w-10 h-10 text-green-500"
@ -275,7 +266,7 @@
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
<h3 class="text-2xl font-bold text-[--ui-text] mb-4">
Welcome to Ghost Guild!
</h3>
@ -283,43 +274,39 @@
<UAlert color="green" :title="successMessage" />
</div>
<div
class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6 text-left"
>
<div class="bg-[--ui-bg] rounded-lg p-6 mb-6 text-left">
<h4 class="font-semibold mb-3">Membership Details:</h4>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Name:</dt>
<dt class="text-[--ui-text-muted]">Name:</dt>
<dd class="font-medium">{{ form.name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Email:</dt>
<dt class="text-[--ui-text-muted]">Email:</dt>
<dd class="font-medium">{{ form.email }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Circle:</dt>
<dt class="text-[--ui-text-muted]">Circle:</dt>
<dd class="font-medium capitalize">{{ form.circle }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">
Contribution:
</dt>
<dt class="text-[--ui-text-muted]">Contribution:</dt>
<dd class="font-medium">{{ selectedTier.label }}</dd>
</div>
<div v-if="customerCode" class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Member ID:</dt>
<dt class="text-[--ui-text-muted]">Member ID:</dt>
<dd class="font-medium">{{ customerCode }}</dd>
</div>
</dl>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-4">
<p class="text-[--ui-text-muted] mb-4">
We've sent a confirmation email to {{ form.email }} with your
membership details.
</p>
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 mb-8">
<p class="text-gray-700 dark:text-gray-300 text-center">
<div class="bg-[--ui-bg] rounded-lg p-4 mb-8">
<p class="text-[--ui-text] text-center">
You will be automatically redirected to your dashboard in a few
seconds...
</p>
@ -339,14 +326,14 @@
</section>
<!-- How Ghost Guild Works -->
<section class="py-20 bg-gray-50 dark:bg-gray-800">
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
How Ghost Guild Works
</h2>
<p class="text-lg text-gray-700 dark:text-gray-300">
<p class="text-lg text-[--ui-text]">
Every member gets everything. Your circle helps you find relevant
content and peers. Your contribution helps sustain our solidarity
economy.
@ -355,13 +342,11 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Full Access -->
<div class="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3
class="text-xl font-semibold mb-4 text-gray-900 dark:text-white"
>
<div class="bg-[--ui-bg] rounded-xl p-6">
<h3 class="text-xl font-semibold mb-4 text-[--ui-text]">
Full Access
</h3>
<ul class="text-gray-700 dark:text-gray-300 space-y-2">
<ul class="text-[--ui-text] space-y-2">
<li>Complete resource library</li>
<li>All workshops and events</li>
<li>Slack community</li>
@ -371,13 +356,11 @@
</div>
<!-- Circle-Specific Guidance -->
<div class="bg-white dark:bg-gray-900 rounded-xl p-6">
<h3
class="text-xl font-semibold mb-4 text-gray-900 dark:text-white"
>
<div class="bg-[--ui-bg] rounded-xl p-6">
<h3 class="text-xl font-semibold mb-4 text-[--ui-text]">
Circle-Specific Guidance
</h3>
<ul class="text-gray-700 dark:text-gray-300 space-y-2">
<ul class="text-[--ui-text] space-y-2">
<li>Curated resources for your stage</li>
<li>Connection with peers on similar journeys</li>
<li>Relevant workshop recommendations</li>
@ -390,25 +373,23 @@
</section>
<!-- How to Join -->
<section class="py-20 bg-white dark:bg-gray-900">
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<div class="space-y-8">
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-gray-900 dark:bg-white rounded-full flex items-center justify-center text-white dark:text-gray-900 font-bold text-xl"
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
1
</div>
</div>
<div class="flex-1">
<h3
class="text-xl font-semibold mb-2 text-gray-900 dark:text-white"
>
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Pick your circle
</h3>
<p class="text-gray-700 dark:text-gray-300">
<p class="text-[--ui-text]">
Where are you in your co-op journey? Select based on where you
are in your cooperative journey - exploring, building, or
practicing. Not sure? Start with Community.
@ -419,18 +400,16 @@
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-gray-900 dark:bg-white rounded-full flex items-center justify-center text-white dark:text-gray-900 font-bold text-xl"
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
2
</div>
</div>
<div class="flex-1">
<h3
class="text-xl font-semibold mb-2 text-gray-900 dark:text-white"
>
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Choose your contribution
</h3>
<p class="text-gray-700 dark:text-gray-300">
<p class="text-[--ui-text]">
What can you afford? ($0-50+/month) Choose based on your
financial capacity. From $0 for those who need support to $50+
for those who can sponsor others. You can adjust anytime.
@ -441,18 +420,16 @@
<div class="flex items-start gap-6">
<div class="flex-shrink-0">
<div
class="w-12 h-12 bg-gray-900 dark:bg-white rounded-full flex items-center justify-center text-white dark:text-gray-900 font-bold text-xl"
class="w-12 h-12 bg-neutral-900 rounded-full flex items-center justify-center text-neutral-50 font-bold text-xl"
>
3
</div>
</div>
<div class="flex-1">
<h3
class="text-xl font-semibold mb-2 text-gray-900 dark:text-white"
>
<h3 class="text-xl font-semibold mb-2 text-[--ui-text]">
Join the community
</h3>
<p class="text-gray-700 dark:text-gray-300">
<p class="text-[--ui-text]">
Get instant access to everything. Fill out your profile, agree
to our community guidelines, and complete payment (if
applicable). You'll get instant access to our community.

View file

@ -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">
<p class="text-[--ui-text-muted]">
Don't have an account?
<NuxtLink to="/join" class="text-blue-600 dark:text-blue-400 hover:underline font-medium">
<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,36 +168,39 @@
</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
@ -195,10 +216,10 @@
</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>
@ -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))
await new Promise((resolve) => setTimeout(resolve, 1500));
resetSuccess.value = true
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>

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>

View file

@ -11,7 +11,7 @@
<UContainer class="px-4">
<!-- Search and Filters -->
<div class="mb-8 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Search -->
<div class="md:col-span-2">
<UInput
@ -26,17 +26,24 @@
<!-- Circle Filter -->
<USelect
v-model="selectedCircle"
:options="circleOptions"
placeholder="All Circles"
:items="circleOptions"
size="lg"
@change="loadMembers"
@update:model-value="loadMembers"
/>
<!-- Peer Support Filter -->
<USelect
v-model="peerSupportFilter"
:items="peerSupportOptions"
size="lg"
@update:model-value="loadMembers"
/>
</div>
<!-- Skills Filter -->
<div v-if="availableSkills && availableSkills.length > 0">
<div class="flex flex-wrap gap-2">
<span class="text-sm text-stone-400 mr-2 self-center"
<span class="text-sm text-ghost-400 mr-2 self-center"
>Filter by skill:</span
>
<button
@ -50,7 +57,7 @@
:class="
selectedSkills.includes(skill)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-stone-800/50 text-stone-400 border-stone-700 hover:border-stone-600'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600'
"
@click="toggleSkill(skill)"
>
@ -71,12 +78,55 @@
</div>
</div>
<!-- Peer Support Topics Filter -->
<div v-if="availableTopics && availableTopics.length > 0">
<div class="flex flex-wrap gap-2">
<span class="text-sm text-ghost-400 mr-2 self-center"
>Filter by peer support topic:</span
>
<button
v-for="topic in (availableTopics || []).slice(
0,
showAllTopics ? undefined : 10,
)"
:key="topic"
type="button"
class="px-3 py-1 rounded-full text-sm transition-all border"
:class="
selectedTopics.includes(topic)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-ghost-800/50 text-ghost-400 border-ghost-700 hover:border-ghost-600'
"
@click="toggleTopic(topic)"
>
{{ topic }}
</button>
<button
v-if="availableTopics && availableTopics.length > 10"
type="button"
class="px-3 py-1 text-sm text-purple-400 hover:text-purple-300"
@click="showAllTopics = !showAllTopics"
>
{{
showAllTopics
? "Show less"
: `+${availableTopics.length - 10} more`
}}
</button>
</div>
</div>
<!-- Active Filters -->
<div
v-if="selectedCircle || selectedSkills.length > 0"
class="flex items-center gap-2 text-sm"
v-if="
selectedCircle ||
peerSupportFilter ||
selectedSkills.length > 0 ||
selectedTopics.length > 0
"
class="flex items-center gap-2 text-sm flex-wrap"
>
<span class="text-stone-400">Active filters:</span>
<span class="text-ghost-400">Active filters:</span>
<span
v-if="selectedCircle"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
@ -90,8 +140,21 @@
×
</button>
</span>
<span
v-if="peerSupportFilter"
class="px-2 py-1 bg-purple-500/20 text-purple-300 rounded-full border border-purple-500/30 flex items-center gap-1"
>
Offering Peer Support
<button
type="button"
class="hover:text-purple-200"
@click="clearPeerSupportFilter"
>
×
</button>
</span>
<button
v-if="selectedSkills.length > 0"
v-if="selectedSkills.length > 0 || selectedTopics.length > 0"
type="button"
class="text-purple-400 hover:text-purple-300"
@click="clearAllFilters"
@ -110,138 +173,282 @@
<div
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-stone-400">Loading members...</p>
<p class="text-ghost-400">Loading members...</p>
</div>
</div>
<!-- Members List -->
<div v-else-if="members.length > 0">
<div class="mb-4 text-stone-400 text-sm">
<div class="mb-4 text-ghost-400 text-sm">
{{ totalCount }} {{ totalCount === 1 ? "member" : "members" }} found
</div>
<div class="space-y-2">
<div class="space-y-4">
<div
v-for="member in members"
:key="member._id"
class="backdrop-blur-sm bg-stone-900/50 border border-stone-700/50 rounded-lg p-4 hover:border-purple-500/50 transition-all group flex items-center gap-4"
class="backdrop-blur-sm bg-ghost-900/50 border border-ghost-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
>
<!-- Avatar -->
<div
class="w-12 h-12 rounded-lg bg-stone-800 border border-stone-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
>
<img
v-if="member.avatar"
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
:alt="member.name"
class="w-8 h-8 object-contain"
/>
<span v-else class="text-xl text-stone-600">👻</span>
</div>
<!-- Header Section -->
<div class="flex items-start gap-4 mb-4">
<!-- Avatar -->
<div
class="w-16 h-16 rounded-lg bg-ghost-800 border border-ghost-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
>
<img
v-if="member.avatar"
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
:alt="member.name"
class="w-12 h-12 object-contain"
/>
<span v-else class="text-2xl text-ghost-600">👻</span>
</div>
<!-- Name and Info -->
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap">
<NuxtLink
:to="`/updates/user/${member._id}`"
class="font-semibold text-stone-100 hover:text-purple-300 transition-colors"
<!-- Name and Meta Info -->
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap mb-2">
<NuxtLink
:to="`/updates/user/${member._id}`"
class="font-semibold text-lg text-ghost-100 hover:text-purple-300 transition-colors"
>
{{ member.name }}
</NuxtLink>
<span v-if="member.pronouns" class="text-sm text-ghost-400">
{{ member.pronouns }}
</span>
</div>
<div class="flex items-center gap-2 flex-wrap mb-2">
<span
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30"
>
{{ circleLabels[member.circle] }}
</span>
<span v-if="member.studio" class="text-sm text-ghost-400">
{{ member.studio }}
</span>
<span v-if="member.location" class="text-sm text-ghost-500">
📍 {{ member.location }}
</span>
<span v-if="member.timeZone" class="text-sm text-ghost-500">
🕐 {{ member.timeZone }}
</span>
</div>
<!-- Social Links -->
<div
v-if="
member.socialLinks && hasSocialLinks(member.socialLinks)
"
class="flex gap-3"
>
{{ member.name }}
</NuxtLink>
<span v-if="member.pronouns" class="text-sm text-stone-400">
{{ member.pronouns }}
</span>
<span
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30"
>
{{ circleLabels[member.circle] }}
</span>
<span v-if="member.studio" class="text-sm text-stone-400">
{{ member.studio }}
</span>
<span v-if="member.location" class="text-sm text-stone-500">
{{ member.location }}
</span>
<a
v-if="member.socialLinks.mastodon"
:href="member.socialLinks.mastodon"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Mastodon"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M23.193 7.879c0-5.206-3.411-6.732-3.411-6.732C18.062.357 15.108.025 12.041 0h-.076c-3.068.025-6.02.357-7.74 1.147 0 0-3.411 1.526-3.411 6.732 0 1.192-.023 2.618.015 4.129.124 5.092.934 10.109 5.641 11.355 2.17.574 4.034.695 5.535.612 2.722-.15 4.25-.972 4.25-.972l-.09-1.975s-1.945.613-4.129.539c-2.165-.074-4.449-.233-4.799-2.891a5.499 5.499 0 0 1-.048-.745s2.125.52 4.817.643c1.646.075 3.19-.097 4.758-.283 3.007-.359 5.625-2.212 5.954-3.905.517-2.665.475-6.507.475-6.507zm-4.024 6.709h-2.497V8.469c0-1.29-.543-1.944-1.628-1.944-1.2 0-1.802.776-1.802 2.312v3.349h-2.483v-3.35c0-1.536-.602-2.312-1.802-2.312-1.085 0-1.628.655-1.628 1.944v6.119H4.832V8.284c0-1.289.328-2.313.987-3.07.68-.758 1.569-1.146 2.674-1.146 1.278 0 2.246.491 2.886 1.474L12 6.585l.622-1.043c.64-.983 1.608-1.474 2.886-1.474 1.104 0 1.994.388 2.674 1.146.658.757.986 1.781.986 3.07v6.304z"
/>
</svg>
</a>
<a
v-if="member.socialLinks.linkedin"
:href="member.socialLinks.linkedin"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="LinkedIn"
>
<svg
class="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>
</a>
<a
v-if="member.socialLinks.website"
:href="member.socialLinks.website"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Website"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
/>
</svg>
</a>
<a
v-if="member.socialLinks.other"
:href="member.socialLinks.other"
target="_blank"
rel="noopener noreferrer"
class="text-ghost-400 hover:text-purple-400 transition-colors"
title="Other link"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</a>
</div>
</div>
</div>
<!-- Social Links -->
<!-- Bio -->
<div v-if="member.bio" class="mb-4">
<p class="text-ghost-300 text-sm leading-relaxed">
{{ member.bio }}
</p>
</div>
<!-- Peer Support Section -->
<div
v-if="member.socialLinks && hasSocialLinks(member.socialLinks)"
class="flex gap-3 flex-shrink-0"
v-if="member.peerSupport?.enabled"
class="mb-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg"
>
<a
v-if="member.socialLinks.mastodon"
:href="member.socialLinks.mastodon"
target="_blank"
rel="noopener noreferrer"
class="text-stone-400 hover:text-purple-400 transition-colors"
title="Mastodon"
<div class="flex items-center gap-2 mb-2">
<span class="text-purple-300 font-medium text-sm">
💜 Offering Peer Support
</span>
</div>
<!-- Topics -->
<div
v-if="
member.peerSupport.topics &&
member.peerSupport.topics.length > 0
"
class="mb-2"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M23.193 7.879c0-5.206-3.411-6.732-3.411-6.732C18.062.357 15.108.025 12.041 0h-.076c-3.068.025-6.02.357-7.74 1.147 0 0-3.411 1.526-3.411 6.732 0 1.192-.023 2.618.015 4.129.124 5.092.934 10.109 5.641 11.355 2.17.574 4.034.695 5.535.612 2.722-.15 4.25-.972 4.25-.972l-.09-1.975s-1.945.613-4.129.539c-2.165-.074-4.449-.233-4.799-2.891a5.499 5.499 0 0 1-.048-.745s2.125.52 4.817.643c1.646.075 3.19-.097 4.758-.283 3.007-.359 5.625-2.212 5.954-3.905.517-2.665.475-6.507.475-6.507zm-4.024 6.709h-2.497V8.469c0-1.29-.543-1.944-1.628-1.944-1.2 0-1.802.776-1.802 2.312v3.349h-2.483v-3.35c0-1.536-.602-2.312-1.802-2.312-1.085 0-1.628.655-1.628 1.944v6.119H4.832V8.284c0-1.289.328-2.313.987-3.07.68-.758 1.569-1.146 2.674-1.146 1.278 0 2.246.491 2.886 1.474L12 6.585l.622-1.043c.64-.983 1.608-1.474 2.886-1.474 1.104 0 1.994.388 2.674 1.146.658.757.986 1.781.986 3.07v6.304z"
/>
</svg>
<div class="flex flex-wrap gap-1">
<span
v-for="topic in member.peerSupport.topics"
:key="topic"
class="px-2 py-0.5 bg-purple-500/20 text-purple-200 rounded text-xs border border-purple-500/40"
>
{{ topic }}
</span>
</div>
</div>
<!-- Personal Message -->
<div
v-if="member.peerSupport.personalMessage"
class="text-sm text-ghost-300 italic mb-2"
>
"{{ member.peerSupport.personalMessage }}"
</div>
<!-- Availability -->
<div
v-if="member.peerSupport.availability"
class="text-xs text-ghost-400 mb-2"
>
Availability: {{ member.peerSupport.availability }}
</div>
<!-- Contact Button -->
<a
v-if="member.peerSupport.slackUsername"
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
@click.prevent="openSlackDM(member)"
class="inline-block px-3 py-1.5 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer"
>
Message {{ member.peerSupport.slackUsername }} on Slack
</a>
<a
v-if="member.socialLinks.linkedin"
:href="member.socialLinks.linkedin"
target="_blank"
rel="noopener noreferrer"
class="text-stone-400 hover:text-purple-400 transition-colors"
title="LinkedIn"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>
</a>
<a
v-if="member.socialLinks.website"
:href="member.socialLinks.website"
target="_blank"
rel="noopener noreferrer"
class="text-stone-400 hover:text-purple-400 transition-colors"
title="Website"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
</div>
<!-- Offering and Looking For -->
<div
v-if="member.offering || member.lookingFor"
class="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<!-- Offering -->
<div v-if="member.offering" class="space-y-2">
<h4 class="text-xs font-semibold text-purple-400 uppercase">
Offering
</h4>
<p
v-if="member.offering.description"
class="text-ghost-300 text-sm"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
/>
</svg>
</a>
<a
v-if="member.socialLinks.other"
:href="member.socialLinks.other"
target="_blank"
rel="noopener noreferrer"
class="text-stone-400 hover:text-purple-400 transition-colors"
title="Other link"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
{{ member.offering.description }}
</p>
<div
v-if="
member.offering.tags && member.offering.tags.length > 0
"
class="flex flex-wrap gap-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
/>
</svg>
</a>
<span
v-for="tag in member.offering.tags"
:key="tag"
class="px-2 py-0.5 bg-green-500/20 text-green-300 rounded text-xs border border-green-500/30"
>
{{ tag }}
</span>
</div>
</div>
<!-- Looking For -->
<div v-if="member.lookingFor" class="space-y-2">
<h4 class="text-xs font-semibold text-purple-400 uppercase">
Looking For
</h4>
<p
v-if="member.lookingFor.description"
class="text-ghost-300 text-sm"
>
{{ member.lookingFor.description }}
</p>
<div
v-if="
member.lookingFor.tags &&
member.lookingFor.tags.length > 0
"
class="flex flex-wrap gap-1"
>
<span
v-for="tag in member.lookingFor.tags"
:key="tag"
class="px-2 py-0.5 bg-blue-500/20 text-blue-300 rounded text-xs border border-blue-500/30"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</div>
@ -254,7 +461,7 @@
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
class="text-stone-600"
class="text-ghost-600"
>
<path
stroke-linecap="round"
@ -264,10 +471,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 members found
</h3>
<p class="text-stone-400 mb-6">
<p class="text-ghost-400 mb-6">
Try adjusting your search or filters
</p>
<UButton variant="outline" @click="clearAllFilters">
@ -300,15 +507,19 @@ const { isAuthenticated } = useAuth();
const members = ref([]);
const totalCount = ref(0);
const availableSkills = ref([]);
const availableTopics = ref([]);
const loading = ref(true); // Start with loading true
const searchQuery = ref("");
const selectedCircle = ref("");
const selectedCircle = ref("all");
const peerSupportFilter = ref("all");
const selectedSkills = ref([]);
const selectedTopics = ref([]);
const showAllSkills = ref(false);
const showAllTopics = ref(false);
// Circle options
const circleOptions = [
{ label: "All Circles", value: "" },
{ label: "All Circles", value: "all" },
{ label: "Community", value: "community" },
{ label: "Founder", value: "founder" },
{ label: "Practitioner", value: "practitioner" },
@ -320,6 +531,12 @@ const circleLabels = {
practitioner: "Practitioner",
};
// Peer support filter options
const peerSupportOptions = [
{ label: "All Members", value: "all" },
{ label: "Offering Peer Support", value: "true" },
];
// Helper to check if member has social links
const hasSocialLinks = (links) => {
if (!links) return false;
@ -333,20 +550,27 @@ const loadMembers = async () => {
try {
const params = {};
if (searchQuery.value) params.search = searchQuery.value;
if (selectedCircle.value) params.circle = selectedCircle.value;
if (selectedCircle.value && selectedCircle.value !== "all")
params.circle = selectedCircle.value;
if (peerSupportFilter.value && peerSupportFilter.value !== "all")
params.peerSupport = peerSupportFilter.value;
if (selectedSkills.value.length > 0)
params.skills = selectedSkills.value.join(",");
if (selectedTopics.value.length > 0)
params.topics = selectedTopics.value.join(",");
const data = await $fetch("/api/members/directory", { params });
members.value = data.members || [];
totalCount.value = data.totalCount || 0;
availableSkills.value = data.filters?.availableSkills || [];
availableTopics.value = data.filters?.availableTopics || [];
} catch (error) {
console.error("Failed to load members:", error);
members.value = [];
totalCount.value = 0;
availableSkills.value = [];
availableTopics.value = [];
} finally {
loading.value = false;
}
@ -372,19 +596,53 @@ const toggleSkill = (skill) => {
loadMembers();
};
// Toggle topic filter
const toggleTopic = (topic) => {
const index = selectedTopics.value.indexOf(topic);
if (index > -1) {
selectedTopics.value.splice(index, 1);
} else {
selectedTopics.value.push(topic);
}
loadMembers();
};
// Clear filters
const clearCircleFilter = () => {
selectedCircle.value = "";
loadMembers();
selectedCircle.value = "all";
};
const clearPeerSupportFilter = () => {
peerSupportFilter.value = "all";
};
const clearAllFilters = () => {
searchQuery.value = "";
selectedCircle.value = "";
selectedCircle.value = "all";
peerSupportFilter.value = "all";
selectedSkills.value = [];
selectedTopics.value = [];
loadMembers();
};
// Slack DM functionality
const openSlackDM = async (member) => {
const username = member.peerSupport?.slackUsername || member.name;
// Copy username to clipboard
try {
await navigator.clipboard.writeText(username);
} catch (err) {
console.log("Could not copy to clipboard:", err);
}
// Show alert and open Slack
alert(
`Opening Slack...\n\nSearch for: ${username}\n\n(Username copied to clipboard)`,
);
window.open("https://gammaspace.slack.com", "_blank");
};
// Load on mount
onMounted(() => {
loadMembers();

View file

@ -1,338 +1,12 @@
<template>
<div>
<PageHeader
title="Peer Support"
subtitle="Connect with fellow members for 1:1 guidance and support"
theme="purple"
size="medium"
/>
<section class="py-12 px-4">
<UContainer class="px-4">
<!-- Intro Text -->
<div
class="mb-8 backdrop-blur-sm bg-stone-900/50 border border-stone-700/50 rounded-lg p-6"
>
<p class="text-stone-300 mb-2">
Ghost Guild members offering peer support on various topics. Reach
out to schedule a conversation, ask questions, or get feedback on
your cooperative journey.
</p>
<p class="text-sm text-stone-400">
Interested in offering peer support?
<NuxtLink
to="/member/settings/peer-support"
class="text-purple-400 hover:text-purple-300 underline"
>
Enable it in your settings
</NuxtLink>
</p>
</div>
<!-- Topic Filter -->
<div v-if="availableTopics.length > 0" class="mb-8">
<div class="flex flex-wrap gap-2">
<span class="text-sm text-stone-400 mr-2 self-center"
>Filter by topic:</span
>
<button
type="button"
class="px-3 py-1 rounded-full text-sm transition-all border"
:class="
!selectedTopic
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-stone-800/50 text-stone-400 border-stone-700 hover:border-stone-600'
"
@click="
selectedTopic = null;
loadSupporters();
"
>
All Topics
</button>
<button
v-for="topic in availableTopics"
:key="topic"
type="button"
class="px-3 py-1 rounded-full text-sm transition-all border"
:class="
selectedTopic === topic
? 'bg-purple-500/20 text-purple-300 border-purple-500/50'
: 'bg-stone-800/50 text-stone-400 border-stone-700 hover:border-stone-600'
"
@click="
selectedTopic = topic;
loadSupporters();
"
>
{{ topic }}
</button>
</div>
</div>
<!-- Loading State -->
<div
v-if="loading && !supporters.length"
class="flex justify-center items-center py-20"
>
<div class="text-center">
<div
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div>
<p class="text-stone-400">Loading peer supporters...</p>
</div>
</div>
<!-- Supporters Grid -->
<div v-else-if="supporters.length > 0">
<div class="mb-4 text-stone-400 text-sm">
{{ totalCount }}
{{ totalCount === 1 ? "peer supporter" : "peer supporters" }}
available
</div>
<div class="space-y-4 max-w-3xl">
<div
v-for="supporter in supporters"
:key="supporter._id"
class="backdrop-blur-sm bg-stone-900/50 border border-stone-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
>
<!-- Avatar and Name -->
<div class="flex items-center gap-3 mb-4">
<div
class="w-12 h-12 rounded-lg bg-stone-800 border border-stone-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
>
<img
v-if="supporter.avatar"
:src="`/ghosties/Ghost-${supporter.avatar.charAt(0).toUpperCase() + supporter.avatar.slice(1)}.png`"
:alt="supporter.name"
class="w-8 h-8 object-contain"
/>
<span v-else class="text-xl text-stone-600">👻</span>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-stone-100 truncate">
{{ supporter.name }}
</h3>
<span
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30 inline-block"
>
{{ circleLabels[supporter.circle] }}
</span>
</div>
</div>
<!-- Topics -->
<div
v-if="
supporter.peerSupport?.topics &&
supporter.peerSupport.topics.length > 0
"
class="mb-4"
>
<div class="text-xs text-stone-500 mb-2">Topics:</div>
<div class="flex flex-wrap gap-1">
<span
v-for="topic in supporter.peerSupport.topics"
:key="topic"
class="px-2 py-0.5 bg-stone-800/50 text-stone-300 rounded text-xs border border-stone-700"
>
{{ topic }}
</span>
</div>
</div>
<!-- Availability -->
<div
v-if="supporter.peerSupport?.availability"
class="mb-4 text-sm text-stone-400"
>
<div class="text-xs text-stone-500 mb-1">Availability:</div>
{{ supporter.peerSupport.availability }}
</div>
<!-- Personal Message -->
<div
v-if="supporter.peerSupport?.personalMessage"
class="mb-4 text-sm text-stone-300 italic bg-stone-800/30 rounded-lg p-3 border-l-2 border-purple-500/50"
>
"{{ supporter.peerSupport.personalMessage }}"
</div>
<!-- Action Buttons -->
<div class="flex gap-2 mt-4">
<a
v-if="supporter.peerSupport?.slackUsername"
:href="getSlackDMLink(supporter)"
@click.prevent="openSlackDM(supporter)"
class="flex-1 px-3 py-2 bg-purple-500/20 text-purple-300 rounded border border-purple-500/30 hover:bg-purple-500/30 transition-colors text-center text-sm font-medium cursor-pointer"
>
Message {{ supporter.peerSupport.slackUsername }} on Slack
</a>
<a
v-else
href="slack://open"
@click.prevent="openSlackApp"
class="flex-1 px-3 py-2 bg-stone-800/50 text-stone-300 rounded border border-stone-700 hover:border-stone-600 transition-colors text-center text-sm font-medium cursor-pointer"
>
Find on Slack
</a>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-20">
<div class="w-16 h-16 mx-auto mb-4 opacity-50">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
class="text-stone-600"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
</svg>
</div>
<h3 class="text-lg font-medium text-stone-300 mb-2">
No peer supporters yet
</h3>
<p class="text-stone-400 mb-6">
Be the first to offer peer support to the community!
</p>
<UButton to="/member/settings/peer-support">
Enable Peer Support
</UButton>
</div>
<!-- CTA for Members -->
<div
v-if="isAuthenticated && supporters.length > 0"
class="mt-8 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-6 text-center"
>
<p class="text-purple-200 mb-4">
💜 Want to offer peer support to fellow members?
</p>
<UButton to="/member/settings/peer-support" variant="outline">
Set Up Peer Support
</UButton>
</div>
<!-- Not Authenticated Notice -->
<div
v-if="!isAuthenticated"
class="mt-8 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-6 text-center"
>
<p class="text-purple-200 mb-4">
🔒 Peer support is available to Ghost Guild members
</p>
<div class="flex gap-3 justify-center">
<UButton to="/login" variant="outline"> Log In </UButton>
<UButton to="/join"> Join Ghost Guild </UButton>
</div>
</div>
</UContainer>
</section>
</div>
<div></div>
</template>
<script setup>
const { isAuthenticated } = useAuth();
const { getSupporters } = usePeerSupport();
// State
const supporters = ref([]);
const totalCount = ref(0);
const availableTopics = ref([]);
const loading = ref(false);
const selectedTopic = ref(null);
// Gamma Space Slack team ID
const slackTeamId = "T03A96LV4";
// Circle labels
const circleLabels = {
community: "Community",
founder: "Founder",
practitioner: "Practitioner",
};
// Get Slack DM deep link
const getSlackDMLink = (supporter) => {
// If we have the DM channel ID, use it for direct DM
if (supporter.peerSupport?.slackDMChannelId) {
return `slack://channel?team=${slackTeamId}&id=${supporter.peerSupport.slackDMChannelId}`;
}
// Otherwise fall back to opening workspace
return "slack://open";
};
// Open Slack DM
const openSlackDM = async (supporter) => {
console.log("Opening Slack DM for supporter:", supporter);
const username = supporter.peerSupport?.slackUsername || supporter.name;
// Copy username to clipboard
try {
await navigator.clipboard.writeText(username);
console.log("Copied username to clipboard:", username);
} catch (err) {
console.log("Could not copy to clipboard:", err);
}
// Show a toast/alert (you can replace this with a proper toast notification)
alert(
`Opening Slack...\n\nSearch for: ${username}\n\n(Username copied to clipboard)`,
);
// Open Slack workspace
window.open("https://gammaspace.slack.com", "_blank");
};
const openSlackApp = () => {
// Try to open Slack app
window.location.href = "slack://open";
// Fallback to web after a short delay if app doesn't open
setTimeout(() => {
window.open("https://gammaspace.slack.com", "_blank");
}, 1000);
};
// Load supporters
const loadSupporters = async () => {
loading.value = true;
try {
const data = await getSupporters(selectedTopic.value);
supporters.value = data.supporters;
totalCount.value = data.totalCount;
availableTopics.value = data.filters.availableTopics;
} catch (error) {
console.error("Failed to load peer supporters:", error);
} finally {
loading.value = false;
}
};
// Load on mount
onMounted(() => {
loadSupporters();
});
useHead({
title: "Peer Support - Ghost Guild",
meta: [
{
name: "description",
content:
"Connect with Ghost Guild members for 1:1 guidance on governance, fundraising, team building, and more.",
},
],
// Redirect to members directory with peer support filter
definePageMeta({
middleware: defineNuxtRouteMiddleware(() => {
return navigateTo("/members?peerSupport=true");
}),
});
</script>

View file

@ -25,6 +25,8 @@ export default defineEventHandler(async (event) => {
const search = query.search || "";
const circle = query.circle || "";
const tags = query.tags ? query.tags.split(",") : [];
const peerSupport = query.peerSupport || "";
const topics = query.topics ? query.topics.split(",") : [];
// Build query
const dbQuery = {
@ -37,6 +39,11 @@ export default defineEventHandler(async (event) => {
dbQuery.circle = circle;
}
// Filter by peer support availability
if (peerSupport === "true") {
dbQuery["peerSupport.enabled"] = true;
}
// Search by name or bio
if (search) {
dbQuery.$or = [
@ -71,10 +78,15 @@ export default defineEventHandler(async (event) => {
}
}
// Filter by peer support topics
if (topics.length > 0) {
dbQuery["peerSupport.topics"] = { $in: topics };
}
try {
const members = await Member.find(dbQuery)
.select(
"name pronouns timeZone avatar studio bio location socialLinks offering lookingFor privacy circle createdAt",
"name pronouns timeZone avatar studio bio location socialLinks offering lookingFor privacy circle peerSupport slackUserId createdAt",
)
.sort({ createdAt: -1 })
.lean();
@ -109,6 +121,12 @@ export default defineEventHandler(async (event) => {
if (isVisible("offering")) filtered.offering = member.offering;
if (isVisible("lookingFor")) filtered.lookingFor = member.lookingFor;
// Always show peer support if enabled (it's opt-in, so public by nature)
if (member.peerSupport?.enabled) {
filtered.peerSupport = member.peerSupport;
filtered.slackUserId = member.slackUserId;
}
return filtered;
});
@ -121,11 +139,19 @@ export default defineEventHandler(async (event) => {
.filter((tag, index, self) => self.indexOf(tag) === index)
.sort();
// Get unique peer support topics
const allTopics = members
.filter((m) => m.peerSupport?.enabled)
.flatMap((m) => m.peerSupport?.topics || [])
.filter((topic, index, self) => self.indexOf(topic) === index)
.sort();
return {
members: filteredMembers,
totalCount: filteredMembers.length,
filters: {
availableTags: allTags,
availableSkills: allTags,
availableTopics: allTopics,
},
};
} catch (error) {

View file

@ -64,12 +64,16 @@ export default defineEventHandler(async (event) => {
// Handle offering and lookingFor separately (nested objects)
if (body.offering !== undefined) {
updateData["offering.text"] = body.offering.text || "";
updateData["offering.tags"] = body.offering.tags || [];
updateData.offering = {
text: body.offering.text || "",
tags: body.offering.tags || [],
};
}
if (body.lookingFor !== undefined) {
updateData["lookingFor.text"] = body.lookingFor.text || "";
updateData["lookingFor.tags"] = body.lookingFor.tags || [];
updateData.lookingFor = {
text: body.lookingFor.text || "",
tags: body.lookingFor.tags || [],
};
}
// Handle privacy settings

View file

@ -0,0 +1,62 @@
// Migration to fix offering and lookingFor field structure
// Run this once to convert string values to object structure
import mongoose from "mongoose";
import Member from "../models/member.js";
import { connectDB } from "../utils/mongoose.js";
async function migrateOfferingLookingFor() {
await connectDB();
console.log("Starting migration: fixing offering and lookingFor structure...");
try {
// Find all members where offering or lookingFor is a string (not an object)
const members = await Member.find({
$or: [
{ offering: { $type: "string" } },
{ lookingFor: { $type: "string" } },
],
});
console.log(`Found ${members.length} members to migrate`);
for (const member of members) {
const updates = {};
// Convert offering if it's a string
if (typeof member.offering === "string") {
updates.offering = {
text: member.offering,
tags: [],
};
console.log(
`Converting offering for member ${member._id}: "${member.offering}"`,
);
}
// Convert lookingFor if it's a string
if (typeof member.lookingFor === "string") {
updates.lookingFor = {
text: member.lookingFor,
tags: [],
};
console.log(
`Converting lookingFor for member ${member._id}: "${member.lookingFor}"`,
);
}
// Update the member
if (Object.keys(updates).length > 0) {
await Member.findByIdAndUpdate(member._id, { $set: updates });
}
}
console.log("Migration completed successfully!");
process.exit(0);
} catch (error) {
console.error("Migration failed:", error);
process.exit(1);
}
}
migrateOfferingLookingFor();