Add light/dark mode support with CSS variables

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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