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: { formField: {
slots: { 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 "tailwindcss";
@import "@nuxt/ui"; @import "@nuxt/ui";
@theme static { @theme {
/* Font families */ /* Font families */
--font-sans: "Inter", sans-serif; --font-sans: "Inter", sans-serif;
--font-body: "Inter", sans-serif; --font-body: "Inter", sans-serif;
--font-mono: "Ubuntu Mono", monospace; --font-mono: "Ubuntu Mono", monospace;
--font-display: "NB Television Pro", 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-50: #f0f0f0;
--color-ghost-100: #d0d0d0; --color-ghost-100: #d0d0d0;
--color-ghost-200: #b0b0b0; --color-ghost-200: #b0b0b0;
@ -20,8 +58,8 @@
--color-ghost-700: #2a2a2a; --color-ghost-700: #2a2a2a;
--color-ghost-800: #1a1a1a; --color-ghost-800: #1a1a1a;
--color-ghost-900: #0a0a0a; --color-ghost-900: #0a0a0a;
/* Subtle accent - barely visible blue-gray */ /* Subtle accent - barely visible blue-gray (dark mode) */
--color-whisper-50: #d4dae6; --color-whisper-50: #d4dae6;
--color-whisper-100: #a8b3c7; --color-whisper-100: #a8b3c7;
--color-whisper-200: #8491a8; --color-whisper-200: #8491a8;
@ -33,7 +71,7 @@
--color-whisper-800: #1a1f2e; --color-whisper-800: #1a1f2e;
--color-whisper-900: #0f1419; --color-whisper-900: #0f1419;
/* Sparkle accent */ /* Sparkle accent (dark mode) */
--color-sparkle-50: #fafafa; --color-sparkle-50: #fafafa;
--color-sparkle-100: #f0f0f0; --color-sparkle-100: #f0f0f0;
--color-sparkle-200: #e8e8e8; --color-sparkle-200: #e8e8e8;
@ -46,33 +84,79 @@
--color-sparkle-900: #202020; --color-sparkle-900: #202020;
} }
/* Global ethereal background */ /* Global ethereal background - light mode */
:root { :root {
--ethereal-bg: radial-gradient(circle at 20% 80%, rgba(232, 232, 232, 0.03) 0%, transparent 50%), --ethereal-bg:
radial-gradient(circle at 80% 20%, rgba(232, 232, 232, 0.02) 0%, transparent 50%), radial-gradient(
radial-gradient(circle at 40% 40%, rgba(232, 232, 232, 0.01) 0%, transparent 50%); circle at 20% 80%,
rgba(40, 40, 40, 0.03) 0%,
--halftone-pattern: radial-gradient(circle, rgba(255,255,255,0.1) 1px, transparent 1px); 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(0, 0, 0, 0.1) 1px,
transparent 1px
);
--halftone-size: 8px 8px; --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 { html {
background: var(--color-ghost-900); @apply text-[--ui-text];
color: var(--color-ghost-200);
} }
body { body {
background: var(--ethereal-bg), var(--color-ghost-900); background: var(--ethereal-bg), #f0f0f0;
background-attachment: fixed; background-attachment: fixed;
} }
.dark body {
background: var(--ethereal-bg), #0a0a0a;
}
/* Halftone texture overlay */ /* Halftone texture overlay */
.halftone-texture { .halftone-texture {
position: relative; position: relative;
} }
.halftone-texture::before { .halftone-texture::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -86,14 +170,28 @@ body {
/* Sparkle effects */ /* Sparkle effects */
@keyframes sparkle { @keyframes sparkle {
0%, 100% { opacity: 0.3; transform: scale(0.8); } 0%,
50% { opacity: 1; transform: scale(1.2); } 100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
} }
@keyframes twinkle { @keyframes twinkle {
0%, 100% { opacity: 0.2; } 0%,
25% { opacity: 0.8; } 100% {
75% { opacity: 0.4; } opacity: 0.2;
}
25% {
opacity: 0.8;
}
75% {
opacity: 0.4;
}
} }
.sparkle-field { .sparkle-field {
@ -102,20 +200,50 @@ body {
} }
.sparkle-field::after { .sparkle-field::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-image: background-image:
radial-gradient(circle at 10% 20%, var(--color-sparkle-200) 1px, transparent 1px), radial-gradient(
radial-gradient(circle at 90% 80%, var(--color-sparkle-400) 1px, transparent 1px), circle at 10% 20%,
radial-gradient(circle at 30% 70%, var(--color-sparkle-200) 0.5px, transparent 0.5px), var(--color-sparkle-200) 1px,
radial-gradient(circle at 70% 30%, var(--color-sparkle-400) 0.5px, transparent 0.5px), transparent 1px
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); radial-gradient(
background-size: 200px 200px, 300px 300px, 150px 150px, 250px 250px, 180px 180px, 220px 220px; 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; animation: twinkle 4s infinite ease-in-out;
pointer-events: none; pointer-events: none;
opacity: 0.6; opacity: 0.6;
@ -123,7 +251,7 @@ body {
/* Ethereal glow effects */ /* Ethereal glow effects */
.ethereal-glow { .ethereal-glow {
box-shadow: box-shadow:
0 0 20px rgba(232, 232, 232, 0.1), 0 0 20px rgba(232, 232, 232, 0.1),
0 0 40px rgba(232, 232, 232, 0.05), 0 0 40px rgba(232, 232, 232, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.1); inset 0 1px 0 rgba(255, 255, 255, 0.1);
@ -135,12 +263,28 @@ body {
/* Dithered gradients */ /* Dithered gradients */
.dithered-bg { .dithered-bg {
background: background:
linear-gradient(45deg, var(--color-ghost-800) 25%, transparent 25%), linear-gradient(45deg, var(--color-ghost-800) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-ghost-800) 25%, transparent 25%), linear-gradient(-45deg, var(--color-ghost-800) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-ghost-700) 75%), linear-gradient(45deg, transparent 75%, var(--color-ghost-700) 75%),
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-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 --> <!-- Left: Copyright and minimal info -->
<div> <div>
<p class="text-stone-500 text-xs mb-2"> <p class="text-ghost-500 text-xs mb-2">
© {{ currentYear }} Ghost Guild © {{ currentYear }} Ghost Guild
</p> </p>
</div> </div>
@ -16,7 +16,7 @@
<div class="flex flex-wrap gap-6 text-xs"> <div class="flex flex-wrap gap-6 text-xs">
<a <a
href="mailto:hello@ghostguild.org" 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 Contact
</a> </a>

View file

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

View file

@ -2,14 +2,14 @@
<div class="space-y-4"> <div class="space-y-4">
<!-- Current Image Preview --> <!-- Current Image Preview -->
<div v-if="modelValue?.url" class="relative"> <div v-if="modelValue?.url" class="relative">
<img <img
:src="transformedImageUrl" :src="transformedImageUrl"
:alt="modelValue.alt || 'Event image'" :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)" @error="console.log('Image failed to load:', transformedImageUrl)"
@load="console.log('Image loaded successfully:', transformedImageUrl)" @load="console.log('Image loaded successfully:', transformedImageUrl)"
/> />
<button <button
@click="removeImage" @click="removeImage"
type="button" type="button"
class="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors" class="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
@ -19,7 +19,7 @@
</div> </div>
<!-- Upload Area --> <!-- Upload Area -->
<div <div
v-if="!modelValue?.url" v-if="!modelValue?.url"
class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors" class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors"
@dragover.prevent="isDragging = true" @dragover.prevent="isDragging = true"
@ -34,12 +34,12 @@
@change="handleFileSelect" @change="handleFileSelect"
class="hidden" class="hidden"
/> />
<div class="space-y-3"> <div class="space-y-3">
<Icon name="heroicons:photo" class="w-12 h-12 text-gray-400 mx-auto" /> <Icon name="heroicons:photo" class="w-12 h-12 text-gray-400 mx-auto" />
<div> <div>
<p class="text-gray-600"> <p class="text-gray-600">
<button <button
type="button" type="button"
@click="$refs.fileInput.click()" @click="$refs.fileInput.click()"
class="text-blue-600 hover:text-blue-500 font-medium" class="text-blue-600 hover:text-blue-500 font-medium"
@ -62,7 +62,7 @@
:value="modelValue.alt || ''" :value="modelValue.alt || ''"
@input="updateAltText($event.target.value)" @input="updateAltText($event.target.value)"
placeholder="Describe this image..." 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> </div>
@ -73,7 +73,7 @@
<span class="text-gray-600">{{ uploadProgress }}%</span> <span class="text-gray-600">{{ uploadProgress }}%</span>
</div> </div>
<div class="w-full bg-gray-200 rounded-full h-2"> <div class="w-full bg-gray-200 rounded-full h-2">
<div <div
class="bg-blue-600 h-2 rounded-full transition-all duration-300" class="bg-blue-600 h-2 rounded-full transition-all duration-300"
:style="`width: ${uploadProgress}%`" :style="`width: ${uploadProgress}%`"
/> />
@ -91,111 +91,113 @@
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object, type: Object,
default: () => null default: () => null,
} },
}) });
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(["update:modelValue"]);
const isDragging = ref(false) const isDragging = ref(false);
const isUploading = ref(false) const isUploading = ref(false);
const uploadProgress = ref(0) const uploadProgress = ref(0);
const errorMessage = ref('') const errorMessage = ref("");
const fileInput = ref() const fileInput = ref();
// Transform image URL for preview (smaller size) // Transform image URL for preview (smaller size)
const transformedImageUrl = computed(() => { 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 we have the direct URL, use it
if (props.modelValue?.url) { if (props.modelValue?.url) {
console.log('Using direct URL:', props.modelValue.url) console.log("Using direct URL:", props.modelValue.url);
return props.modelValue.url return props.modelValue.url;
} }
// Otherwise try to construct from publicId // Otherwise try to construct from publicId
if (props.modelValue?.publicId) { if (props.modelValue?.publicId) {
const config = useRuntimeConfig() 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}` 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) console.log("Constructed URL:", constructedUrl);
return constructedUrl return constructedUrl;
} }
console.log('No URL or publicId found') console.log("No URL or publicId found");
return '' return "";
}) });
const handleFileSelect = (event) => { const handleFileSelect = (event) => {
const file = event.target.files[0] const file = event.target.files[0];
if (file) { if (file) {
uploadFile(file) uploadFile(file);
} }
} };
const handleDrop = (event) => { const handleDrop = (event) => {
isDragging.value = false isDragging.value = false;
const files = event.dataTransfer.files const files = event.dataTransfer.files;
if (files.length > 0) { if (files.length > 0) {
uploadFile(files[0]) uploadFile(files[0]);
} }
} };
const uploadFile = async (file) => { const uploadFile = async (file) => {
// Validate file // Validate file
if (!file.type.startsWith('image/')) { if (!file.type.startsWith("image/")) {
errorMessage.value = 'Please select an image file' errorMessage.value = "Please select an image file";
return return;
} }
if (file.size > 10 * 1024 * 1024) { // 10MB if (file.size > 10 * 1024 * 1024) {
errorMessage.value = 'File size must be less than 10MB' // 10MB
return errorMessage.value = "File size must be less than 10MB";
return;
} }
errorMessage.value = '' errorMessage.value = "";
isUploading.value = true isUploading.value = true;
uploadProgress.value = 0 uploadProgress.value = 0;
try { try {
// Create form data for upload // Create form data for upload
const formData = new FormData() const formData = new FormData();
formData.append('file', file) formData.append("file", file);
// Upload to Cloudinary // Upload to Cloudinary
const response = await $fetch(`/api/upload/image`, { const response = await $fetch(`/api/upload/image`, {
method: 'POST', method: "POST",
body: formData, body: formData,
onUploadProgress: (progress) => { 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 // Update the model value
emit('update:modelValue', { emit("update:modelValue", {
url: response.secure_url, url: response.secure_url,
publicId: response.public_id, publicId: response.public_id,
alt: '' alt: "",
}) });
} catch (error) { } catch (error) {
console.error('Upload failed:', error) console.error("Upload failed:", error);
errorMessage.value = 'Upload failed. Please try again.' errorMessage.value = "Upload failed. Please try again.";
} finally { } finally {
isUploading.value = false isUploading.value = false;
uploadProgress.value = 0 uploadProgress.value = 0;
} }
} };
const removeImage = () => { const removeImage = () => {
emit('update:modelValue', null) emit("update:modelValue", null);
} };
const updateAltText = (altText) => { const updateAltText = (altText) => {
emit('update:modelValue', { emit("update:modelValue", {
...props.modelValue, ...props.modelValue,
alt: altText alt: altText,
}) });
} };
</script> </script>

View file

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

View file

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

View file

@ -12,7 +12,7 @@
/> />
<div <div
v-else 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() || "?" }} {{ update.author?.name?.charAt(0)?.toUpperCase() || "?" }}
</div> </div>
@ -23,30 +23,30 @@
<!-- Header --> <!-- Header -->
<div class="flex items-start justify-between gap-4 mb-2"> <div class="flex items-start justify-between gap-4 mb-2">
<div> <div>
<h3 class="font-semibold text-stone-100"> <h3 class="font-semibold text-ghost-100">
<NuxtLink <NuxtLink
v-if="update.author?._id" v-if="update.author?._id"
:to="`/updates/user/${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 }} {{ update.author.name }}
</NuxtLink> </NuxtLink>
<span v-else>Unknown Member</span> <span v-else>Unknown Member</span>
</h3> </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"> <time :datetime="update.createdAt">
{{ formatDate(update.createdAt) }} {{ formatDate(update.createdAt) }}
</time> </time>
<span v-if="isEdited" class="text-stone-500">(edited)</span> <span v-if="isEdited" class="text-ghost-500">(edited)</span>
<span <span
v-if="update.privacy === 'private'" 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 Private
</span> </span>
<span <span
v-if="update.privacy === 'public'" 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 Public
</span> </span>
@ -73,12 +73,12 @@
</div> </div>
<!-- Content --> <!-- 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"> <template v-if="showPreview && update.content.length > 300">
{{ update.content.substring(0, 300) }}... {{ update.content.substring(0, 300) }}...
<NuxtLink <NuxtLink
:to="`/updates/${update._id}`" :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 Read more
</NuxtLink> </NuxtLink>
@ -100,14 +100,14 @@
</div> </div>
<!-- Footer actions --> <!-- 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 <NuxtLink
:to="`/updates/${update._id}`" :to="`/updates/${update._id}`"
class="hover:text-stone-300 transition-colors" class="hover:text-ghost-300 transition-colors"
> >
View full update View full update
</NuxtLink> </NuxtLink>
<span v-if="update.commentsEnabled" class="text-stone-500"> <span v-if="update.commentsEnabled" class="text-ghost-500">
Comments (coming soon) Comments (coming soon)
</span> </span>
</div> </div>

View file

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

View file

@ -1,5 +1,5 @@
<template> <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 --> <!-- Background image at top - full page width -->
<div <div
class="absolute inset-x-0 pointer-events-none z-0" 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 --> <!-- Main Content Column - Left -->
<div class="flex-1 overflow-y-auto relative z-[5]"> <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 /> <slot />
</div> </div>
<AppFooter /> <AppFooter />
</div> </div>
<!-- Navigation Column - Right --> <!-- Desktop Navigation Column - Right -->
<AppNavigation class="relative z-20" /> <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> </div>
</template> </template>
<script setup>
const isMobileMenuOpen = ref(false);
</script>

View file

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

View file

@ -4,13 +4,13 @@
<section class="mb-24"> <section class="mb-24">
<div class="relative"> <div class="relative">
<h1 <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 Get in Touch
</h1> </h1>
<div class="mb-16"> <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 /> We'd be happy to answer any questions<br />
you might have about Ghost Guild you might have about Ghost Guild
</p> </p>
@ -21,7 +21,7 @@
<!-- Contact Form --> <!-- Contact Form -->
<section class="mb-32 relative"> <section class="mb-32 relative">
<div class="mb-12"> <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) Send us a message (or email hello@ghostguild.org)
</h2> </h2>
</div> </div>
@ -79,7 +79,7 @@
:loading="isSubmitting" :loading="isSubmitting"
:disabled="!isFormValid" :disabled="!isFormValid"
size="xl" 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 Send Message
</UButton> </UButton>
@ -98,7 +98,7 @@
</div> </div>
<div v-if="error" class="mt-6 p-4 border border-ghost-700 bg-ghost-900"> <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 }} {{ error }}
</p> </p>
</div> </div>

View file

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

View file

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

View file

@ -5,14 +5,14 @@
<div class="relative"> <div class="relative">
<!-- Large artistic title --> <!-- Large artistic title -->
<h1 <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 Become a Ghostie
</h1> </h1>
<!-- Floating subtitle --> <!-- Floating subtitle -->
<div class="mb-16"> <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 /> A community for creatives and game devs<br />
exploring cooperative models exploring cooperative models
</p> </p>
@ -33,7 +33,7 @@
<div> <div>
<NuxtLink <NuxtLink
to="/join" 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 Join Us Today
</NuxtLink> </NuxtLink>
@ -55,13 +55,13 @@
> >
<!-- Content --> <!-- Content -->
<div class="flex-1 max-w-lg"> <div class="flex-1 max-w-lg">
<h3 class="text-xl text-stone-100 mb-3">{{ circle.label }}</h3> <h3 class="text-xl text-ghost-100 mb-3">{{ circle.label }}</h3>
<p class="text-stone-200 text-sm leading-relaxed mb-4"> <p class="text-ghost-200 text-sm leading-relaxed mb-4">
{{ circle.description }} {{ circle.description }}
</p> </p>
<!-- Features as inline text --> <!-- 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"> <span v-for="(feature, i) in circle.features" :key="feature">
{{ feature {{ feature
}}<span v-if="i < circle.features.length - 1"> </span> }}<span v-if="i < circle.features.length - 1"> </span>
@ -77,7 +77,7 @@
<!-- Why Join? - Diagonal Layout --> <!-- Why Join? - Diagonal Layout -->
<section class="mb-32 relative"> <section class="mb-32 relative">
<div class="transform -rotate-1"> <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>
<div class="ml-12 relative"> <div class="ml-12 relative">
@ -86,11 +86,11 @@
/> />
<div class="max-w-2xl"> <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. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p> </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 Sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua.<br /> aliqua.<br />
Ut enim ad minim veniam, quis nostrud exercitation. Ut enim ad minim veniam, quis nostrud exercitation.
@ -98,7 +98,7 @@
</div> </div>
<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> </div>

View file

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

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- Page Header --> <!-- Page Header -->
<PageHeader <PageHeader
title="Login" title="Login"
subtitle="Welcome back! Sign in to access your Ghost Guild account and connect with the cooperative community." subtitle="Welcome back! Sign in to access your Ghost Guild account and connect with the cooperative community."
theme="blue" theme="blue"
@ -9,18 +9,20 @@
/> />
<!-- Login Form --> <!-- Login Form -->
<section class="py-20 bg-white dark:bg-gray-900"> <section class="py-20 bg-[--ui-bg]">
<UContainer class="max-w-md"> <UContainer class="max-w-md">
<div class="text-center mb-12"> <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 Passwordless Login
</h2> </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 Enter your email to receive a secure login link
</p> </p>
</div> </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"> <UForm :state="loginForm" class="space-y-6" @submit="handleLogin">
<!-- Email Field --> <!-- Email Field -->
<UFormField label="Email Address" name="email" required> <UFormField label="Email Address" name="email" required>
@ -34,7 +36,7 @@
</UFormField> </UFormField>
<!-- Passwordless Info --> <!-- 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="flex items-start gap-3">
<div class="space-y-1 flex-shrink-0 mt-1"> <div class="space-y-1 flex-shrink-0 mt-1">
<div class="w-2 h-2 bg-blue-500 rounded-full" /> <div class="w-2 h-2 bg-blue-500 rounded-full" />
@ -46,8 +48,9 @@
<div class="h-1 bg-blue-200 rounded-full w-1/2" /> <div class="h-1 bg-blue-200 rounded-full w-1/2" />
</div> </div>
</div> </div>
<p class="text-blue-700 dark:text-blue-300 text-sm mt-3"> <p class="text-primary-700 text-sm mt-3">
We'll send you a secure login link via email. No password needed! We'll send you a secure login link via email. No password
needed!
</p> </p>
</div> </div>
@ -66,23 +69,31 @@
</UForm> </UForm>
<!-- Success/Error Messages --> <!-- 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"> <div
<p class="text-green-700 dark:text-green-300 text-center"> v-if="loginSuccess"
Magic link sent! Check your email and click the link to sign in. 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> </p>
</div> </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"> <div
<p class="text-red-700 dark:text-red-300 text-center"> v-if="loginError"
{{ loginError }} class="mt-6 p-4 bg-red-50 rounded-lg border border-red-200"
</p> >
<p class="text-red-700 text-center"> {{ loginError }}</p>
</div> </div>
<!-- Sign Up Link --> <!-- Sign Up Link -->
<div class="mt-6 text-center"> <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? 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 Join Ghost Guild
</NuxtLink> </NuxtLink>
</p> </p>
@ -92,19 +103,25 @@
</section> </section>
<!-- Forgot Password --> <!-- 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"> <UContainer class="max-w-md">
<div class="text-center mb-12"> <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 Forgot Password
</h2> </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 Enter your email to receive a password reset link
</p> </p>
</div> </div>
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800"> <div
<UForm :state="forgotPasswordForm" class="space-y-6" @submit="handleForgotPassword"> 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 --> <!-- Email Field -->
<UFormField label="Email Address" name="email" required> <UFormField label="Email Address" name="email" required>
<UInput <UInput
@ -117,7 +134,7 @@
</UFormField> </UFormField>
<!-- Reset Instructions --> <!-- 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="flex items-start gap-3">
<div class="space-y-1 flex-shrink-0 mt-1"> <div class="space-y-1 flex-shrink-0 mt-1">
<div class="w-2 h-2 bg-blue-500 rounded-full" /> <div class="w-2 h-2 bg-blue-500 rounded-full" />
@ -129,8 +146,9 @@
<div class="h-1 bg-blue-200 rounded-full w-1/2" /> <div class="h-1 bg-blue-200 rounded-full w-1/2" />
</div> </div>
</div> </div>
<p class="text-blue-700 dark:text-blue-300 text-sm mt-3"> <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. We'll send you a secure link to reset your password. Check your
email inbox and spam folder.
</p> </p>
</div> </div>
@ -150,41 +168,44 @@
</UForm> </UForm>
<!-- Success/Error Messages --> <!-- 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"> <div
<p class="text-green-700 dark:text-green-300 text-center"> 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. Password reset link sent! Check your email.
</p> </p>
</div> </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"> <div
<p class="text-red-700 dark:text-red-300 text-center"> v-if="resetError"
{{ resetError }} class="mt-6 p-4 bg-red-50 rounded-lg border border-red-200"
</p> >
<p class="text-red-700 text-center"> {{ resetError }}</p>
</div> </div>
</div> </div>
</UContainer> </UContainer>
</section> </section>
<!-- Sign In CTA --> <!-- Sign In CTA -->
<section class="py-20 bg-white dark:bg-gray-900"> <section class="py-20 bg-[--ui-bg]">
<UContainer> <UContainer>
<div class="text-center max-w-2xl mx-auto"> <div class="text-center max-w-2xl 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">Sign In</h2>
Sign In
</h2>
<div class="space-y-4 mb-8"> <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-500 rounded-full w-64 mx-auto" />
<div class="h-2 bg-blue-300 rounded-full w-48 mx-auto" /> <div class="h-2 bg-blue-300 rounded-full w-48 mx-auto" />
</div> </div>
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8"> <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? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ready to
access your account and connect with the community?
</p> </p>
<UButton <UButton
@click="scrollToLoginForm" @click="scrollToLoginForm"
size="xl" size="xl"
color="primary" color="primary"
class="px-12" class="px-12"
> >
@ -195,13 +216,13 @@
</section> </section>
<!-- Access Your Dashboard --> <!-- Access Your Dashboard -->
<section class="py-20 bg-blue-50 dark:bg-blue-900/20"> <section class="py-20 bg-primary-50">
<UContainer> <UContainer>
<div class="text-center max-w-3xl mx-auto"> <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 Access Your Dashboard
</h2> </h2>
<div class="space-y-3 mb-8"> <div class="space-y-3 mb-8">
<div class="h-2 bg-blue-500 rounded-full w-full max-w-lg mx-auto" /> <div class="h-2 bg-blue-500 rounded-full w-full max-w-lg mx-auto" />
<div class="h-2 bg-blue-400 rounded-full w-full max-w-md mx-auto" /> <div class="h-2 bg-blue-400 rounded-full w-full max-w-md mx-auto" />
@ -209,53 +230,59 @@
<div class="h-2 bg-blue-200 rounded-full w-full max-w-xs mx-auto" /> <div class="h-2 bg-blue-200 rounded-full w-full max-w-xs mx-auto" />
</div> </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"> <div
<p class="text-lg text-gray-600 dark:text-gray-300 mb-6"> class="bg-[--ui-bg-elevated] rounded-2xl p-8 shadow-lg border border-primary-200 mb-8"
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Once you're logged in, you'll have access to: >
<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> </p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-left"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 text-left">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-blue-500 rounded-full" /> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-blue-400 rounded-full" /> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-blue-300 rounded-full" /> <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> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-emerald-500 rounded-full" /> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-emerald-400 rounded-full" /> <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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="w-2 h-2 bg-emerald-300 rounded-full" /> <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> </div>
</div> </div>
<div class="text-center"> <div class="text-center">
<p class="text-gray-600 dark:text-gray-300 mb-4"> <p class="text-[--ui-text-muted] mb-4">New to Ghost Guild?</p>
New to Ghost Guild? <UButton to="/join" variant="outline" size="lg" class="px-8">
</p>
<UButton
to="/join"
variant="outline"
size="lg"
class="px-8"
>
Create Your Account Create Your Account
</UButton> </UButton>
</div> </div>
@ -266,112 +293,112 @@
</template> </template>
<script setup> <script setup>
import { reactive, ref, computed } from 'vue' import { reactive, ref, computed } from "vue";
// Login form state // Login form state
const loginForm = reactive({ const loginForm = reactive({
email: '' email: "",
}) });
// Forgot password form state // Forgot password form state
const forgotPasswordForm = reactive({ const forgotPasswordForm = reactive({
email: '' email: "",
}) });
// UI state // UI state
const isLoggingIn = ref(false) const isLoggingIn = ref(false);
const isResettingPassword = ref(false) const isResettingPassword = ref(false);
const loginSuccess = ref(false) const loginSuccess = ref(false);
const loginError = ref('') const loginError = ref("");
const resetSuccess = ref(false) const resetSuccess = ref(false);
const resetError = ref('') const resetError = ref("");
// Form validation // Form validation
const isLoginFormValid = computed(() => { const isLoginFormValid = computed(() => {
return loginForm.email && loginForm.email.includes('@') return loginForm.email && loginForm.email.includes("@");
}) });
// Login handler // Login handler
const handleLogin = async () => { const handleLogin = async () => {
if (isLoggingIn.value) return if (isLoggingIn.value) return;
isLoggingIn.value = true isLoggingIn.value = true;
loginError.value = '' loginError.value = "";
loginSuccess.value = false loginSuccess.value = false;
try { try {
// Call the passwordless login API // Call the passwordless login API
const response = await $fetch('/api/auth/login', { const response = await $fetch("/api/auth/login", {
method: 'POST', method: "POST",
body: { body: {
email: loginForm.email email: loginForm.email,
} },
}) });
if (response.success) { if (response.success) {
loginSuccess.value = true loginSuccess.value = true;
loginError.value = '' loginError.value = "";
// Clear the form // Clear the form
loginForm.email = '' loginForm.email = "";
} }
} catch (err) { } catch (err) {
console.error('Login error:', err) console.error("Login error:", err);
// Handle different error types // Handle different error types
if (err.statusCode === 404) { 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) { } 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 { } else {
loginError.value = err.statusMessage || 'Something went wrong. Please try again.' loginError.value =
err.statusMessage || "Something went wrong. Please try again.";
} }
} finally { } finally {
isLoggingIn.value = false isLoggingIn.value = false;
} }
} };
// Forgot password handler // Forgot password handler
const handleForgotPassword = async () => { const handleForgotPassword = async () => {
if (isResettingPassword.value) return if (isResettingPassword.value) return;
isResettingPassword.value = true isResettingPassword.value = true;
resetError.value = '' resetError.value = "";
resetSuccess.value = false resetSuccess.value = false;
try { try {
// Simulate API call // 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 // Reset form after success
setTimeout(() => { setTimeout(() => {
forgotPasswordForm.email = '' forgotPasswordForm.email = "";
resetSuccess.value = false resetSuccess.value = false;
}, 5000) }, 5000);
} catch (err) { } catch (err) {
console.error('Password reset error:', err) console.error("Password reset error:", err);
resetError.value = 'Failed to send reset email. Please try again.' resetError.value = "Failed to send reset email. Please try again.";
} finally { } finally {
isResettingPassword.value = false isResettingPassword.value = false;
} }
} };
// Scroll functions // Scroll functions
const scrollToLoginForm = () => { const scrollToLoginForm = () => {
const formSection = document.querySelector('form') const formSection = document.querySelector("form");
if (formSection) { if (formSection) {
formSection.scrollIntoView({ behavior: 'smooth', block: 'center' }) formSection.scrollIntoView({ behavior: "smooth", block: "center" });
} }
} };
const scrollToForgotPassword = () => { const scrollToForgotPassword = () => {
const forgotSection = document.getElementById('forgot-password') const forgotSection = document.getElementById("forgot-password");
if (forgotSection) { if (forgotSection) {
forgotSection.scrollIntoView({ behavior: 'smooth', block: 'start' }) forgotSection.scrollIntoView({ behavior: "smooth", block: "start" });
} }
} };
</script> </script>

View file

@ -18,7 +18,7 @@
<div <div
class="w-8 h-8 border-4 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" class="w-8 h-8 border-4 border-whisper-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div> ></div>
<p class="text-stone-300">Loading your dashboard...</p> <p class="text-ghost-300">Loading your dashboard...</p>
</div> </div>
</div> </div>
@ -36,10 +36,10 @@
<template #header> <template #header>
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="flex-1"> <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 }}! Welcome to Ghost Guild, {{ memberData?.name }}!
</h1> </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 Your membership is active and you're part of our cooperative
community. community.
</p> </p>
@ -53,7 +53,7 @@
</div> </div>
<div v-else class="flex-shrink-0"> <div v-else class="flex-shrink-0">
<div <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() }} {{ memberData?.name?.charAt(0)?.toUpperCase() }}
</div> </div>
@ -63,13 +63,13 @@
<div class="flex flex-wrap gap-4 text-sm"> <div class="flex flex-wrap gap-4 text-sm">
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2"> <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">{{ <span class="font-medium text-whisper-300 ml-1 capitalize">{{
memberData?.circle memberData?.circle
}}</span> }}</span>
</div> </div>
<div class="bg-ghost-800 border border-ghost-600 px-4 py-2"> <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" <span class="font-medium text-whisper-300 ml-1"
>${{ memberData?.contributionTier }} CAD/month</span >${{ memberData?.contributionTier }} CAD/month</span
> >
@ -86,28 +86,16 @@
}" }"
> >
<template #header> <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 Quick Links
</h2> </h2>
</template> </template>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> <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 <UButton
disabled disabled
variant="outline" 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 block
title="Coming soon" title="Coming soon"
> >
@ -120,7 +108,7 @@
<UButton <UButton
disabled disabled
variant="outline" 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 block
title="Coming soon" title="Coming soon"
> >
@ -133,7 +121,7 @@
<UButton <UButton
disabled disabled
variant="outline" 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 block
title="Coming soon" title="Coming soon"
> >
@ -146,7 +134,7 @@
<UButton <UButton
to="/member/profile" to="/member/profile"
variant="outline" 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 block
> >
<template #leading> <template #leading>
@ -179,10 +167,10 @@
</div> </div>
</template> </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 Upcoming Events
</h3> </h3>
<p class="text-stone-300 mb-4"> <p class="text-ghost-300 mb-4">
Discover and register for community events and workshops. Discover and register for community events and workshops.
</p> </p>
@ -191,7 +179,7 @@
to="/events" to="/events"
variant="outline" variant="outline"
size="sm" 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 View Events
</UButton> </UButton>
@ -217,8 +205,8 @@
</div> </div>
</template> </template>
<h3 class="text-lg font-semibold mb-2 text-stone-100">Community</h3> <h3 class="text-lg font-semibold mb-2 text-ghost-100">Community</h3>
<p class="text-stone-300 mb-4"> <p class="text-ghost-300 mb-4">
Connect with other members in your circle and beyond. Connect with other members in your circle and beyond.
</p> </p>
@ -227,7 +215,7 @@
to="/members" to="/members"
variant="outline" variant="outline"
size="sm" 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 Browse Members
</UButton> </UButton>
@ -253,10 +241,10 @@
</div> </div>
</template> </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 Account Settings
</h3> </h3>
<p class="text-stone-300 mb-4"> <p class="text-ghost-300 mb-4">
Manage your profile and membership settings. Manage your profile and membership settings.
</p> </p>
@ -265,7 +253,7 @@
to="/member/profile#account" to="/member/profile#account"
variant="outline" variant="outline"
size="sm" 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 Manage Account
</UButton> </UButton>
@ -283,14 +271,14 @@
> >
<template #header> <template #header>
<div class="flex items-center justify-between"> <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 Your Upcoming Events
</h2> </h2>
<UButton <UButton
to="/events" to="/events"
variant="ghost" variant="ghost"
size="sm" size="sm"
class="text-stone-300 hover:text-stone-100" class="text-ghost-300 hover:text-ghost-100"
> >
Browse All Events Browse All Events
</UButton> </UButton>
@ -334,10 +322,10 @@
/> />
</div> </div>
<div class="flex-1 min-w-0"> <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 }} {{ evt.title }}
</h3> </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"> <span class="flex items-center gap-1">
<Icon name="heroicons:calendar" class="w-4 h-4" /> <Icon name="heroicons:calendar" class="w-4 h-4" />
{{ formatEventDate(evt.startDate) }} {{ formatEventDate(evt.startDate) }}
@ -351,7 +339,7 @@
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<Icon <Icon
name="heroicons:chevron-right" name="heroicons:chevron-right"
class="w-5 h-5 text-stone-500" class="w-5 h-5 text-ghost-500"
/> />
</div> </div>
</div> </div>
@ -361,108 +349,20 @@
<div v-else class="text-center py-8"> <div v-else class="text-center py-8">
<Icon <Icon
name="heroicons:calendar-days" 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 You haven't registered for any upcoming events
</p> </p>
<UButton <UButton
to="/events" to="/events"
size="sm" 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 Browse Events
</UButton> </UButton>
</div> </div>
</UCard> </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> </div>
</UContainer> </UContainer>
</div> </div>
@ -471,8 +371,6 @@
<script setup> <script setup>
const { memberData, checkMemberStatus } = useAuth(); const { memberData, checkMemberStatus } = useAuth();
const recentUpdates = ref([]);
const loadingUpdates = ref(false);
const registeredEvents = ref([]); const registeredEvents = ref([]);
const loadingEvents = ref(false); 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 // Load registered events
const loadRegisteredEvents = async () => { const loadRegisteredEvents = async () => {
console.log( console.log(
@ -575,23 +458,6 @@ const capitalize = (str) => {
.join("-"); .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 // Helper functions for event display
const getEventImageUrl = (featureImage) => { const getEventImageUrl = (featureImage) => {
if (!featureImage) return ""; if (!featureImage) return "";
@ -626,7 +492,6 @@ const formatEventTime = (dateString) => {
}; };
onMounted(() => { onMounted(() => {
loadRecentUpdates();
loadRegisteredEvents(); loadRegisteredEvents();
}); });

View file

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

View file

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

View file

@ -11,7 +11,7 @@
<UContainer class="px-4"> <UContainer class="px-4">
<!-- Search and Filters --> <!-- Search and Filters -->
<div class="mb-8 space-y-4"> <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 --> <!-- Search -->
<div class="md:col-span-2"> <div class="md:col-span-2">
<UInput <UInput
@ -26,17 +26,24 @@
<!-- Circle Filter --> <!-- Circle Filter -->
<USelect <USelect
v-model="selectedCircle" v-model="selectedCircle"
:options="circleOptions" :items="circleOptions"
placeholder="All Circles"
size="lg" size="lg"
@change="loadMembers" @update:model-value="loadMembers"
/>
<!-- Peer Support Filter -->
<USelect
v-model="peerSupportFilter"
:items="peerSupportOptions"
size="lg"
@update:model-value="loadMembers"
/> />
</div> </div>
<!-- Skills Filter --> <!-- Skills Filter -->
<div v-if="availableSkills && availableSkills.length > 0"> <div v-if="availableSkills && availableSkills.length > 0">
<div class="flex flex-wrap gap-2"> <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 >Filter by skill:</span
> >
<button <button
@ -50,7 +57,7 @@
:class=" :class="
selectedSkills.includes(skill) selectedSkills.includes(skill)
? 'bg-purple-500/20 text-purple-300 border-purple-500/50' ? '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)" @click="toggleSkill(skill)"
> >
@ -71,12 +78,55 @@
</div> </div>
</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 --> <!-- Active Filters -->
<div <div
v-if="selectedCircle || selectedSkills.length > 0" v-if="
class="flex items-center gap-2 text-sm" 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 <span
v-if="selectedCircle" 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" 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> </button>
</span> </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 <button
v-if="selectedSkills.length > 0" v-if="selectedSkills.length > 0 || selectedTopics.length > 0"
type="button" type="button"
class="text-purple-400 hover:text-purple-300" class="text-purple-400 hover:text-purple-300"
@click="clearAllFilters" @click="clearAllFilters"
@ -110,138 +173,282 @@
<div <div
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
></div> ></div>
<p class="text-stone-400">Loading members...</p> <p class="text-ghost-400">Loading members...</p>
</div> </div>
</div> </div>
<!-- Members List --> <!-- Members List -->
<div v-else-if="members.length > 0"> <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 {{ totalCount }} {{ totalCount === 1 ? "member" : "members" }} found
</div> </div>
<div class="space-y-2"> <div class="space-y-4">
<div <div
v-for="member in members" v-for="member in members"
:key="member._id" :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 --> <!-- Header Section -->
<div <div class="flex items-start gap-4 mb-4">
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" <!-- Avatar -->
> <div
<img 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"
v-if="member.avatar" >
:src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`" <img
:alt="member.name" v-if="member.avatar"
class="w-8 h-8 object-contain" :src="`/ghosties/Ghost-${member.avatar.charAt(0).toUpperCase() + member.avatar.slice(1)}.png`"
/> :alt="member.name"
<span v-else class="text-xl text-stone-600">👻</span> class="w-12 h-12 object-contain"
</div> />
<span v-else class="text-2xl text-ghost-600">👻</span>
</div>
<!-- Name and Info --> <!-- Name and Meta Info -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 flex-wrap"> <div class="flex items-baseline gap-2 flex-wrap mb-2">
<NuxtLink <NuxtLink
:to="`/updates/user/${member._id}`" :to="`/updates/user/${member._id}`"
class="font-semibold text-stone-100 hover:text-purple-300 transition-colors" 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 }} <a
</NuxtLink> v-if="member.socialLinks.mastodon"
<span v-if="member.pronouns" class="text-sm text-stone-400"> :href="member.socialLinks.mastodon"
{{ member.pronouns }} target="_blank"
</span> rel="noopener noreferrer"
<span class="text-ghost-400 hover:text-purple-400 transition-colors"
class="px-2 py-0.5 bg-purple-500/20 text-purple-300 rounded text-xs border border-purple-500/30" title="Mastodon"
> >
{{ circleLabels[member.circle] }} <svg
</span> class="w-5 h-5"
<span v-if="member.studio" class="text-sm text-stone-400"> fill="currentColor"
{{ member.studio }} viewBox="0 0 24 24"
</span> >
<span v-if="member.location" class="text-sm text-stone-500"> <path
{{ member.location }} 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"
</span> />
</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>
</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 <div
v-if="member.socialLinks && hasSocialLinks(member.socialLinks)" v-if="member.peerSupport?.enabled"
class="flex gap-3 flex-shrink-0" class="mb-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg"
> >
<a <div class="flex items-center gap-2 mb-2">
v-if="member.socialLinks.mastodon" <span class="text-purple-300 font-medium text-sm">
:href="member.socialLinks.mastodon" 💜 Offering Peer Support
target="_blank" </span>
rel="noopener noreferrer" </div>
class="text-stone-400 hover:text-purple-400 transition-colors"
title="Mastodon" <!-- 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"> <div class="flex flex-wrap gap-1">
<path <span
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" v-for="topic in member.peerSupport.topics"
/> :key="topic"
</svg> 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>
<a </div>
v-if="member.socialLinks.linkedin"
:href="member.socialLinks.linkedin" <!-- Offering and Looking For -->
target="_blank" <div
rel="noopener noreferrer" v-if="member.offering || member.lookingFor"
class="text-stone-400 hover:text-purple-400 transition-colors" class="grid grid-cols-1 md:grid-cols-2 gap-4"
title="LinkedIn" >
> <!-- Offering -->
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <div v-if="member.offering" class="space-y-2">
<path <h4 class="text-xs font-semibold text-purple-400 uppercase">
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" Offering
/> </h4>
</svg> <p
</a> v-if="member.offering.description"
<a class="text-ghost-300 text-sm"
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"
> >
<path {{ member.offering.description }}
stroke-linecap="round" </p>
stroke-linejoin="round" <div
stroke-width="2" v-if="
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" member.offering.tags && member.offering.tags.length > 0
/> "
</svg> class="flex flex-wrap gap-1"
</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"
> >
<path <span
stroke-linecap="round" v-for="tag in member.offering.tags"
stroke-linejoin="round" :key="tag"
stroke-width="2" class="px-2 py-0.5 bg-green-500/20 text-green-300 rounded text-xs border border-green-500/30"
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" >
/> {{ tag }}
</svg> </span>
</a> </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> </div>
</div> </div>
@ -254,7 +461,7 @@
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="text-stone-600" class="text-ghost-600"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -264,10 +471,10 @@
/> />
</svg> </svg>
</div> </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 No members found
</h3> </h3>
<p class="text-stone-400 mb-6"> <p class="text-ghost-400 mb-6">
Try adjusting your search or filters Try adjusting your search or filters
</p> </p>
<UButton variant="outline" @click="clearAllFilters"> <UButton variant="outline" @click="clearAllFilters">
@ -300,15 +507,19 @@ const { isAuthenticated } = useAuth();
const members = ref([]); const members = ref([]);
const totalCount = ref(0); const totalCount = ref(0);
const availableSkills = ref([]); const availableSkills = ref([]);
const availableTopics = ref([]);
const loading = ref(true); // Start with loading true const loading = ref(true); // Start with loading true
const searchQuery = ref(""); const searchQuery = ref("");
const selectedCircle = ref(""); const selectedCircle = ref("all");
const peerSupportFilter = ref("all");
const selectedSkills = ref([]); const selectedSkills = ref([]);
const selectedTopics = ref([]);
const showAllSkills = ref(false); const showAllSkills = ref(false);
const showAllTopics = ref(false);
// Circle options // Circle options
const circleOptions = [ const circleOptions = [
{ label: "All Circles", value: "" }, { label: "All Circles", value: "all" },
{ label: "Community", value: "community" }, { label: "Community", value: "community" },
{ label: "Founder", value: "founder" }, { label: "Founder", value: "founder" },
{ label: "Practitioner", value: "practitioner" }, { label: "Practitioner", value: "practitioner" },
@ -320,6 +531,12 @@ const circleLabels = {
practitioner: "Practitioner", 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 // Helper to check if member has social links
const hasSocialLinks = (links) => { const hasSocialLinks = (links) => {
if (!links) return false; if (!links) return false;
@ -333,20 +550,27 @@ const loadMembers = async () => {
try { try {
const params = {}; const params = {};
if (searchQuery.value) params.search = searchQuery.value; 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) if (selectedSkills.value.length > 0)
params.skills = selectedSkills.value.join(","); params.skills = selectedSkills.value.join(",");
if (selectedTopics.value.length > 0)
params.topics = selectedTopics.value.join(",");
const data = await $fetch("/api/members/directory", { params }); const data = await $fetch("/api/members/directory", { params });
members.value = data.members || []; members.value = data.members || [];
totalCount.value = data.totalCount || 0; totalCount.value = data.totalCount || 0;
availableSkills.value = data.filters?.availableSkills || []; availableSkills.value = data.filters?.availableSkills || [];
availableTopics.value = data.filters?.availableTopics || [];
} catch (error) { } catch (error) {
console.error("Failed to load members:", error); console.error("Failed to load members:", error);
members.value = []; members.value = [];
totalCount.value = 0; totalCount.value = 0;
availableSkills.value = []; availableSkills.value = [];
availableTopics.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -372,19 +596,53 @@ const toggleSkill = (skill) => {
loadMembers(); 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 // Clear filters
const clearCircleFilter = () => { const clearCircleFilter = () => {
selectedCircle.value = ""; selectedCircle.value = "all";
loadMembers(); };
const clearPeerSupportFilter = () => {
peerSupportFilter.value = "all";
}; };
const clearAllFilters = () => { const clearAllFilters = () => {
searchQuery.value = ""; searchQuery.value = "";
selectedCircle.value = ""; selectedCircle.value = "all";
peerSupportFilter.value = "all";
selectedSkills.value = []; selectedSkills.value = [];
selectedTopics.value = [];
loadMembers(); 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 // Load on mount
onMounted(() => { onMounted(() => {
loadMembers(); loadMembers();

View file

@ -1,338 +1,12 @@
<template> <template>
<div> <div></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>
</template> </template>
<script setup> <script setup>
const { isAuthenticated } = useAuth(); // Redirect to members directory with peer support filter
const { getSupporters } = usePeerSupport(); definePageMeta({
middleware: defineNuxtRouteMiddleware(() => {
// State return navigateTo("/members?peerSupport=true");
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.",
},
],
}); });
</script> </script>

View file

@ -25,6 +25,8 @@ export default defineEventHandler(async (event) => {
const search = query.search || ""; const search = query.search || "";
const circle = query.circle || ""; const circle = query.circle || "";
const tags = query.tags ? query.tags.split(",") : []; const tags = query.tags ? query.tags.split(",") : [];
const peerSupport = query.peerSupport || "";
const topics = query.topics ? query.topics.split(",") : [];
// Build query // Build query
const dbQuery = { const dbQuery = {
@ -37,6 +39,11 @@ export default defineEventHandler(async (event) => {
dbQuery.circle = circle; dbQuery.circle = circle;
} }
// Filter by peer support availability
if (peerSupport === "true") {
dbQuery["peerSupport.enabled"] = true;
}
// Search by name or bio // Search by name or bio
if (search) { if (search) {
dbQuery.$or = [ 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 { try {
const members = await Member.find(dbQuery) const members = await Member.find(dbQuery)
.select( .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 }) .sort({ createdAt: -1 })
.lean(); .lean();
@ -109,6 +121,12 @@ export default defineEventHandler(async (event) => {
if (isVisible("offering")) filtered.offering = member.offering; if (isVisible("offering")) filtered.offering = member.offering;
if (isVisible("lookingFor")) filtered.lookingFor = member.lookingFor; 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; return filtered;
}); });
@ -121,11 +139,19 @@ export default defineEventHandler(async (event) => {
.filter((tag, index, self) => self.indexOf(tag) === index) .filter((tag, index, self) => self.indexOf(tag) === index)
.sort(); .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 { return {
members: filteredMembers, members: filteredMembers,
totalCount: filteredMembers.length, totalCount: filteredMembers.length,
filters: { filters: {
availableTags: allTags, availableSkills: allTags,
availableTopics: allTopics,
}, },
}; };
} catch (error) { } catch (error) {

View file

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