Compare commits
No commits in common. "main" and "security/asvs-remediation" have entirely different histories.
main
...
security/a
105 changed files with 2440 additions and 4093 deletions
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ghostguild",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3003
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -17,7 +17,7 @@ logs
|
|||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
/docs/
|
||||
docs/*
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<UApp>
|
||||
<SvgFilters />
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@
|
|||
* See nuxt.config.ts for configuration.
|
||||
*/
|
||||
|
||||
/* @font-face declarations commented out until .woff2 files are added to public/fonts/
|
||||
Uncomment these when Quietism font files are available:
|
||||
|
||||
@font-face {
|
||||
font-family: "Quietism";
|
||||
src: url("/fonts/Quietism-Regular.woff2") format("woff2");
|
||||
|
|
@ -44,4 +41,3 @@
|
|||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@
|
|||
--font-sans: "Inter", sans-serif;
|
||||
--font-body: "Inter", sans-serif;
|
||||
--font-mono: "Ubuntu Mono", monospace;
|
||||
--font-display: "Georgia", serif;
|
||||
--font-serif: "Georgia", serif;
|
||||
--font-display: "Quietism", serif;
|
||||
--font-serif: "Quietism", serif;
|
||||
|
||||
/* Guild - warm neutral ground (light mode: dark values for text on light bg) */
|
||||
--color-guild-50: #1a1510;
|
||||
|
|
@ -199,11 +199,11 @@
|
|||
--color-circle-founder-dark: #76432a;
|
||||
--color-circle-founder-bg: rgba(178, 104, 64, 0.1);
|
||||
|
||||
/* Practitioner - sage green (growth, wisdom, mentorship) */
|
||||
--color-circle-practitioner: #6b7c58;
|
||||
--color-circle-practitioner-light: #8fa47a;
|
||||
--color-circle-practitioner-dark: #4a5a39;
|
||||
--color-circle-practitioner-bg: rgba(107, 124, 88, 0.1);
|
||||
/* Practitioner - deep gold/ochre */
|
||||
--color-circle-practitioner: #8a7658;
|
||||
--color-circle-practitioner-light: #b7a487;
|
||||
--color-circle-practitioner-dark: #5a4d39;
|
||||
--color-circle-practitioner-bg: rgba(138, 118, 88, 0.1);
|
||||
|
||||
/* Typographic scale */
|
||||
--text-display-xl: 3.5rem;
|
||||
|
|
@ -250,23 +250,23 @@
|
|||
--halftone-size: 8px 8px;
|
||||
}
|
||||
|
||||
/* Dark mode - doubled opacity for perceptible candlelight warmth */
|
||||
/* Dark mode */
|
||||
.dark:root {
|
||||
--ambient-bg:
|
||||
radial-gradient(
|
||||
circle at 20% 80%,
|
||||
rgba(224, 184, 110, 0.06) 0%,
|
||||
rgba(224, 184, 110, 0.03) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 20%,
|
||||
rgba(218, 154, 114, 0.04) 0%,
|
||||
rgba(218, 154, 114, 0.02) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 50% 50%,
|
||||
rgba(183, 164, 135, 0.02) 0%,
|
||||
transparent 60%
|
||||
circle at 40% 40%,
|
||||
rgba(183, 164, 135, 0.01) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
|
||||
--halftone-pattern: radial-gradient(
|
||||
|
|
@ -351,7 +351,7 @@ body {
|
|||
text-shadow: 0 0 10px rgba(224, 184, 110, 0.25);
|
||||
}
|
||||
|
||||
/* Subtle noise overlay - SVG filter based, no image dependency */
|
||||
/* Subtle noise overlay */
|
||||
.ink-grain {
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -359,15 +359,19 @@ body {
|
|||
.ink-grain::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
filter: url(#grain-fine);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("/textures/grain.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 128px 128px;
|
||||
mix-blend-mode: overlay;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Paper fiber overlay - SVG filter based */
|
||||
/* Paper fiber overlay */
|
||||
.paper-texture {
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -375,27 +379,15 @@ body {
|
|||
.paper-texture::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
filter: url(#grain-paper);
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("/textures/paper.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 256px 256px;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Generic grain overlay utility */
|
||||
.texture-grain-overlay {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.texture-grain-overlay::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
filter: url(#grain-fine);
|
||||
opacity: 0.035;
|
||||
mix-blend-mode: overlay;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Irregular line border */
|
||||
|
|
@ -418,14 +410,10 @@ body {
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Warm interior feel for content areas - enclosed, lit space */
|
||||
/* Warm interior feel for content areas */
|
||||
.guild-interior {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(26, 21, 16, 0.2) 0%, transparent 200px),
|
||||
var(--ambient-bg);
|
||||
box-shadow:
|
||||
inset 0 2px 12px rgba(26, 21, 16, 0.12),
|
||||
inset 0 0 60px rgba(26, 21, 16, 0.04);
|
||||
background: var(--ambient-bg);
|
||||
box-shadow: inset 0 2px 8px rgba(26, 21, 16, 0.06);
|
||||
}
|
||||
|
||||
/* Dithered gradients */
|
||||
|
|
@ -458,112 +446,6 @@ body {
|
|||
-2px 0px;
|
||||
}
|
||||
|
||||
/* Fine dither - 2px, subtle surface texture */
|
||||
.dithered-fine {
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--color-guild-700) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--color-guild-700) 25%, transparent 25%);
|
||||
background-size: 2px 2px;
|
||||
}
|
||||
|
||||
/* Dither fade - gradient from dithered to solid */
|
||||
.dithered-fade-top {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
var(--color-guild-800) 0px,
|
||||
var(--color-guild-800) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
mask-image: linear-gradient(to bottom, black, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, black, transparent);
|
||||
}
|
||||
|
||||
.dithered-fade-bottom {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
var(--color-guild-800) 0px,
|
||||
var(--color-guild-800) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
mask-image: linear-gradient(to top, black, transparent);
|
||||
-webkit-mask-image: linear-gradient(to top, black, transparent);
|
||||
}
|
||||
|
||||
/* Circle badge utilities */
|
||||
.circle-badge-community {
|
||||
background-color: var(--color-circle-community-bg);
|
||||
color: var(--color-circle-community-light);
|
||||
border: 1px solid color-mix(in srgb, var(--color-circle-community) 40%, transparent);
|
||||
}
|
||||
|
||||
.circle-badge-founder {
|
||||
background-color: var(--color-circle-founder-bg);
|
||||
color: var(--color-circle-founder-light);
|
||||
border: 1px solid color-mix(in srgb, var(--color-circle-founder) 40%, transparent);
|
||||
}
|
||||
|
||||
.circle-badge-practitioner {
|
||||
background-color: var(--color-circle-practitioner-bg);
|
||||
color: var(--color-circle-practitioner-light);
|
||||
border: 1px solid color-mix(in srgb, var(--color-circle-practitioner) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Circle surface treatments - subtle background for circle-specific sections */
|
||||
.circle-surface-community {
|
||||
background-color: var(--color-circle-community-bg);
|
||||
}
|
||||
|
||||
.circle-surface-founder {
|
||||
background-color: var(--color-circle-founder-bg);
|
||||
}
|
||||
|
||||
.circle-surface-practitioner {
|
||||
background-color: var(--color-circle-practitioner-bg);
|
||||
}
|
||||
|
||||
/* Parchment surface for elevated content panels */
|
||||
.parchment-surface {
|
||||
background-color: var(--color-parchment-950);
|
||||
border: 1px solid var(--color-parchment-800);
|
||||
}
|
||||
|
||||
.dark .parchment-surface {
|
||||
background-color: var(--color-guild-800);
|
||||
border-color: var(--color-guild-700);
|
||||
}
|
||||
|
||||
/* UI typography utilities */
|
||||
.text-ui-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-overline);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-ui-mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
/* Illustration integration utilities */
|
||||
.guild-illustration {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 0 20px rgba(184, 135, 58, 0.08));
|
||||
}
|
||||
|
||||
.guild-illustration-adaptive {
|
||||
color: var(--color-guild-300);
|
||||
}
|
||||
|
||||
.dark .guild-illustration-adaptive {
|
||||
color: var(--color-guild-600);
|
||||
}
|
||||
|
||||
/* Prose overrides for guild wiki/long-form content */
|
||||
.prose-guild {
|
||||
max-width: 72ch;
|
||||
|
|
|
|||
|
|
@ -7,14 +7,11 @@
|
|||
]"
|
||||
>
|
||||
<!-- Logo/Brand at top (desktop only) -->
|
||||
<!-- Logo/Brand: designer will replace text with logo asset -->
|
||||
<div v-if="!isMobile" class="p-8 border-b border-guild-700 bg-guild-900">
|
||||
<div v-if="!isMobile" class="p-8 border-b border-guild-700 bg-primary-500">
|
||||
<NuxtLink to="/" class="flex flex-col items-center gap-3 group">
|
||||
<slot name="logo">
|
||||
<span class="text-display-sm font-bold text-candlelight-400 warm-text tracking-wider"
|
||||
>Ghost Guild</span
|
||||
<span class="text-xl font-bold text-white warm-text tracking-wider"
|
||||
>Ghost Guild Logo</span
|
||||
>
|
||||
</slot>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
|
|
@ -115,16 +112,16 @@
|
|||
|
||||
<div
|
||||
v-if="loginSuccess"
|
||||
class="p-3 bg-candlelight-900/20 rounded border border-candlelight-800"
|
||||
class="p-3 bg-green-900/20 rounded border border-green-800"
|
||||
>
|
||||
<p class="text-candlelight-400 text-sm">Check your email!</p>
|
||||
<p class="text-green-300 text-sm">✅ Check your email!</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loginError"
|
||||
class="p-3 bg-ember-900/20 rounded border border-ember-800"
|
||||
class="p-3 bg-red-900/20 rounded border border-red-800"
|
||||
>
|
||||
<p class="text-ember-400 text-sm">{{ loginError }}</p>
|
||||
<p class="text-red-300 text-sm">{{ loginError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,33 +4,33 @@
|
|||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
class="bg-gradient-to-br from-candlelight-500 to-candlelight-700 dark:from-candlelight-600 dark:to-candlelight-800 p-6"
|
||||
class="bg-gradient-to-br from-purple-600 to-purple-800 dark:from-purple-700 dark:to-purple-900 p-6"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Icon
|
||||
name="heroicons:ticket"
|
||||
class="w-5 h-5 text-candlelight-900 dark:text-candlelight-200"
|
||||
class="w-5 h-5 text-purple-200 dark:text-purple-300"
|
||||
/>
|
||||
<span class="text-sm font-semibold text-candlelight-900 dark:text-candlelight-200">
|
||||
<span class="text-sm font-semibold text-purple-200 dark:text-purple-300">
|
||||
Series Pass
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-white mb-1">
|
||||
{{ ticket.name }}
|
||||
</h3>
|
||||
<p v-if="ticket.description" class="text-sm text-candlelight-900 dark:text-candlelight-200">
|
||||
<p v-if="ticket.description" class="text-sm text-purple-200 dark:text-purple-300">
|
||||
{{ ticket.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right flex-shrink-0">
|
||||
<div class="text-3xl font-bold text-white text-ui-mono">
|
||||
<div class="text-3xl font-bold text-white">
|
||||
{{ formatPrice(ticket.price, ticket.currency) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="ticket.isEarlyBird"
|
||||
class="text-xs text-candlelight-900 dark:text-candlelight-200 mt-1"
|
||||
class="text-xs text-purple-200 dark:text-purple-300 mt-1"
|
||||
>
|
||||
Early Bird Price
|
||||
</div>
|
||||
|
|
@ -47,21 +47,21 @@
|
|||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-2 text-guild-300 dark:text-guild-300">
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
||||
<span>Access to all {{ totalEvents }} events in the series</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="ticket.isFree && !isMember"
|
||||
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
|
||||
>
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
||||
<span>Automatic registration for all sessions</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="memberSavings > 0"
|
||||
class="flex items-center gap-2 text-guild-300 dark:text-guild-300"
|
||||
>
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400" />
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-400" />
|
||||
<span>Save {{ formatPrice(memberSavings, ticket.currency) }} as a member</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -79,9 +79,9 @@
|
|||
class="flex items-start gap-3 p-3 bg-guild-700/50 dark:bg-guild-600/30 rounded-lg"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-candlelight-600/20 border border-candlelight-500/30 flex items-center justify-center flex-shrink-0"
|
||||
class="w-8 h-8 rounded-full bg-purple-600/20 border border-purple-500/30 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<span class="text-sm font-bold text-candlelight-300">{{ index + 1 }}</span>
|
||||
<span class="text-sm font-bold text-purple-300">{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-guild-100 dark:text-guild-100 text-sm">
|
||||
|
|
@ -104,13 +104,13 @@
|
|||
<!-- Member Benefit Callout -->
|
||||
<div
|
||||
v-if="ticket.isFree && isMember"
|
||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg mb-6"
|
||||
class="p-4 bg-green-900/20 border border-green-700/30 rounded-lg mb-6"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon name="heroicons:sparkles" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
|
||||
<Icon name="heroicons:sparkles" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<div class="font-semibold text-candlelight-300 mb-1">Member Benefit</div>
|
||||
<div class="text-sm text-candlelight-400">
|
||||
<div class="font-semibold text-green-300 mb-1">Member Benefit</div>
|
||||
<div class="text-sm text-green-400">
|
||||
This series pass is free for Ghost Guild members! Complete registration to secure your spot.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -120,13 +120,13 @@
|
|||
<!-- Public vs Member Pricing -->
|
||||
<div
|
||||
v-if="!ticket.isFree && publicPrice && ticket.type === 'member'"
|
||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg mb-6"
|
||||
class="p-4 bg-blue-900/20 border border-blue-700/30 rounded-lg mb-6"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon name="heroicons:tag" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
|
||||
<Icon name="heroicons:tag" class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-candlelight-300 mb-1">Member Savings</div>
|
||||
<div class="text-sm text-candlelight-400">
|
||||
<div class="font-semibold text-blue-300 mb-1">Member Savings</div>
|
||||
<div class="text-sm text-blue-400">
|
||||
You're saving {{ formatPrice(memberSavings, ticket.currency) }} as a member.
|
||||
Public price: {{ formatPrice(publicPrice, ticket.currency) }}
|
||||
</div>
|
||||
|
|
@ -144,13 +144,13 @@
|
|||
:name="availability.remaining > 5 ? 'heroicons:check-circle' : 'heroicons:exclamation-triangle'"
|
||||
:class="[
|
||||
'w-5 h-5',
|
||||
availability.remaining > 5 ? 'text-candlelight-400' : 'text-ember-400'
|
||||
availability.remaining > 5 ? 'text-green-400' : 'text-yellow-400'
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
'text-sm font-medium',
|
||||
availability.remaining > 5 ? 'text-candlelight-300' : 'text-ember-300'
|
||||
availability.remaining > 5 ? 'text-green-300' : 'text-yellow-300'
|
||||
]"
|
||||
>
|
||||
{{ availability.remaining }} series pass{{ availability.remaining !== 1 ? 'es' : '' }} remaining
|
||||
|
|
@ -160,12 +160,12 @@
|
|||
|
||||
<!-- Sold Out / Waitlist -->
|
||||
<div v-if="!available" class="space-y-3">
|
||||
<div class="p-4 bg-ember-900/20 border border-ember-700/30 rounded-lg">
|
||||
<div class="p-4 bg-red-900/20 border border-red-700/30 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 flex-shrink-0 mt-0.5" />
|
||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<div class="font-semibold text-ember-300 mb-1">Series Pass Sold Out</div>
|
||||
<div class="text-sm text-ember-400">
|
||||
<div class="font-semibold text-red-300 mb-1">Series Pass Sold Out</div>
|
||||
<div class="text-sm text-red-400">
|
||||
All series passes have been claimed.
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,12 +183,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Already Registered -->
|
||||
<div v-else-if="alreadyRegistered" class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg">
|
||||
<div v-else-if="alreadyRegistered" class="p-4 bg-green-900/20 border border-green-700/30 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon name="heroicons:check-badge" class="w-6 h-6 text-candlelight-400 flex-shrink-0" />
|
||||
<Icon name="heroicons:check-badge" class="w-6 h-6 text-green-400 flex-shrink-0" />
|
||||
<div>
|
||||
<div class="font-semibold text-candlelight-300 mb-1">You're Registered!</div>
|
||||
<div class="text-sm text-candlelight-400">
|
||||
<div class="font-semibold text-green-300 mb-1">You're Registered!</div>
|
||||
<div class="text-sm text-green-400">
|
||||
You have a series pass and are registered for all {{ totalEvents }} events.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<!-- Badge -->
|
||||
<div v-if="ticketInfo.isMember" class="flex-shrink-0 ml-4">
|
||||
<span
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-candlelight-900/20 text-candlelight-500 dark:bg-candlelight-900/30 dark:text-candlelight-400"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
>
|
||||
Members Only
|
||||
</span>
|
||||
|
|
@ -36,8 +36,8 @@
|
|||
<div class="mb-4">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<span
|
||||
class="text-3xl font-bold text-ui-mono"
|
||||
:class="ticketInfo.isFree ? 'text-candlelight-400' : 'text-guild-100'"
|
||||
class="text-3xl font-bold"
|
||||
:class="ticketInfo.isFree ? 'text-green-400' : 'text-guild-100'"
|
||||
>
|
||||
{{ ticketInfo.formattedPrice }}
|
||||
</span>
|
||||
|
|
@ -74,9 +74,9 @@
|
|||
<!-- Member Savings -->
|
||||
<div
|
||||
v-if="ticketInfo.publicTicket && ticketInfo.memberSavings > 0"
|
||||
class="mb-4 p-3 bg-candlelight-900/20 rounded-lg border border-candlelight-800"
|
||||
class="mb-4 p-3 bg-green-900/20 rounded-lg border border-green-800"
|
||||
>
|
||||
<p class="text-sm text-candlelight-400">
|
||||
<p class="text-sm text-green-400">
|
||||
<Icon name="heroicons:check-circle" class="w-4 h-4 inline mr-1" />
|
||||
You save {{ formatPrice(ticketInfo.memberSavings) }} as a member!
|
||||
</p>
|
||||
|
|
@ -90,14 +90,14 @@
|
|||
<div>
|
||||
<span
|
||||
v-if="alreadyRegistered"
|
||||
class="text-candlelight-400 flex items-center gap-1"
|
||||
class="text-green-400 flex items-center gap-1"
|
||||
>
|
||||
<Icon name="heroicons:check-circle-solid" class="w-4 h-4" />
|
||||
You're registered
|
||||
</span>
|
||||
<span
|
||||
v-else-if="!isAvailable"
|
||||
class="text-ember-400 flex items-center gap-1"
|
||||
class="text-red-400 flex items-center gap-1"
|
||||
>
|
||||
<Icon name="heroicons:x-circle-solid" class="w-4 h-4" />
|
||||
Sold Out
|
||||
|
|
|
|||
|
|
@ -11,26 +11,26 @@
|
|||
<!-- Error State -->
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="p-6 bg-ember-900/20 rounded-xl border border-ember-800"
|
||||
class="p-6 bg-red-900/20 rounded-xl border border-red-800"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-ember-300 mb-2">
|
||||
<h3 class="text-lg font-semibold text-red-300 mb-2">
|
||||
Unable to Load Tickets
|
||||
</h3>
|
||||
<p class="text-ember-400">{{ error }}</p>
|
||||
<p class="text-red-400">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Series Pass Required -->
|
||||
<div
|
||||
v-else-if="ticketInfo?.requiresSeriesPass"
|
||||
class="p-6 bg-candlelight-900/20 rounded-xl border border-candlelight-800"
|
||||
class="p-6 bg-purple-900/20 rounded-xl border border-purple-800"
|
||||
>
|
||||
<h3
|
||||
class="text-lg font-semibold text-candlelight-300 mb-2 flex items-center gap-2"
|
||||
class="text-lg font-semibold text-purple-300 mb-2 flex items-center gap-2"
|
||||
>
|
||||
<Icon name="heroicons:ticket" class="w-6 h-6" />
|
||||
Series Pass Required
|
||||
</h3>
|
||||
<p class="text-candlelight-400 mb-4">
|
||||
<p class="text-purple-400 mb-4">
|
||||
This event is part of
|
||||
<strong>{{ ticketInfo.series?.title }}</strong> and requires a series
|
||||
pass to attend.
|
||||
|
|
@ -51,15 +51,15 @@
|
|||
<!-- Already Registered -->
|
||||
<div
|
||||
v-else-if="ticketInfo?.alreadyRegistered"
|
||||
class="p-6 bg-candlelight-900/20 rounded-xl border border-candlelight-800"
|
||||
class="p-6 bg-green-900/20 rounded-xl border border-green-800"
|
||||
>
|
||||
<h3
|
||||
class="text-lg font-semibold text-candlelight-300 mb-2 flex items-center gap-2"
|
||||
class="text-lg font-semibold text-green-300 mb-2 flex items-center gap-2"
|
||||
>
|
||||
<Icon name="heroicons:check-circle-solid" class="w-6 h-6" />
|
||||
You're Registered!
|
||||
</h3>
|
||||
<p class="text-candlelight-400 mb-4">
|
||||
<p class="text-green-400 mb-4">
|
||||
<template v-if="ticketInfo.viaSeriesPass">
|
||||
You have access to this event via your series pass for
|
||||
<strong>{{ ticketInfo.series?.title }}</strong
|
||||
|
|
@ -136,9 +136,9 @@
|
|||
<!-- Member Benefits Notice -->
|
||||
<div
|
||||
v-if="ticketInfo.isMember && ticketInfo.isFree"
|
||||
class="p-4 bg-candlelight-900/20 rounded-lg border border-candlelight-800"
|
||||
class="p-4 bg-purple-900/20 rounded-lg border border-purple-800"
|
||||
>
|
||||
<p class="text-sm text-candlelight-300 flex items-center gap-2">
|
||||
<p class="text-sm text-purple-300 flex items-center gap-2">
|
||||
<Icon name="heroicons:sparkles" class="w-4 h-4" />
|
||||
This event is free for Ghost Guild members
|
||||
</p>
|
||||
|
|
@ -147,9 +147,9 @@
|
|||
<!-- Payment Required Notice -->
|
||||
<div
|
||||
v-if="!ticketInfo.isFree"
|
||||
class="p-4 bg-candlelight-900/20 rounded-lg border border-candlelight-800"
|
||||
class="p-4 bg-blue-900/20 rounded-lg border border-blue-800"
|
||||
>
|
||||
<p class="text-sm text-candlelight-300 flex items-center gap-2">
|
||||
<p class="text-sm text-blue-300 flex items-center gap-2">
|
||||
<Icon name="heroicons:credit-card" class="w-4 h-4" />
|
||||
Payment of {{ ticketInfo.formattedPrice }} will be processed
|
||||
securely
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
<div v-else-if="!ticketInfo.available" class="text-center py-8">
|
||||
<Icon
|
||||
name="heroicons:x-circle"
|
||||
class="w-16 h-16 text-ember-400 mx-auto mb-4"
|
||||
class="w-16 h-16 text-red-400 mx-auto mb-4"
|
||||
/>
|
||||
<h3 class="text-xl font-bold text-guild-100 mb-2">Event Sold Out</h3>
|
||||
<p class="text-guild-300">
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
<template>
|
||||
<div :class="['guild-divider', spacing]" role="separator" aria-hidden="true">
|
||||
<!-- Woodcut: irregular hand-drawn line -->
|
||||
<svg
|
||||
v-if="variant === 'woodcut'"
|
||||
viewBox="0 0 800 12"
|
||||
preserveAspectRatio="none"
|
||||
class="w-full h-3 text-guild-600"
|
||||
>
|
||||
<path
|
||||
d="M0,6 Q50,3 100,7 T200,5 T300,8 T400,4 T500,7 T600,5 T700,8 T800,6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
fill="none"
|
||||
stroke-dasharray="8,3,15,4,6,5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Dither: warm amber dithered line -->
|
||||
<div v-else-if="variant === 'dither'" class="h-px dithered-warm opacity-60" />
|
||||
|
||||
<!-- Dots: halftone dot row -->
|
||||
<div
|
||||
v-else-if="variant === 'dots'"
|
||||
class="h-2 halftone-texture opacity-30"
|
||||
/>
|
||||
|
||||
<!-- Simple: plain rule -->
|
||||
<hr v-else class="border-guild-700" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'simple',
|
||||
validator: (v) => ['simple', 'woodcut', 'dither', 'dots'].includes(v),
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const spacing = computed(() => props.compact ? 'my-4' : 'my-8')
|
||||
</script>
|
||||
|
|
@ -5,14 +5,14 @@
|
|||
<img
|
||||
:src="transformedImageUrl"
|
||||
:alt="modelValue.alt || 'Event image'"
|
||||
class="w-full h-48 object-cover rounded-lg border border-guild-700"
|
||||
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
|
||||
@click="removeImage"
|
||||
type="button"
|
||||
class="absolute top-2 right-2 p-1 bg-ember-500 text-white rounded-full hover:bg-ember-600 transition-colors"
|
||||
class="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||
</button>
|
||||
|
|
@ -21,11 +21,11 @@
|
|||
<!-- Upload Area -->
|
||||
<div
|
||||
v-if="!modelValue?.url"
|
||||
class="border-2 border-dashed border-guild-700 rounded-lg p-6 text-center hover:border-guild-600 transition-colors"
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="handleDrop"
|
||||
:class="{ 'border-candlelight-400 bg-candlelight-900/20': isDragging }"
|
||||
:class="{ 'border-blue-400 bg-blue-50': isDragging }"
|
||||
>
|
||||
<input
|
||||
ref="fileInput"
|
||||
|
|
@ -36,52 +36,52 @@
|
|||
/>
|
||||
|
||||
<div class="space-y-3">
|
||||
<Icon name="heroicons:photo" class="w-12 h-12 text-guild-400 mx-auto" />
|
||||
<Icon name="heroicons:photo" class="w-12 h-12 text-gray-400 mx-auto" />
|
||||
<div>
|
||||
<p class="text-guild-400">
|
||||
<p class="text-gray-600">
|
||||
<button
|
||||
type="button"
|
||||
@click="$refs.fileInput.click()"
|
||||
class="text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-primary-600 hover:text-primary-500 font-medium"
|
||||
>
|
||||
Click to upload
|
||||
</button>
|
||||
or drag and drop
|
||||
</p>
|
||||
<p class="text-sm text-guild-500">PNG, JPG, GIF up to 10MB</p>
|
||||
<p class="text-sm text-gray-500">PNG, JPG, GIF up to 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alt Text Input -->
|
||||
<div v-if="modelValue?.url">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Alt Text (for accessibility)
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue.alt || ''"
|
||||
@input="updateAltText($event.target.value)"
|
||||
placeholder="Describe this image..."
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-candlelight-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>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div v-if="isUploading" class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-guild-400">Uploading...</span>
|
||||
<span class="text-guild-400">{{ uploadProgress }}%</span>
|
||||
<span class="text-gray-600">Uploading...</span>
|
||||
<span class="text-gray-600">{{ uploadProgress }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-guild-800 rounded-full h-2">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-candlelight-600 h-2 rounded-full transition-all duration-300"
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="`width: ${uploadProgress}%`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="errorMessage" class="text-sm text-ember-400">
|
||||
<div v-if="errorMessage" class="text-sm text-red-600">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,24 +11,19 @@
|
|||
footer: 'bg-guild-900 border-t border-guild-700',
|
||||
title: 'text-guild-100',
|
||||
description: 'text-guild-400',
|
||||
}">
|
||||
}"
|
||||
>
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<!-- Success State -->
|
||||
<div v-if="loginSuccess" class="text-center py-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Icon
|
||||
name="heroicons:check-circle"
|
||||
class="w-10 h-10 text-candlelight-400" />
|
||||
<div class="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Icon name="heroicons:check-circle" class="w-10 h-10 text-green-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">
|
||||
Check your email
|
||||
</h3>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">Check your email</h3>
|
||||
<p class="text-guild-300">
|
||||
We've sent a magic link to
|
||||
<strong class="text-guild-100">{{ loginForm.email }}</strong
|
||||
>. Click the link to sign in.
|
||||
We've sent a magic link to <strong class="text-guild-100">{{ loginForm.email }}</strong>.
|
||||
Click the link to sign in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -42,13 +37,15 @@
|
|||
class="w-full"
|
||||
placeholder="your.email@example.com"
|
||||
:ui="{
|
||||
root: 'bg-guild-800 text-guild-100 placeholder-guild-500',
|
||||
}" />
|
||||
root: 'bg-guild-800 border-guild-600 text-guild-100 placeholder-guild-500',
|
||||
}"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="bg-guild-800 border border-guild-600 p-4 rounded-lg mb-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon name="heroicons:envelope" class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5" />
|
||||
<p class="text-sm text-guild-300">
|
||||
We'll send you a secure magic link. No password needed!
|
||||
</p>
|
||||
|
|
@ -58,8 +55,9 @@
|
|||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="loginError"
|
||||
class="mb-4 p-3 bg-ember-500/10 border border-ember-500/30 rounded-lg">
|
||||
<p class="text-ember-400 text-sm">{{ loginError }}</p>
|
||||
class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"
|
||||
>
|
||||
<p class="text-red-400 text-sm">{{ loginError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
|
|
@ -68,21 +66,23 @@
|
|||
:loading="isLoggingIn"
|
||||
:disabled="!isLoginFormValid"
|
||||
size="lg"
|
||||
class="w-full">
|
||||
class="w-full"
|
||||
>
|
||||
Send Magic Link
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<!-- Join Link -->
|
||||
<div
|
||||
v-if="!loginSuccess"
|
||||
class="text-center pt-2 border-t border-guild-700">
|
||||
<div v-if="!loginSuccess" class="text-center pt-2 border-t border-guild-700">
|
||||
<p class="text-guild-400 text-sm pt-4">
|
||||
<a
|
||||
href="https://babyghosts.fund/ghost-guild/"
|
||||
class="text-candlelight-400 hover:text-candlelight-300 font-medium">
|
||||
Don't have an account?
|
||||
<NuxtLink
|
||||
to="/join"
|
||||
class="text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
@click="close"
|
||||
>
|
||||
Join Ghost Guild
|
||||
</a>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -94,7 +94,8 @@
|
|||
v-if="loginSuccess"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
@click="resetAndClose">
|
||||
@click="resetAndClose"
|
||||
>
|
||||
Close
|
||||
</UButton>
|
||||
</div>
|
||||
|
|
@ -106,94 +107,93 @@
|
|||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "Sign in to continue",
|
||||
default: 'Sign in to continue',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: "Enter your email to receive a secure login link",
|
||||
default: 'Enter your email to receive a secure login link',
|
||||
},
|
||||
dismissible: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const emit = defineEmits(["success", "close"]);
|
||||
const emit = defineEmits(['success', 'close'])
|
||||
|
||||
const { showLoginModal, hideLoginModal } = useLoginModal();
|
||||
const { checkMemberStatus } = useAuth();
|
||||
const { showLoginModal, hideLoginModal } = useLoginModal()
|
||||
const { checkMemberStatus } = useAuth()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => showLoginModal.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
hideLoginModal();
|
||||
emit("close");
|
||||
hideLoginModal()
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const loginForm = reactive({
|
||||
email: "",
|
||||
});
|
||||
email: '',
|
||||
})
|
||||
|
||||
const isLoggingIn = ref(false);
|
||||
const loginSuccess = ref(false);
|
||||
const loginError = ref("");
|
||||
const isLoggingIn = ref(false)
|
||||
const loginSuccess = ref(false)
|
||||
const loginError = ref('')
|
||||
|
||||
const isLoginFormValid = computed(() => {
|
||||
return loginForm.email && loginForm.email.includes("@");
|
||||
});
|
||||
return loginForm.email && loginForm.email.includes('@')
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (isLoggingIn.value) return;
|
||||
if (isLoggingIn.value) return
|
||||
|
||||
isLoggingIn.value = true;
|
||||
loginError.value = "";
|
||||
isLoggingIn.value = true
|
||||
loginError.value = ''
|
||||
|
||||
try {
|
||||
const response = await $fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
const response = await $fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: loginForm.email,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
loginSuccess.value = true;
|
||||
emit("success", { email: loginForm.email });
|
||||
loginSuccess.value = true
|
||||
emit('success', { email: loginForm.email })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
console.error('Login error:', err)
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const resetAndClose = () => {
|
||||
loginForm.email = "";
|
||||
loginSuccess.value = false;
|
||||
loginError.value = "";
|
||||
close();
|
||||
};
|
||||
loginForm.email = ''
|
||||
loginSuccess.value = false
|
||||
loginError.value = ''
|
||||
close()
|
||||
}
|
||||
|
||||
// Reset form when modal opens
|
||||
watch(isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
loginForm.email = "";
|
||||
loginSuccess.value = false;
|
||||
loginError.value = "";
|
||||
loginForm.email = ''
|
||||
loginSuccess.value = false
|
||||
loginError.value = ''
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -97,11 +97,11 @@ const handleActionClick = async () => {
|
|||
// Map color names to UButton color props
|
||||
const getButtonColor = (color) => {
|
||||
const colorMap = {
|
||||
orange: "warning",
|
||||
blue: "primary",
|
||||
gray: "neutral",
|
||||
orange: "orange",
|
||||
blue: "blue",
|
||||
gray: "gray",
|
||||
};
|
||||
return colorMap[color] || "primary";
|
||||
return colorMap[color] || "blue";
|
||||
};
|
||||
|
||||
// Only show banner if status is not active
|
||||
|
|
@ -117,8 +117,8 @@ const nextAction = computed(() => getNextAction());
|
|||
const getActionButtonClass = (color) => {
|
||||
const baseClass = "hover:scale-105 active:scale-95";
|
||||
const colorClasses = {
|
||||
orange: "bg-candlelight-600 text-white hover:bg-candlelight-700",
|
||||
blue: "bg-guild-600 text-white hover:bg-guild-500",
|
||||
orange: "bg-orange-600 text-white hover:bg-orange-700",
|
||||
blue: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
gray: "bg-guild-700 text-guild-100 hover:bg-guild-600",
|
||||
};
|
||||
return `${baseClass} ${colorClasses[color] || colorClasses.blue}`;
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@
|
|||
<Icon
|
||||
v-if="isValidParse && naturalInput.trim()"
|
||||
name="heroicons:check-circle"
|
||||
class="w-5 h-5 text-candlelight-500"
|
||||
class="w-5 h-5 text-green-500"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="hasError && naturalInput.trim()"
|
||||
name="heroicons:exclamation-circle"
|
||||
class="w-5 h-5 text-ember-500"
|
||||
class="w-5 h-5 text-red-500"
|
||||
/>
|
||||
</template>
|
||||
</UInput>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<div
|
||||
v-if="parsedDate && isValidParse"
|
||||
class="text-sm text-candlelight-400 bg-candlelight-900/20 px-3 py-2 rounded-lg border border-candlelight-800"
|
||||
class="text-sm text-green-700 bg-green-50 px-3 py-2 rounded-lg border border-green-200"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="heroicons:calendar" class="w-4 h-4" />
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<div
|
||||
v-if="hasError && naturalInput.trim()"
|
||||
class="text-sm text-ember-400 bg-ember-900/20 px-3 py-2 rounded-lg border border-ember-800"
|
||||
class="text-sm text-red-700 bg-red-50 px-3 py-2 rounded-lg border border-red-200"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
<!-- Fallback datetime-local input -->
|
||||
<details class="text-sm">
|
||||
<summary class="cursor-pointer text-guild-400 hover:text-guild-100">
|
||||
<summary class="cursor-pointer text-muted hover:text-default">
|
||||
Use traditional date picker
|
||||
</summary>
|
||||
<div class="mt-2">
|
||||
|
|
|
|||
|
|
@ -1,60 +1,105 @@
|
|||
<template>
|
||||
<header
|
||||
class="relative py-16 md:py-24 bg-cover bg-center"
|
||||
style="background-image: url('/background-dither.webp')"
|
||||
style="background-image: url("/background-dither.png")"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40"></div>
|
||||
<UContainer class="relative z-10">
|
||||
<div class="text-center max-w-4xl mx-auto">
|
||||
<h1
|
||||
class="font-bold mb-6 md:mb-8 text-white"
|
||||
:class="titleSizeClass"
|
||||
class="font-bold mb-6 md:mb-8"
|
||||
:class="[titleSizeClass, titleColorClass]"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="text-lg md:text-xl leading-relaxed mb-8 text-white/90"
|
||||
class="text-lg md:text-xl leading-relaxed mb-8"
|
||||
:class="subtitleColorClass"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
<!-- Interactive Content Area (for hero sections with carousels) -->
|
||||
<!-- Interactive Content Area (for hero sections with carousels, etc.) -->
|
||||
<div
|
||||
v-if="showInteractiveArea"
|
||||
class="rounded-2xl p-6 md:p-8 mb-12 backdrop-blur-sm bg-guild-800/60 border border-guild-700 candlelight-glow halftone-texture"
|
||||
:class="[
|
||||
'rounded-2xl p-6 md:p-8 mb-12 backdrop-blur-sm',
|
||||
props.theme === 'guild'
|
||||
? 'bg-guild-800/60 border border-guild-700 candlelight-glow halftone-texture'
|
||||
: 'bg-[--ui-bg-elevated] shadow-xl border border-blue-200',
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 md:gap-4">
|
||||
<button
|
||||
class="p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0 bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow"
|
||||
:class="[
|
||||
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
|
||||
props.theme === 'guild'
|
||||
? 'bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||
]"
|
||||
@click="$emit('prev')"
|
||||
>
|
||||
<UIcon name="i-lucide-chevron-left" class="size-5 md:size-6" />
|
||||
<svg
|
||||
class="w-5 h-5 md:w-6 md:h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="text-center flex-1 min-w-0">
|
||||
<slot name="interactive-content">
|
||||
<p class="text-base md:text-lg text-guild-200">
|
||||
{{ interactiveContent }}
|
||||
<p
|
||||
:class="[
|
||||
'text-base md:text-lg',
|
||||
props.theme === 'guild'
|
||||
? 'text-guild-200'
|
||||
: 'text-[--ui-text-muted]',
|
||||
]"
|
||||
>
|
||||
{{
|
||||
interactiveContent ||
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
}}
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0 bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow"
|
||||
:class="[
|
||||
'p-2 md:p-3 rounded-full transition-all duration-300 flex-shrink-0',
|
||||
props.theme === 'guild'
|
||||
? 'bg-candlelight-600/80 text-guild-100 hover:bg-candlelight-500 candlelight-glow'
|
||||
: 'bg-blue-500 text-white hover:bg-blue-600',
|
||||
]"
|
||||
@click="$emit('next')"
|
||||
>
|
||||
<UIcon name="i-lucide-chevron-right" class="size-5 md:size-6" />
|
||||
<svg
|
||||
class="w-5 h-5 md:w-6 md:h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Illustration slot for designer-provided assets -->
|
||||
<div v-if="$slots.illustration" class="mb-8">
|
||||
<slot name="illustration" />
|
||||
</div>
|
||||
|
||||
<!-- Call to Action Button -->
|
||||
<div v-if="showCta" class="flex justify-center">
|
||||
<UButton
|
||||
|
|
@ -77,6 +122,8 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
|
|
@ -84,12 +131,18 @@ const props = defineProps({
|
|||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: "blue",
|
||||
validator: (value) =>
|
||||
["blue", "purple", "emerald", "gray", "guild"].includes(value),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'large',
|
||||
validator: (value) => ['small', 'medium', 'large', 'hero'].includes(value),
|
||||
default: "large",
|
||||
validator: (value) => ["small", "medium", "large", "hero"].includes(value),
|
||||
},
|
||||
showInteractiveArea: {
|
||||
type: Boolean,
|
||||
|
|
@ -97,7 +150,7 @@ const props = defineProps({
|
|||
},
|
||||
interactiveContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
showCta: {
|
||||
type: Boolean,
|
||||
|
|
@ -105,31 +158,55 @@ const props = defineProps({
|
|||
},
|
||||
ctaText: {
|
||||
type: String,
|
||||
default: 'Get Started',
|
||||
default: "Get Started",
|
||||
},
|
||||
ctaLink: {
|
||||
type: String,
|
||||
default: '/join',
|
||||
default: "/join",
|
||||
},
|
||||
ctaSize: {
|
||||
type: String,
|
||||
default: 'lg',
|
||||
default: "lg",
|
||||
},
|
||||
ctaColor: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
default: "primary",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
defineEmits(['prev', 'next'])
|
||||
defineEmits(["prev", "next"]);
|
||||
|
||||
const backgroundClass = computed(() => {
|
||||
const themes = {
|
||||
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",
|
||||
guild:
|
||||
"bg-gradient-to-br from-guild-900 via-guild-800 to-candlelight-900 halftone-texture",
|
||||
};
|
||||
return themes[props.theme] || themes.blue;
|
||||
});
|
||||
|
||||
const titleSizeClass = computed(() => {
|
||||
const sizes = {
|
||||
small: 'text-display-sm',
|
||||
medium: 'text-display',
|
||||
large: 'text-display-lg',
|
||||
hero: 'text-display-xl',
|
||||
}
|
||||
return sizes[props.size] || sizes.large
|
||||
})
|
||||
small: "text-2xl md:text-3xl",
|
||||
medium: "text-3xl md:text-4xl",
|
||||
large: "text-4xl md:text-5xl",
|
||||
hero: "text-5xl md:text-6xl",
|
||||
};
|
||||
return sizes[props.size] || sizes.large;
|
||||
});
|
||||
|
||||
const titleColorClass = computed(() => {
|
||||
return "text-white";
|
||||
});
|
||||
|
||||
const subtitleColorClass = computed(() => {
|
||||
return "text-white";
|
||||
});
|
||||
|
||||
const textColorClass = computed(() => {
|
||||
return "text-white";
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
>
|
||||
<path
|
||||
d="M500 70 150 175.3v217.1C150 785 500 930 500 930s350-145 350-537.6V175.2L500 70Z"
|
||||
class="fill-candlelight-500"
|
||||
class="fill-purple-500"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<!-- Sparkle effect -->
|
||||
<div
|
||||
class="absolute top-0 right-1 w-2 h-2 bg-candlelight-300 rounded-full animate-pulse"
|
||||
class="absolute top-0 right-1 w-2 h-2 bg-yellow-300 rounded-full animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -42,11 +42,11 @@
|
|||
:class="[
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border text-xs font-medium transition-all',
|
||||
variant === 'default' &&
|
||||
'bg-candlelight-900/20 text-candlelight-400 border-candlelight-500/40 hover:bg-candlelight-900/30',
|
||||
'bg-purple-500/20 text-purple-300 border-purple-500/40 hover:bg-purple-500/30',
|
||||
variant === 'subtle' &&
|
||||
'bg-candlelight-900/10 text-candlelight-500 border-candlelight-500/20',
|
||||
'bg-purple-500/10 text-purple-400 border-purple-500/20',
|
||||
variant === 'solid' &&
|
||||
'bg-candlelight-500 text-white border-candlelight-600 hover:bg-candlelight-600',
|
||||
'bg-purple-500 text-white border-purple-600 hover:bg-purple-600',
|
||||
]"
|
||||
:title="title"
|
||||
>
|
||||
|
|
@ -54,8 +54,8 @@
|
|||
name="heroicons:chat-bubble-left-right"
|
||||
:class="[
|
||||
'w-3.5 h-3.5',
|
||||
variant === 'default' && 'text-candlelight-400',
|
||||
variant === 'subtle' && 'text-candlelight-500',
|
||||
variant === 'default' && 'text-purple-300',
|
||||
variant === 'subtle' && 'text-purple-400',
|
||||
variant === 'solid' && 'text-white',
|
||||
]"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<span class="text-guild-400 text-xs font-medium"
|
||||
<span class="text-gray-700 dark:text-guild-400 text-xs font-medium"
|
||||
>{{ label }}:</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -8,8 +8,8 @@
|
|||
class="text-xs transition-colors"
|
||||
:class="
|
||||
isPrivate
|
||||
? 'text-guild-500'
|
||||
: 'text-candlelight-500 font-semibold'
|
||||
? 'text-gray-500 dark:text-guild-500'
|
||||
: 'text-blue-600 dark:text-blue-400 font-semibold'
|
||||
"
|
||||
>
|
||||
Members
|
||||
|
|
@ -24,8 +24,8 @@
|
|||
class="text-xs transition-colors"
|
||||
:class="
|
||||
isPrivate
|
||||
? 'text-candlelight-500 font-semibold'
|
||||
: 'text-guild-500'
|
||||
? 'text-blue-600 dark:text-blue-400 font-semibold'
|
||||
: 'text-gray-500 dark:text-guild-500'
|
||||
"
|
||||
>
|
||||
Private
|
||||
|
|
@ -38,19 +38,19 @@
|
|||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: 'members',
|
||||
default: "members",
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Privacy',
|
||||
default: "Privacy",
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const isPrivate = computed(() => props.modelValue === 'private')
|
||||
const isPrivate = computed(() => props.modelValue === "private");
|
||||
|
||||
const togglePrivacy = (value) => {
|
||||
emit('update:modelValue', value ? 'private' : 'members')
|
||||
}
|
||||
emit("update:modelValue", value ? "private" : "members");
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<div class="section-header" :class="[spacing]">
|
||||
<p v-if="overline" class="text-ui-label text-candlelight-500 mb-2">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<component :is="tag" :class="[sizeClass, 'text-guild-100']">
|
||||
<slot>{{ title }}</slot>
|
||||
</component>
|
||||
<p v-if="subtitle" class="mt-3 text-guild-400 text-lg leading-relaxed">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<GuildDivider v-if="divider" :variant="divider" compact class="mt-4" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
overline: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: (v) => ['xl', 'lg', 'md', 'sm'].includes(v),
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'h2',
|
||||
},
|
||||
divider: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (v) => ['', 'simple', 'woodcut', 'dither', 'dots'].includes(v),
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
const sizes = {
|
||||
xl: 'text-display-xl',
|
||||
lg: 'text-display-lg',
|
||||
md: 'text-display',
|
||||
sm: 'text-display-sm',
|
||||
}
|
||||
return sizes[props.size]
|
||||
})
|
||||
|
||||
const spacing = computed(() => props.compact ? 'mb-4' : 'mb-8')
|
||||
</script>
|
||||
|
|
@ -11,12 +11,12 @@
|
|||
<!-- Error State -->
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="p-6 bg-ember-900/20 rounded-xl border border-ember-800"
|
||||
class="p-6 bg-red-900/20 rounded-xl border border-red-800"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-ember-300 mb-2">
|
||||
<h3 class="text-lg font-semibold text-red-300 mb-2">
|
||||
Unable to Load Series Pass
|
||||
</h3>
|
||||
<p class="text-ember-400">{{ error }}</p>
|
||||
<p class="text-red-400">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
|
|
@ -93,18 +93,18 @@
|
|||
<!-- Member Benefits Notice -->
|
||||
<div
|
||||
v-if="passInfo.ticket.isFree && passInfo.memberInfo?.isMember"
|
||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg"
|
||||
class="p-4 bg-green-900/20 border border-green-700/30 rounded-lg"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<Icon
|
||||
name="heroicons:sparkles"
|
||||
class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5"
|
||||
class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-semibold text-candlelight-300 mb-1">
|
||||
<div class="font-semibold text-green-300 mb-1">
|
||||
Member Benefit
|
||||
</div>
|
||||
<div class="text-sm text-candlelight-400">
|
||||
<div class="text-sm text-green-400">
|
||||
This series pass is free for Ghost Guild members!
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<template>
|
||||
<!-- Hidden SVG filter definitions available to entire app -->
|
||||
<svg class="absolute w-0 h-0 overflow-hidden" aria-hidden="true">
|
||||
<defs>
|
||||
<!-- Fine grain noise - for ink-grain effect -->
|
||||
<filter id="grain-fine" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.65"
|
||||
numOctaves="3"
|
||||
stitchTiles="stitch"
|
||||
result="noise"
|
||||
/>
|
||||
<feColorMatrix type="saturate" values="0" in="noise" result="mono" />
|
||||
</filter>
|
||||
|
||||
<!-- Paper fiber texture - for paper-texture effect -->
|
||||
<filter id="grain-paper" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence
|
||||
type="fractalNoise"
|
||||
baseFrequency="0.04"
|
||||
numOctaves="5"
|
||||
stitchTiles="stitch"
|
||||
result="noise"
|
||||
/>
|
||||
<feColorMatrix type="saturate" values="0" in="noise" result="mono" />
|
||||
</filter>
|
||||
|
||||
<!-- Coarse grain - for workshop/craft surface textures -->
|
||||
<filter id="grain-coarse" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence
|
||||
type="turbulence"
|
||||
baseFrequency="0.3"
|
||||
numOctaves="2"
|
||||
stitchTiles="stitch"
|
||||
result="noise"
|
||||
/>
|
||||
<feColorMatrix type="saturate" values="0" in="noise" result="mono" />
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -93,9 +93,9 @@
|
|||
<!-- Error Message -->
|
||||
<div
|
||||
v-if="error"
|
||||
class="bg-ember-900/20 border border-ember-400/30 rounded-lg p-4"
|
||||
class="bg-red-500/10 border border-red-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-ember-400">{{ error }}</p>
|
||||
<p class="text-red-300">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
let pendingRequest = null;
|
||||
|
||||
export const useAuth = () => {
|
||||
const memberData = useState("auth.member", () => null);
|
||||
|
||||
|
|
@ -8,24 +6,29 @@ export const useAuth = () => {
|
|||
const isMember = computed(() => !!memberData.value);
|
||||
|
||||
const checkMemberStatus = async () => {
|
||||
if (pendingRequest) {
|
||||
return pendingRequest;
|
||||
}
|
||||
console.log("🔍 checkMemberStatus called");
|
||||
console.log(" - Current memberData:", !!memberData.value);
|
||||
|
||||
pendingRequest = (async () => {
|
||||
try {
|
||||
console.log(" - Making API call to /api/auth/member...");
|
||||
const response = await $fetch("/api/auth/member");
|
||||
console.log(" - API response received:", {
|
||||
email: response.email,
|
||||
id: response.id,
|
||||
});
|
||||
memberData.value = response;
|
||||
console.log(" - ✅ Member authenticated successfully");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
" - ❌ Failed to fetch member status:",
|
||||
error.statusCode,
|
||||
error.statusMessage,
|
||||
);
|
||||
memberData.value = null;
|
||||
console.log(" - Cleared memberData");
|
||||
return false;
|
||||
} finally {
|
||||
pendingRequest = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return pendingRequest;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-guild-950">
|
||||
<div class="min-h-screen bg-muted">
|
||||
<!-- Admin Navigation -->
|
||||
<nav class="bg-guild-900 border-b border-guild-700 shadow-sm">
|
||||
<nav class="bg-elevated border-b border-default shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center py-4">
|
||||
<div class="flex items-center gap-8">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="text-xl font-bold font-serif warm-text text-guild-100 hover:text-candlelight-400"
|
||||
class="text-xl font-bold text-highlighted hover:text-primary"
|
||||
>
|
||||
Ghost Guild
|
||||
</NuxtLink>
|
||||
|
|
@ -18,8 +18,8 @@
|
|||
:class="[
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
$route.path === '/admin'
|
||||
? 'bg-candlelight-900/30 text-candlelight-400 shadow-sm'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary shadow-sm'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -49,8 +49,8 @@
|
|||
:class="[
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
$route.path.includes('/admin/members')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400 shadow-sm'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary shadow-sm'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -74,8 +74,8 @@
|
|||
:class="[
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
$route.path.includes('/admin/events')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400 shadow-sm'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary shadow-sm'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -99,8 +99,8 @@
|
|||
:class="[
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
||||
$route.path.includes('/admin/series')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400 shadow-sm'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary shadow-sm'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -129,10 +129,10 @@
|
|||
v-click-outside="() => (showUserMenu = false)"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-guild-800 cursor-pointer transition-colors"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted cursor-pointer transition-colors"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 bg-candlelight-600 rounded-full flex items-center justify-center"
|
||||
class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-white"
|
||||
|
|
@ -148,11 +148,11 @@
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="hidden md:block text-sm font-medium text-guild-100"
|
||||
<span class="hidden md:block text-sm font-medium text-default"
|
||||
>Admin</span
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-guild-400"
|
||||
class="w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -169,14 +169,14 @@
|
|||
<!-- User Menu Dropdown -->
|
||||
<div
|
||||
v-if="showUserMenu"
|
||||
class="absolute right-0 mt-2 w-56 bg-guild-800 rounded-lg shadow-lg border border-guild-700 py-1 z-50"
|
||||
class="absolute right-0 mt-2 w-56 bg-elevated rounded-lg shadow-lg border border-default py-1 z-50"
|
||||
>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="flex items-center px-4 py-2 text-sm text-guild-100 hover:bg-guild-700"
|
||||
class="flex items-center px-4 py-2 text-sm text-default hover:bg-muted"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-3 text-guild-400"
|
||||
class="w-4 h-4 mr-3 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -198,10 +198,10 @@
|
|||
</NuxtLink>
|
||||
<NuxtLink
|
||||
to="/admin/settings"
|
||||
class="flex items-center px-4 py-2 text-sm text-guild-100 hover:bg-guild-700"
|
||||
class="flex items-center px-4 py-2 text-sm text-default hover:bg-muted"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-3 text-guild-400"
|
||||
class="w-4 h-4 mr-3 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -221,13 +221,13 @@
|
|||
</svg>
|
||||
Settings
|
||||
</NuxtLink>
|
||||
<hr class="my-1 border-guild-700" />
|
||||
<hr class="my-1 border-default" />
|
||||
<button
|
||||
@click="logout"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-ember-400 hover:bg-ember-900/20"
|
||||
class="flex items-center w-full px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-3 text-ember-400"
|
||||
class="w-4 h-4 mr-3 text-red-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -249,7 +249,7 @@
|
|||
</nav>
|
||||
|
||||
<!-- Mobile Navigation -->
|
||||
<div class="md:hidden bg-guild-900 border-b border-guild-700">
|
||||
<div class="md:hidden bg-elevated border-b border-default">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center gap-2 py-3 overflow-x-auto">
|
||||
<NuxtLink
|
||||
|
|
@ -257,8 +257,8 @@
|
|||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
|
||||
$route.path === '/admin'
|
||||
? 'bg-candlelight-900/30 text-candlelight-400'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
Dashboard
|
||||
|
|
@ -269,8 +269,8 @@
|
|||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
|
||||
$route.path.includes('/admin/members')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
Members
|
||||
|
|
@ -281,8 +281,8 @@
|
|||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
|
||||
$route.path.includes('/admin/events')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
Events
|
||||
|
|
@ -293,8 +293,8 @@
|
|||
:class="[
|
||||
'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors',
|
||||
$route.path.includes('/admin/series')
|
||||
? 'bg-candlelight-900/30 text-candlelight-400'
|
||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted hover:text-primary hover:bg-primary/5',
|
||||
]"
|
||||
>
|
||||
Series
|
||||
|
|
@ -309,10 +309,10 @@
|
|||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-guild-900 border-t border-guild-700 mt-auto">
|
||||
<footer class="bg-elevated border-t border-default mt-auto">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-8 text-center">
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-muted">
|
||||
© 2025 Ghost Guild. Admin Panel.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div
|
||||
class="absolute inset-x-0 pointer-events-none z-0"
|
||||
style="
|
||||
background-image: url("/background-dither.webp");
|
||||
background-image: url("/background-dither.png");
|
||||
background-size: 100% auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<div class="flex items-center justify-between p-4">
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="text-display-sm font-bold text-candlelight-400 warm-text tracking-wider"
|
||||
class="text-lg font-bold text-white warm-text tracking-wider"
|
||||
>
|
||||
Ghost Guild
|
||||
</NuxtLink>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<nav class="w-full px-6 md:px-8 py-4">
|
||||
<div class="max-w-6xl mx-auto flex items-center justify-between">
|
||||
<!-- Logo/Wordmark -->
|
||||
<NuxtLink to="/" class="text-display-sm font-bold text-candlelight-400 warm-text tracking-wide">
|
||||
<NuxtLink to="/" class="text-xl font-bold text-primary-400 tracking-wide">
|
||||
Ghost Guild
|
||||
</NuxtLink>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
const config = useRuntimeConfig();
|
||||
const isComingSoonMode =
|
||||
config.public.comingSoon === "true" || config.public.comingSoon === true;
|
||||
|
|
@ -8,12 +8,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Allow access to the coming-soon page, OIDC login, and admin routes
|
||||
if (
|
||||
to.path === "/coming-soon" ||
|
||||
to.path === "/auth/wiki-login" ||
|
||||
to.path.startsWith("/admin")
|
||||
) {
|
||||
// Allow access to the coming-soon page itself
|
||||
if (to.path === "/coming-soon") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<PageHeader
|
||||
title="About Ghost Guild"
|
||||
subtitle=""
|
||||
theme="blue"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
|
|
@ -128,7 +129,7 @@
|
|||
<section class="py-20 bg-[--ui-bg-elevated]">
|
||||
<UContainer>
|
||||
<div class="max-w-3xl">
|
||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-4">
|
||||
<h2 class="text-2xl font-bold text-[--ui-text] mb-4">
|
||||
Membership Circles
|
||||
</h2>
|
||||
<p class="text-lg text-[--ui-text-muted] mb-6">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<PageHeader
|
||||
title="About Our Membership Circles"
|
||||
subtitle="All members of Ghost Guild share the Baby Ghosts mission: Advancing cooperative and worker-centric labour models in the Canadian interactive digital arts sector."
|
||||
theme="blue"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
<section class="py-20 bg-[--ui-bg]">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-display font-bold text-[--ui-text] mb-6">
|
||||
<h2 class="text-3xl font-bold text-[--ui-text] mb-6">
|
||||
How membership works
|
||||
</h2>
|
||||
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
<section class="py-20 bg-[--ui-bg-elevated]">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-display font-bold text-[--ui-text] mb-4">
|
||||
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
|
||||
Find your circle
|
||||
</h2>
|
||||
<p class="text-lg text-[--ui-text-muted] mb-12">
|
||||
|
|
@ -52,8 +53,8 @@
|
|||
|
||||
<div class="space-y-12">
|
||||
<!-- Community Circle -->
|
||||
<div class="circle-surface-community rounded-xl p-6">
|
||||
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
||||
<div class="">
|
||||
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
|
||||
Community Circle
|
||||
</h3>
|
||||
|
||||
|
|
@ -87,8 +88,8 @@
|
|||
</div>
|
||||
|
||||
<!-- Founder Circle -->
|
||||
<div class="circle-surface-founder rounded-xl p-6">
|
||||
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
||||
<div class="">
|
||||
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
|
||||
Founder Circle
|
||||
</h3>
|
||||
|
||||
|
|
@ -137,8 +138,8 @@
|
|||
</div>
|
||||
|
||||
<!-- Practitioner Circle -->
|
||||
<div class="circle-surface-practitioner rounded-xl p-6">
|
||||
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
||||
<div class="">
|
||||
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
|
||||
Practitioner Circle
|
||||
</h3>
|
||||
|
||||
|
|
@ -177,7 +178,7 @@
|
|||
<section class="py-20 bg-[--ui-bg]">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-display font-bold text-[--ui-text] mb-8">
|
||||
<h2 class="text-3xl font-bold text-[--ui-text] mb-8">
|
||||
Important Notes
|
||||
</h2>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b border-default">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Admin Dashboard</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-highlighted">Admin Dashboard</h1>
|
||||
<p class="text-muted">
|
||||
Manage Ghost Guild members, events, and community operations
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -14,19 +14,19 @@
|
|||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Total Members</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Total Members</p>
|
||||
<p class="text-2xl font-bold text-blue-600">
|
||||
{{ stats.totalMembers || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -42,19 +42,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Active Events</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Active Events</p>
|
||||
<p class="text-2xl font-bold text-green-600">
|
||||
{{ stats.activeEvents || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -70,19 +70,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-purple-600">
|
||||
${{ stats.monthlyRevenue || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -98,19 +98,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-orange-600">
|
||||
{{ stats.pendingSlackInvites || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-orange-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -129,13 +129,13 @@
|
|||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
class="w-8 h-8 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -148,28 +148,28 @@
|
|||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-display-sm font-semibold text-guild-100 mb-2">
|
||||
<h3 class="text-lg font-semibold text-highlighted mb-2">
|
||||
Add New Member
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
<p class="text-muted text-sm mb-4">
|
||||
Add a new member to the Ghost Guild community
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/members-working')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Members
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
class="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -182,15 +182,15 @@
|
|||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-display-sm font-semibold text-guild-100 mb-2">
|
||||
<h3 class="text-lg font-semibold text-highlighted mb-2">
|
||||
Create Event
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
<p class="text-muted text-sm mb-4">
|
||||
Schedule a new community event or workshop
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/events-working')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Events
|
||||
</button>
|
||||
|
|
@ -200,15 +200,15 @@
|
|||
|
||||
<!-- Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="bg-elevated rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Recent Members
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/members-working')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-sm text-primary hover:text-primary"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
|
|
@ -218,20 +218,20 @@
|
|||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mx-auto"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"
|
||||
></div>
|
||||
</div>
|
||||
<div v-else-if="recentMembers.length" class="space-y-3">
|
||||
<div
|
||||
v-for="member in recentMembers"
|
||||
:key="member._id"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-guild-700"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-default"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
<p class="font-medium text-highlighted">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-muted">
|
||||
{{ member.email }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -242,27 +242,27 @@
|
|||
>
|
||||
{{ member.circle }}
|
||||
</span>
|
||||
<p class="text-xs text-guild-500 text-ui-mono">
|
||||
<p class="text-xs text-dimmed">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
<div v-else class="text-center py-6 text-dimmed">
|
||||
No recent members
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="bg-elevated rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Upcoming Events
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/events-working')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-sm text-primary hover:text-primary"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
|
|
@ -272,20 +272,20 @@
|
|||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mx-auto"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"
|
||||
></div>
|
||||
</div>
|
||||
<div v-else-if="upcomingEvents.length" class="space-y-3">
|
||||
<div
|
||||
v-for="event in upcomingEvents"
|
||||
:key="event._id"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-guild-700"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-default"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
<p class="font-medium text-highlighted">
|
||||
{{ event.title }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-muted">
|
||||
{{ formatDateTime(event.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -296,13 +296,13 @@
|
|||
>
|
||||
{{ event.eventType }}
|
||||
</span>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
{{ event.location || "Online" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
<div v-else class="text-center py-6 text-dimmed">
|
||||
No upcoming events
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -327,21 +327,21 @@ const upcomingEvents = computed(
|
|||
|
||||
const getCircleBadgeClasses = (circle) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
founder: "bg-purple-100 text-purple-800",
|
||||
practitioner: "bg-green-100 text-green-800",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
return classes[circle] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const getEventTypeBadgeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
workshop: "bg-green-100 text-green-800",
|
||||
social: "bg-purple-100 text-purple-800",
|
||||
showcase: "bg-orange-100 text-orange-800",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
return classes[type] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Event Management</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-highlighted">Event Management</h1>
|
||||
<p class="text-muted">
|
||||
Create, manage, and monitor Ghost Guild events and workshops
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -44,73 +44,73 @@
|
|||
</div>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Create Event
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Events Table -->
|
||||
<div class="bg-guild-900 rounded-lg shadow overflow-hidden">
|
||||
<div class="bg-elevated rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center">
|
||||
<div class="inline-flex items-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mr-3"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"
|
||||
></div>
|
||||
Loading events...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="p-8 text-center text-ember-400">
|
||||
<div v-else-if="error" class="p-8 text-center text-red-600">
|
||||
Error loading events: {{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full">
|
||||
<thead class="bg-guild-950">
|
||||
<thead class="bg-muted">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Title
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Start Date
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Registration
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-guild-900 divide-y divide-guild-700">
|
||||
<tbody class="bg-elevated divide-y divide-default">
|
||||
<tr
|
||||
v-for="event in filteredEvents"
|
||||
:key="event._id"
|
||||
class="hover:bg-guild-800"
|
||||
class="hover:bg-muted"
|
||||
>
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-guild-100">
|
||||
<div class="text-sm font-medium text-highlighted">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
<div class="text-sm text-guild-500">
|
||||
<div class="text-sm text-dimmed">
|
||||
{{ event.description.substring(0, 100) }}...
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
{{ event.eventType }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-400">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
|
||||
{{ formatDateTime(event.startDate) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
|
|
@ -137,31 +137,31 @@
|
|||
<span
|
||||
:class="
|
||||
event.registrationRequired
|
||||
? 'bg-candlelight-900/20 text-candlelight-400'
|
||||
: 'bg-guild-800 text-guild-300'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: 'bg-accented text-default'
|
||||
"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
>
|
||||
{{ event.registrationRequired ? "Required" : "Open" }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-100">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-default">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="editEvent(event)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary hover:text-primary"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
@click="duplicateEvent(event)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary hover:text-primary"
|
||||
>
|
||||
Duplicate
|
||||
</button>
|
||||
<button
|
||||
@click="deleteEvent(event)"
|
||||
class="text-ember-400 hover:text-ember-300"
|
||||
class="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
|
||||
<div
|
||||
v-if="!pending && !error && filteredEvents.length === 0"
|
||||
class="p-8 text-center text-guild-500"
|
||||
class="p-8 text-center text-dimmed"
|
||||
>
|
||||
No events found matching your criteria
|
||||
</div>
|
||||
|
|
@ -185,9 +185,9 @@
|
|||
v-if="showCreateModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 overflow-y-auto"
|
||||
>
|
||||
<div class="bg-guild-900 rounded-lg shadow-xl max-w-2xl w-full mx-4 my-8">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<div class="bg-elevated rounded-lg shadow-xl max-w-2xl w-full mx-4 my-8">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-semibold">
|
||||
{{ editingEvent ? "Edit Event" : "Create New Event" }}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
<form @submit.prevent="saveEvent" class="p-6 space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Event Title</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -207,7 +207,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Event Type</label
|
||||
>
|
||||
<USelect
|
||||
|
|
@ -223,7 +223,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Location</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -234,7 +234,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Start Date & Time</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -246,7 +246,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>End Date & Time</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -258,7 +258,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Max Attendees</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Registration Deadline</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -283,7 +283,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Description</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -296,7 +296,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Additional Content</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -312,17 +312,17 @@
|
|||
<input
|
||||
v-model="eventForm.isOnline"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-guild-100">Online Event</span>
|
||||
<span class="ml-2 text-sm text-default">Online Event</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input
|
||||
v-model="eventForm.registrationRequired"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="ml-2 text-sm text-guild-100"
|
||||
<span class="ml-2 text-sm text-default"
|
||||
>Registration Required</span
|
||||
>
|
||||
</label>
|
||||
|
|
@ -332,14 +332,14 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="cancelEdit"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
|
||||
class="px-4 py-2 text-muted hover:text-highlighted"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{
|
||||
creating
|
||||
|
|
@ -411,12 +411,12 @@ const filteredEvents = computed(() => {
|
|||
|
||||
const getEventTypeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
workshop: "bg-green-100 text-green-800",
|
||||
social: "bg-purple-100 text-purple-800",
|
||||
showcase: "bg-orange-100 text-orange-800",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
return classes[type] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const getEventStatus = (event) => {
|
||||
|
|
@ -432,11 +432,11 @@ const getEventStatus = (event) => {
|
|||
const getStatusClasses = (event) => {
|
||||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Ongoing: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Past: "bg-guild-800 text-guild-300",
|
||||
Upcoming: "bg-blue-100 text-blue-800",
|
||||
Ongoing: "bg-green-100 text-green-800",
|
||||
Past: "bg-gray-100 text-gray-800",
|
||||
};
|
||||
return classes[status] || "bg-guild-800 text-guild-300";
|
||||
return classes[status] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<NuxtLink to="/admin/events" class="text-guild-500 hover:text-guild-100">
|
||||
<NuxtLink to="/admin/events" class="text-dimmed hover:text-default">
|
||||
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
|
||||
</NuxtLink>
|
||||
<h1 class="text-display font-bold text-guild-100">
|
||||
<h1 class="text-2xl font-bold text-highlighted">
|
||||
{{ editingEvent ? "Edit Event" : "Create New Event" }}
|
||||
</h1>
|
||||
</div>
|
||||
<p class="text-guild-400">
|
||||
<p class="text-muted">
|
||||
Fill out the form below to create or update an event
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -22,18 +22,18 @@
|
|||
<!-- Error Summary -->
|
||||
<div
|
||||
v-if="formErrors.length > 0"
|
||||
class="mb-6 p-4 bg-ember-900/20 border border-ember-800 rounded-lg"
|
||||
class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg"
|
||||
>
|
||||
<div class="flex">
|
||||
<Icon
|
||||
name="heroicons:exclamation-circle"
|
||||
class="w-5 h-5 text-ember-400 mr-3 mt-0.5"
|
||||
class="w-5 h-5 text-red-500 mr-3 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-ember-400 mb-2">
|
||||
<h3 class="text-sm font-medium text-red-800 mb-2">
|
||||
Please fix the following errors:
|
||||
</h3>
|
||||
<ul class="text-sm text-ember-400 space-y-1">
|
||||
<ul class="text-sm text-red-700 space-y-1">
|
||||
<li v-for="error in formErrors" :key="error">• {{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -43,15 +43,15 @@
|
|||
<!-- Success Message -->
|
||||
<div
|
||||
v-if="showSuccessMessage"
|
||||
class="mb-6 p-4 bg-candlelight-900/20 border border-candlelight-800 rounded-lg"
|
||||
class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg"
|
||||
>
|
||||
<div class="flex">
|
||||
<Icon
|
||||
name="heroicons:check-circle"
|
||||
class="w-5 h-5 text-candlelight-400 mr-3 mt-0.5"
|
||||
class="w-5 h-5 text-green-500 mr-3 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-candlelight-400">
|
||||
<h3 class="text-sm font-medium text-green-800">
|
||||
{{
|
||||
editingEvent
|
||||
? "Event updated successfully!"
|
||||
|
|
@ -65,14 +65,14 @@
|
|||
<form @submit.prevent="saveEvent">
|
||||
<!-- Basic Information -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Basic Information
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Event Title <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Event Title <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="eventForm.title"
|
||||
|
|
@ -81,25 +81,25 @@
|
|||
:color="fieldErrors.title ? 'error' : undefined"
|
||||
class="w-full"
|
||||
/>
|
||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-ember-400">
|
||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-red-600">
|
||||
{{ fieldErrors.title }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Feature Image</label
|
||||
>
|
||||
<ImageUpload v-model="eventForm.featureImage" />
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
Upload a high-quality image (1200x630px recommended) to
|
||||
represent your event
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Event Description <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Event Description <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<UTextarea
|
||||
v-model="eventForm.description"
|
||||
|
|
@ -111,17 +111,17 @@
|
|||
/>
|
||||
<p
|
||||
v-if="fieldErrors.description"
|
||||
class="mt-1 text-sm text-ember-400"
|
||||
class="mt-1 text-sm text-red-600"
|
||||
>
|
||||
{{ fieldErrors.description }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
This will be displayed on the event listing and detail pages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Additional Content</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
:rows="6"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
Optional: Provide additional context, agenda items, or detailed
|
||||
requirements
|
||||
</p>
|
||||
|
|
@ -140,14 +140,14 @@
|
|||
|
||||
<!-- Event Details -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Event Details
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Event Type <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Event Type <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<USelect
|
||||
v-model="eventForm.eventType"
|
||||
|
|
@ -159,14 +159,14 @@
|
|||
]"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
Choose the category that best describes your event
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Location <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Location <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="eventForm.location"
|
||||
|
|
@ -175,50 +175,50 @@
|
|||
:color="fieldErrors.location ? 'error' : undefined"
|
||||
class="w-full"
|
||||
/>
|
||||
<p v-if="fieldErrors.location" class="mt-1 text-sm text-ember-400">
|
||||
<p v-if="fieldErrors.location" class="mt-1 text-sm text-red-600">
|
||||
{{ fieldErrors.location }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
Enter a video conference link or Slack channel (starting with #)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Start Date & Time <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Start Date & Time <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<NaturalDateInput
|
||||
v-model="eventForm.startDate"
|
||||
placeholder="e.g., 'tomorrow at 3pm', 'next Friday at 9am'"
|
||||
:required="true"
|
||||
:input-class="{
|
||||
'border-ember-700 focus:ring-ember-500': fieldErrors.startDate,
|
||||
'border-red-300 focus:ring-red-500': fieldErrors.startDate,
|
||||
}"
|
||||
/>
|
||||
<p v-if="fieldErrors.startDate" class="mt-1 text-sm text-ember-400">
|
||||
<p v-if="fieldErrors.startDate" class="mt-1 text-sm text-red-600">
|
||||
{{ fieldErrors.startDate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
End Date & Time <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
End Date & Time <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<NaturalDateInput
|
||||
v-model="eventForm.endDate"
|
||||
placeholder="e.g., 'tomorrow at 5pm', 'next Friday at 11am'"
|
||||
:required="true"
|
||||
:input-class="{
|
||||
'border-ember-700 focus:ring-ember-500': fieldErrors.endDate,
|
||||
'border-red-300 focus:ring-red-500': fieldErrors.endDate,
|
||||
}"
|
||||
/>
|
||||
<p v-if="fieldErrors.endDate" class="mt-1 text-sm text-ember-400">
|
||||
<p v-if="fieldErrors.endDate" class="mt-1 text-sm text-red-600">
|
||||
{{ fieldErrors.endDate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Max Attendees</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -228,20 +228,20 @@
|
|||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
Set a maximum number of attendees (optional)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Registration Deadline</label
|
||||
>
|
||||
<NaturalDateInput
|
||||
v-model="eventForm.registrationDeadline"
|
||||
placeholder="e.g., 'tomorrow at noon', '1 hour before event'"
|
||||
/>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-dimmed">
|
||||
When should registration close? (optional)
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -250,12 +250,12 @@
|
|||
|
||||
<!-- Target Audience -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Target Audience
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-3"
|
||||
<label class="block text-sm font-medium text-default mb-3"
|
||||
>Target Circles</label
|
||||
>
|
||||
<div class="space-y-3">
|
||||
|
|
@ -264,13 +264,13 @@
|
|||
v-model="eventForm.targetCircles"
|
||||
value="community"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Community Circle</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
New members and those exploring the community
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -280,13 +280,13 @@
|
|||
v-model="eventForm.targetCircles"
|
||||
value="founder"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Founder Circle</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Entrepreneurs and business leaders
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -296,19 +296,19 @@
|
|||
v-model="eventForm.targetCircles"
|
||||
value="practitioner"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Practitioner Circle</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Experts and professionals sharing knowledge
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-guild-500">
|
||||
<p class="mt-2 text-sm text-dimmed">
|
||||
Select which circles this event is most relevant for (leave blank
|
||||
for all circles)
|
||||
</p>
|
||||
|
|
@ -317,20 +317,20 @@
|
|||
|
||||
<!-- Ticketing -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">Ticketing</h2>
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">Ticketing</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="eventForm.tickets.enabled"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Enable Ticketing</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Allow ticket sales for this event
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -338,19 +338,19 @@
|
|||
|
||||
<div
|
||||
v-if="eventForm.tickets.enabled"
|
||||
class="ml-6 space-y-4 p-4 bg-guild-950 rounded-lg"
|
||||
class="ml-6 space-y-4 p-4 bg-muted rounded-lg"
|
||||
>
|
||||
<label class="flex items-start">
|
||||
<input
|
||||
v-model="eventForm.tickets.public.available"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Public Tickets Available</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Allow non-members to purchase tickets
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -359,7 +359,7 @@
|
|||
<div v-if="eventForm.tickets.public.available" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Ticket Name</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -370,7 +370,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Price (CAD)</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -381,14 +381,14 @@
|
|||
placeholder="0.00"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-guild-500">
|
||||
<p class="mt-1 text-xs text-dimmed">
|
||||
Set to 0 for free public events
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Ticket Description</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -401,7 +401,7 @@
|
|||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Quantity Available</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -414,7 +414,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Price (Optional)</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -429,7 +429,7 @@
|
|||
</div>
|
||||
|
||||
<div v-if="eventForm.tickets.public.earlyBirdPrice > 0">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Deadline</label
|
||||
>
|
||||
<div class="md:w-1/2">
|
||||
|
|
@ -438,15 +438,15 @@
|
|||
placeholder="e.g., '1 week before event', 'next Monday'"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-guild-500">
|
||||
<p class="mt-1 text-xs text-dimmed">
|
||||
Price increases to regular price after this date
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-candlelight-900/20 rounded-lg">
|
||||
<p class="text-sm text-candlelight-400">
|
||||
<div class="p-3 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-700">
|
||||
<strong>Note:</strong> Members always get free access to all
|
||||
events regardless of ticket settings.
|
||||
</p>
|
||||
|
|
@ -456,7 +456,7 @@
|
|||
|
||||
<!-- Series Management -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Series Management
|
||||
</h2>
|
||||
|
||||
|
|
@ -465,13 +465,13 @@
|
|||
<input
|
||||
v-model="eventForm.series.isSeriesEvent"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-earth-500 focus:ring-earth-500 mt-1"
|
||||
class="rounded border-default text-purple-600 focus:ring-purple-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Part of Event Series</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
This event is part of a multi-event series
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -479,11 +479,11 @@
|
|||
|
||||
<div
|
||||
v-if="eventForm.series.isSeriesEvent"
|
||||
class="ml-6 space-y-4 p-4 bg-earth-900/20 rounded-lg border border-earth-800"
|
||||
class="ml-6 space-y-4 p-4 bg-purple-500/10 rounded-lg border border-purple-500/20"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Select Series <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Select Series <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<USelect
|
||||
|
|
@ -501,12 +501,12 @@
|
|||
/>
|
||||
<NuxtLink
|
||||
to="/admin/series/create"
|
||||
class="px-4 py-2 bg-earth-600 text-white rounded-lg hover:bg-earth-700 text-sm font-medium whitespace-nowrap"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium whitespace-nowrap"
|
||||
>
|
||||
New Series
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Select an existing series or create a new one
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -516,18 +516,18 @@
|
|||
class="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Title <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Series Title <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<UInput
|
||||
v-model="eventForm.series.title"
|
||||
placeholder="e.g., Cooperative Game Development Fundamentals"
|
||||
required
|
||||
:readonly="selectedSeriesId"
|
||||
:class="{ 'bg-guild-800': selectedSeriesId }"
|
||||
:class="{ 'bg-accented': selectedSeriesId }"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{
|
||||
selectedSeriesId
|
||||
? "From selected series"
|
||||
|
|
@ -537,8 +537,8 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Description <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-default mb-2">
|
||||
Series Description <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<UTextarea
|
||||
v-model="eventForm.series.description"
|
||||
|
|
@ -546,10 +546,10 @@
|
|||
required
|
||||
:rows="3"
|
||||
:readonly="selectedSeriesId"
|
||||
:class="{ 'bg-guild-800': selectedSeriesId }"
|
||||
:class="{ 'bg-accented': selectedSeriesId }"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{
|
||||
selectedSeriesId
|
||||
? "From selected series"
|
||||
|
|
@ -560,9 +560,9 @@
|
|||
|
||||
<div
|
||||
v-if="selectedSeriesId"
|
||||
class="p-3 bg-candlelight-900/20 rounded-lg border border-candlelight-800"
|
||||
class="p-3 bg-blue-500/10 rounded-lg border border-blue-500/20"
|
||||
>
|
||||
<p class="text-sm text-candlelight-400">
|
||||
<p class="text-sm text-blue-600 dark:text-blue-400">
|
||||
<strong>Note:</strong> This event will be added to the
|
||||
existing "{{ eventForm.series.title }}" series.
|
||||
</p>
|
||||
|
|
@ -574,7 +574,7 @@
|
|||
|
||||
<!-- Event Agenda -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Event Agenda
|
||||
</h2>
|
||||
|
||||
|
|
@ -592,7 +592,7 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="removeAgendaItem(index)"
|
||||
class="px-3 py-2 text-ember-400 hover:bg-ember-900/20 rounded-lg transition-colors"
|
||||
class="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Icon name="heroicons:trash" class="w-5 h-5" />
|
||||
</button>
|
||||
|
|
@ -601,14 +601,14 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="addAgendaItem"
|
||||
class="flex items-center gap-2 px-4 py-2 text-candlelight-400 hover:bg-candlelight-900/20 rounded-lg transition-colors font-medium"
|
||||
class="flex items-center gap-2 px-4 py-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
<Icon name="heroicons:plus" class="w-5 h-5" />
|
||||
Add Agenda Item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-guild-500">
|
||||
<p class="mt-2 text-sm text-dimmed">
|
||||
Add agenda items to help attendees know what to expect during the
|
||||
event
|
||||
</p>
|
||||
|
|
@ -616,7 +616,7 @@
|
|||
|
||||
<!-- Event Settings -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">
|
||||
<h2 class="text-lg font-semibold text-highlighted mb-4">
|
||||
Event Settings
|
||||
</h2>
|
||||
|
||||
|
|
@ -626,13 +626,13 @@
|
|||
<input
|
||||
v-model="eventForm.isOnline"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Online Event</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Event will be conducted virtually
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -642,13 +642,13 @@
|
|||
<input
|
||||
v-model="eventForm.registrationRequired"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Registration Required</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Attendees must register before attending
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -660,13 +660,13 @@
|
|||
<input
|
||||
v-model="eventForm.isVisible"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Visible on Public Calendar</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Event will appear on the public events page
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -676,13 +676,13 @@
|
|||
<input
|
||||
v-model="eventForm.isCancelled"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-ember-500 focus:ring-ember-500 mt-1"
|
||||
class="rounded border-default text-red-600 focus:ring-red-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Event Cancelled</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Mark this event as cancelled
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -693,7 +693,7 @@
|
|||
|
||||
<!-- Cancellation Message (conditional) -->
|
||||
<div v-if="eventForm.isCancelled" class="mb-8">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Cancellation Message</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -703,18 +703,18 @@
|
|||
color="error"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
This message will be displayed to users viewing the event page
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div
|
||||
class="flex justify-between items-center pt-6 border-t border-guild-700"
|
||||
class="flex justify-between items-center pt-6 border-t border-default"
|
||||
>
|
||||
<NuxtLink
|
||||
to="/admin/events"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
|
||||
class="px-4 py-2 text-muted hover:text-highlighted font-medium"
|
||||
>
|
||||
Cancel
|
||||
</NuxtLink>
|
||||
|
|
@ -725,7 +725,7 @@
|
|||
type="button"
|
||||
@click="saveAndCreateAnother"
|
||||
:disabled="creating"
|
||||
class="px-4 py-2 bg-guild-600 text-white rounded-lg hover:bg-guild-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ creating ? "Saving..." : "Save & Create Another" }}
|
||||
</button>
|
||||
|
|
@ -733,7 +733,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="px-6 py-2 bg-candlelight-600 text-white rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{
|
||||
creating
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Event Management</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-highlighted">Event Management</h1>
|
||||
<p class="text-muted">
|
||||
Create, manage, and monitor Ghost Guild events and workshops
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
</div>
|
||||
<NuxtLink
|
||||
to="/admin/events/create"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2 inline-flex items-center"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 inline-flex items-center"
|
||||
>
|
||||
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
||||
Create Event
|
||||
|
|
@ -61,65 +61,65 @@
|
|||
</div>
|
||||
|
||||
<!-- Events Table -->
|
||||
<div class="bg-guild-900 rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center text-guild-100">
|
||||
<div class="bg-elevated rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center">
|
||||
<div class="inline-flex items-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mr-3"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"
|
||||
></div>
|
||||
Loading events...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="p-8 text-center text-ember-400">
|
||||
<div v-else-if="error" class="p-8 text-center text-red-600">
|
||||
Error loading events: {{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full">
|
||||
<thead class="bg-guild-950">
|
||||
<thead class="bg-muted">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Title
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Registration
|
||||
</th>
|
||||
<th
|
||||
class="px-4 py-4 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-4 py-4 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Tickets
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-right text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-4 text-right text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-guild-900 divide-y divide-guild-700">
|
||||
<tbody class="bg-elevated divide-y divide-default">
|
||||
<tr
|
||||
v-for="event in filteredEvents"
|
||||
:key="event._id"
|
||||
class="hover:bg-guild-800"
|
||||
class="hover:bg-muted"
|
||||
>
|
||||
<!-- Title Column -->
|
||||
<td class="px-6 py-6">
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
v-if="
|
||||
event.featureImage?.url && !event.featureImage?.publicId
|
||||
"
|
||||
class="flex-shrink-0 w-12 h-12 bg-guild-800 rounded-lg overflow-hidden"
|
||||
class="flex-shrink-0 w-12 h-12 bg-accented rounded-lg overflow-hidden"
|
||||
>
|
||||
<img
|
||||
:src="event.featureImage.url"
|
||||
|
|
@ -139,26 +139,26 @@
|
|||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex-shrink-0 w-12 h-12 bg-guild-800 rounded-lg flex items-center justify-center"
|
||||
class="flex-shrink-0 w-12 h-12 bg-accented rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:calendar-days"
|
||||
class="w-6 h-6 text-guild-400"
|
||||
class="w-6 h-6 text-muted"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-semibold text-guild-100 mb-1">
|
||||
<div class="text-sm font-semibold text-highlighted mb-1">
|
||||
{{ event.title }}
|
||||
</div>
|
||||
<div class="text-sm text-guild-500 line-clamp-2">
|
||||
<div class="text-sm text-dimmed line-clamp-2">
|
||||
{{ event.description.substring(0, 100) }}...
|
||||
</div>
|
||||
<div v-if="event.series?.isSeriesEvent" class="mt-2 mb-2">
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-earth-900/20 text-earth-400 text-xs font-medium rounded-full"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 text-xs font-medium rounded-full"
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 bg-earth-800 text-earth-400 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
class="w-4 h-4 bg-purple-200 text-purple-700 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
>
|
||||
{{ event.series.position }}
|
||||
</div>
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
<div class="flex items-center space-x-4 mt-2">
|
||||
<div
|
||||
v-if="event.membersOnly"
|
||||
class="flex items-center text-xs text-earth-400"
|
||||
class="flex items-center text-xs text-purple-600"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:lock-closed"
|
||||
|
|
@ -184,15 +184,15 @@
|
|||
>
|
||||
<Icon
|
||||
name="heroicons:user-group"
|
||||
class="w-3 h-3 text-guild-400"
|
||||
class="w-3 h-3 text-muted"
|
||||
/>
|
||||
<span class="text-xs text-guild-500">{{
|
||||
<span class="text-xs text-dimmed">{{
|
||||
event.targetCircles.join(", ")
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!event.isVisible"
|
||||
class="flex items-center text-xs text-guild-500"
|
||||
class="flex items-center text-xs text-dimmed"
|
||||
>
|
||||
<Icon name="heroicons:eye-slash" class="w-3 h-3 mr-1" />
|
||||
Hidden
|
||||
|
|
@ -213,12 +213,12 @@
|
|||
</td>
|
||||
|
||||
<!-- Date Column -->
|
||||
<td class="px-4 py-6 whitespace-nowrap text-sm text-guild-400">
|
||||
<td class="px-4 py-6 whitespace-nowrap text-sm text-muted">
|
||||
<div class="space-y-1">
|
||||
<div class="font-medium">
|
||||
{{ formatDate(event.startDate) }}
|
||||
</div>
|
||||
<div class="text-xs text-guild-500">
|
||||
<div class="text-xs text-dimmed">
|
||||
{{ formatTime(event.startDate) }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
</span>
|
||||
<div
|
||||
v-if="event.isCancelled"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-ember-900/20 text-ember-400"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800"
|
||||
>
|
||||
Cancelled
|
||||
</div>
|
||||
|
|
@ -247,17 +247,17 @@
|
|||
<div class="space-y-2">
|
||||
<div
|
||||
v-if="event.registrationRequired"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-candlelight-900/20 text-candlelight-400"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||
>
|
||||
Required
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-guild-800 text-guild-300"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-accented text-default"
|
||||
>
|
||||
Optional
|
||||
</div>
|
||||
<div v-if="event.maxAttendees" class="text-xs text-guild-500">
|
||||
<div v-if="event.maxAttendees" class="text-xs text-dimmed">
|
||||
{{ event.registeredCount || 0 }} / {{ event.maxAttendees }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -270,20 +270,20 @@
|
|||
<div class="flex items-center gap-1 text-xs">
|
||||
<Icon
|
||||
name="heroicons:ticket"
|
||||
class="w-3.5 h-3.5 text-candlelight-400"
|
||||
class="w-3.5 h-3.5 text-blue-600"
|
||||
/>
|
||||
<span class="font-medium text-guild-100">Ticketing On</span>
|
||||
<span class="font-medium text-default">Ticketing On</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="event.tickets?.requiresSeriesTicket"
|
||||
class="text-xs text-earth-400"
|
||||
class="text-xs text-purple-600"
|
||||
>
|
||||
Series Pass Required
|
||||
</div>
|
||||
<div v-else class="space-y-0.5">
|
||||
<div
|
||||
v-if="event.tickets.member?.available"
|
||||
class="text-xs text-guild-500"
|
||||
class="text-xs text-dimmed"
|
||||
>
|
||||
Member:
|
||||
{{
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="event.tickets.public?.available"
|
||||
class="text-xs text-guild-500"
|
||||
class="text-xs text-dimmed"
|
||||
>
|
||||
Public: ${{ event.tickets.public.price || 0 }}
|
||||
<span v-if="event.tickets.public.quantity" class="ml-1">
|
||||
|
|
@ -305,7 +305,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-guild-500">No tickets</div>
|
||||
<div v-else class="text-xs text-dimmed">No tickets</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
|
@ -314,28 +314,28 @@
|
|||
<div class="flex items-center justify-end space-x-2">
|
||||
<NuxtLink
|
||||
:to="`/events/${event.slug || String(event._id)}`"
|
||||
class="p-2 text-guild-500 hover:text-guild-100 hover:bg-guild-800 rounded-full transition-colors"
|
||||
class="p-2 text-dimmed hover:text-default hover:bg-accented rounded-full transition-colors"
|
||||
title="View Event"
|
||||
>
|
||||
<Icon name="heroicons:eye" class="w-4 h-4" />
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="editEvent(event)"
|
||||
class="p-2 text-candlelight-400 hover:text-candlelight-300 hover:bg-candlelight-900/20 rounded-full transition-colors"
|
||||
class="p-2 text-primary hover:text-primary hover:bg-primary/10 rounded-full transition-colors"
|
||||
title="Edit Event"
|
||||
>
|
||||
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="duplicateEvent(event)"
|
||||
class="p-2 text-candlelight-400 hover:text-candlelight-300 hover:bg-candlelight-900/20 rounded-full transition-colors"
|
||||
class="p-2 text-primary hover:text-primary hover:bg-primary/10 rounded-full transition-colors"
|
||||
title="Duplicate Event"
|
||||
>
|
||||
<Icon name="heroicons:document-duplicate" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="deleteEvent(event)"
|
||||
class="p-2 text-ember-400 hover:text-ember-300 hover:bg-ember-900/20 rounded-full transition-colors"
|
||||
class="p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-full transition-colors"
|
||||
title="Delete Event"
|
||||
>
|
||||
<Icon name="heroicons:trash" class="w-4 h-4" />
|
||||
|
|
@ -348,7 +348,7 @@
|
|||
|
||||
<div
|
||||
v-if="!pending && !error && filteredEvents.length === 0"
|
||||
class="p-8 text-center text-guild-500"
|
||||
class="p-8 text-center text-dimmed"
|
||||
>
|
||||
No events found matching your criteria
|
||||
</div>
|
||||
|
|
@ -402,12 +402,12 @@ const filteredEvents = computed(() => {
|
|||
|
||||
const getEventTypeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
workshop: "bg-green-100 text-green-800",
|
||||
social: "bg-purple-100 text-purple-800",
|
||||
showcase: "bg-orange-100 text-orange-800",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
return classes[type] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const getEventStatus = (event) => {
|
||||
|
|
@ -423,11 +423,11 @@ const getEventStatus = (event) => {
|
|||
const getStatusClasses = (event) => {
|
||||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Ongoing: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Past: "bg-guild-800 text-guild-300",
|
||||
Upcoming: "bg-blue-100 text-blue-800",
|
||||
Ongoing: "bg-green-100 text-green-800",
|
||||
Past: "bg-gray-100 text-gray-800",
|
||||
};
|
||||
return classes[status] || "bg-guild-800 text-guild-300";
|
||||
return classes[status] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const formatDateTime = (dateString) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-white border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Admin Interface - Working Version</h1>
|
||||
<p class="text-guild-400">Fully functional admin interface without Nuxt UI component issues</p>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Admin Interface - Working Version</h1>
|
||||
<p class="text-gray-600">Fully functional admin interface without Nuxt UI component issues</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -12,56 +12,56 @@
|
|||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Navigation Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<NuxtLink to="/admin/dashboard" class="block bg-guild-900 rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-candlelight-800">
|
||||
<div class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-candlelight-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<NuxtLink to="/admin/dashboard" class="block bg-white rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-blue-200">
|
||||
<div class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">Dashboard</h3>
|
||||
<p class="text-guild-400 text-sm">Overview & statistics</p>
|
||||
<h3 class="text-lg font-semibold mb-2">Dashboard</h3>
|
||||
<p class="text-gray-600 text-sm">Overview & statistics</p>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/admin/members-working" class="block bg-guild-900 rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-candlelight-800">
|
||||
<div class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-candlelight-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<NuxtLink to="/admin/members-working" class="block bg-white rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-green-200">
|
||||
<div class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-.5a4 4 0 110 5.292M4 19.5a4 4 0 010-5.292"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">Members</h3>
|
||||
<p class="text-guild-400 text-sm">Manage members</p>
|
||||
<h3 class="text-lg font-semibold mb-2">Members</h3>
|
||||
<p class="text-gray-600 text-sm">Manage members</p>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink to="/admin/events-working" class="block bg-guild-900 rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-earth-800">
|
||||
<div class="w-16 h-16 bg-earth-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-earth-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<NuxtLink to="/admin/events-working" class="block bg-white rounded-lg shadow hover:shadow-lg transition-shadow p-6 text-center border-2 border-transparent hover:border-purple-200">
|
||||
<div class="w-16 h-16 bg-purple-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">Events</h3>
|
||||
<p class="text-guild-400 text-sm">Manage events</p>
|
||||
<h3 class="text-lg font-semibold mb-2">Events</h3>
|
||||
<p class="text-gray-600 text-sm">Manage events</p>
|
||||
</NuxtLink>
|
||||
|
||||
<div class="bg-guild-950 rounded-lg p-6 text-center border-2 border-dashed border-guild-700">
|
||||
<div class="w-16 h-16 bg-guild-800 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-guild-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="bg-gray-100 rounded-lg p-6 text-center border-2 border-dashed border-gray-300">
|
||||
<div class="w-16 h-16 bg-gray-200 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold mb-2 text-guild-400">More</h3>
|
||||
<p class="text-guild-500 text-sm">Coming soon</p>
|
||||
<h3 class="text-lg font-semibold mb-2 text-gray-600">More</h3>
|
||||
<p class="text-gray-500 text-sm">Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Information -->
|
||||
<div class="bg-candlelight-900/20 border border-candlelight-800 rounded-lg p-6">
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-6">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-candlelight-400 mt-0.5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-6 h-6 text-green-600 mt-0.5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-candlelight-400 mb-2">Admin Interface Status: Fully Working</h3>
|
||||
<div class="space-y-2 text-candlelight-400">
|
||||
<h3 class="text-lg font-semibold text-green-800 mb-2">Admin Interface Status: Fully Working</h3>
|
||||
<div class="space-y-2 text-green-700">
|
||||
<p>✅ <strong>Dashboard:</strong> Shows statistics, recent members, and upcoming events</p>
|
||||
<p>✅ <strong>Member Management:</strong> Full CRUD operations, search, filter, create members</p>
|
||||
<p>✅ <strong>Event Management:</strong> Create, edit, delete, duplicate events with full forms</p>
|
||||
|
|
@ -75,21 +75,21 @@
|
|||
|
||||
<!-- Quick Stats Preview -->
|
||||
<div class="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">{{ memberCount }}</p>
|
||||
<p class="text-sm text-guild-400">Members</p>
|
||||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-blue-600">{{ memberCount }}</p>
|
||||
<p class="text-sm text-gray-600">Members</p>
|
||||
</div>
|
||||
<div class="bg-guild-900 rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">{{ eventCount }}</p>
|
||||
<p class="text-sm text-guild-400">Events</p>
|
||||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-green-600">{{ eventCount }}</p>
|
||||
<p class="text-sm text-gray-600">Events</p>
|
||||
</div>
|
||||
<div class="bg-guild-900 rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-earth-400 text-ui-mono">${{ monthlyRevenue }}</p>
|
||||
<p class="text-sm text-guild-400">Monthly Revenue</p>
|
||||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-purple-600">${{ monthlyRevenue }}</p>
|
||||
<p class="text-sm text-gray-600">Monthly Revenue</p>
|
||||
</div>
|
||||
<div class="bg-guild-900 rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-ember-400 text-ui-mono">{{ pendingInvites }}</p>
|
||||
<p class="text-sm text-guild-400">Pending Invites</p>
|
||||
<div class="bg-white rounded-lg shadow p-4 text-center">
|
||||
<p class="text-2xl font-bold text-orange-600">{{ pendingInvites }}</p>
|
||||
<p class="text-sm text-gray-600">Pending Invites</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b border-default">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Admin Dashboard</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-highlighted">Admin Dashboard</h1>
|
||||
<p class="text-muted">
|
||||
Manage Ghost Guild members, events, and community operations
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -14,19 +14,19 @@
|
|||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Total Members</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Total Members</p>
|
||||
<p class="text-2xl font-bold text-blue-600">
|
||||
{{ stats.totalMembers || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -42,19 +42,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Active Events</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Active Events</p>
|
||||
<p class="text-2xl font-bold text-green-600">
|
||||
{{ stats.activeEvents || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -70,19 +70,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Monthly Revenue</p>
|
||||
<p class="text-2xl font-bold text-purple-600">
|
||||
${{ stats.monthlyRevenue || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -98,19 +98,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
||||
<p class="text-sm text-muted">Pending Slack Invites</p>
|
||||
<p class="text-2xl font-bold text-orange-600">
|
||||
{{ stats.pendingSlackInvites || 0 }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
||||
class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-orange-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -129,13 +129,13 @@
|
|||
|
||||
<!-- Quick Actions -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
class="w-16 h-16 bg-blue-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
class="w-8 h-8 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -148,28 +148,28 @@
|
|||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-display-sm font-semibold mb-2 text-guild-100">
|
||||
<h3 class="text-lg font-semibold mb-2 text-highlighted">
|
||||
Add New Member
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
<p class="text-muted text-sm mb-4">
|
||||
Add a new member to the Ghost Guild community
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/members')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Members
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
class="w-16 h-16 bg-green-100 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-candlelight-400"
|
||||
class="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -182,15 +182,15 @@
|
|||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-display-sm font-semibold mb-2 text-guild-100">
|
||||
<h3 class="text-lg font-semibold mb-2 text-highlighted">
|
||||
Create Event
|
||||
</h3>
|
||||
<p class="text-guild-400 text-sm mb-4">
|
||||
<p class="text-muted text-sm mb-4">
|
||||
Schedule a new community event or workshop
|
||||
</p>
|
||||
<button
|
||||
@click="navigateTo('/admin/events')"
|
||||
class="w-full bg-candlelight-600 text-white py-2 px-4 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
>
|
||||
Manage Events
|
||||
</button>
|
||||
|
|
@ -200,15 +200,15 @@
|
|||
|
||||
<!-- Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="bg-elevated rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Recent Members
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/members')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-sm text-primary hover:text-primary"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
|
|
@ -218,20 +218,20 @@
|
|||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mx-auto"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"
|
||||
></div>
|
||||
</div>
|
||||
<div v-else-if="recentMembers.length" class="space-y-3">
|
||||
<div
|
||||
v-for="member in recentMembers"
|
||||
:key="member._id"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-guild-700"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-default"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
<p class="font-medium text-highlighted">
|
||||
{{ member.name }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-muted">
|
||||
{{ member.email }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -242,27 +242,27 @@
|
|||
>
|
||||
{{ member.circle }}
|
||||
</span>
|
||||
<p class="text-xs text-guild-500 text-ui-mono">
|
||||
<p class="text-xs text-dimmed">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
<div v-else class="text-center py-6 text-dimmed">
|
||||
No recent members
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-900 rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="bg-elevated rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Upcoming Events
|
||||
</h3>
|
||||
<button
|
||||
@click="navigateTo('/admin/events')"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-sm text-primary hover:text-primary"
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
|
|
@ -272,20 +272,20 @@
|
|||
<div class="p-6">
|
||||
<div v-if="pending" class="text-center py-4">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mx-auto"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"
|
||||
></div>
|
||||
</div>
|
||||
<div v-else-if="upcomingEvents.length" class="space-y-3">
|
||||
<div
|
||||
v-for="event in upcomingEvents"
|
||||
:key="event._id"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-guild-700"
|
||||
class="flex items-center justify-between p-3 rounded-lg border border-default"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">
|
||||
<p class="font-medium text-highlighted">
|
||||
{{ event.title }}
|
||||
</p>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-muted">
|
||||
{{ formatDateTime(event.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -296,13 +296,13 @@
|
|||
>
|
||||
{{ event.eventType }}
|
||||
</span>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
{{ event.location || "Online" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-6 text-guild-500">
|
||||
<div v-else class="text-center py-6 text-dimmed">
|
||||
No upcoming events
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -327,21 +327,21 @@ const upcomingEvents = computed(
|
|||
|
||||
const getCircleBadgeClasses = (circle) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
founder: "bg-purple-100 text-purple-800",
|
||||
practitioner: "bg-green-100 text-green-800",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
return classes[circle] || "bg-accented text-default";
|
||||
};
|
||||
|
||||
const getEventTypeBadgeClasses = (type) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop: "bg-candlelight-900/20 text-candlelight-400",
|
||||
social: "bg-earth-900/20 text-earth-400",
|
||||
showcase: "bg-ember-900/20 text-ember-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
workshop: "bg-green-100 text-green-800",
|
||||
social: "bg-purple-100 text-purple-800",
|
||||
showcase: "bg-orange-100 text-orange-800",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
return classes[type] || "bg-accented text-default";
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1 class="text-display font-bold mb-6 text-guild-100">Members</h1>
|
||||
<h1 class="text-2xl font-bold mb-6">Members</h1>
|
||||
|
||||
<div v-if="pending" class="text-center text-guild-400">Loading...</div>
|
||||
<div v-if="pending" class="text-center">Loading...</div>
|
||||
|
||||
<div v-else-if="error" class="text-ember-400">
|
||||
<div v-else-if="error" class="text-red-600">
|
||||
Error loading members: {{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="bg-guild-900 rounded-lg border border-guild-700 p-4">
|
||||
<h3 class="font-semibold mb-2 text-guild-100">Total Members: {{ members?.length || 0 }}</h3>
|
||||
<div class="bg-white rounded-lg border p-4">
|
||||
<h3 class="font-semibold mb-2">Total Members: {{ members?.length || 0 }}</h3>
|
||||
|
||||
<div v-for="member in members" :key="member._id" class="border-b border-guild-700 pb-2 mb-2 last:border-b-0">
|
||||
<div v-for="member in members" :key="member._id" class="border-b pb-2 mb-2 last:border-b-0">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="font-medium text-guild-100">{{ member.name }}</p>
|
||||
<p class="text-guild-400 text-sm">{{ member.email }}</p>
|
||||
<p class="font-medium">{{ member.name }}</p>
|
||||
<p class="text-gray-600 text-sm">{{ member.email }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded bg-candlelight-900/20 text-candlelight-400">
|
||||
<span class="inline-block px-2 py-1 text-xs rounded bg-blue-100 text-blue-800">
|
||||
{{ member.circle }}
|
||||
</span>
|
||||
<p class="text-sm text-guild-500 text-ui-mono">${{ member.contributionTier }}/month</p>
|
||||
<p class="text-sm text-gray-500">${{ member.contributionTier }}/month</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Add Member Form -->
|
||||
<div class="bg-guild-900 rounded-lg border border-guild-700 p-4">
|
||||
<h3 class="text-display-sm font-semibold mb-4 text-guild-100">Add Member</h3>
|
||||
<div class="bg-white rounded-lg border p-4">
|
||||
<h3 class="font-semibold mb-4">Add Member</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<input v-model="newMember.name" placeholder="Name" class="border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded p-2" />
|
||||
<input v-model="newMember.email" placeholder="Email" class="border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded p-2" />
|
||||
<select v-model="newMember.circle" class="border border-guild-700 bg-guild-800 text-guild-100 rounded p-2">
|
||||
<input v-model="newMember.name" placeholder="Name" class="border rounded p-2" />
|
||||
<input v-model="newMember.email" placeholder="Email" class="border rounded p-2" />
|
||||
<select v-model="newMember.circle" class="border rounded p-2">
|
||||
<option value="community">Community</option>
|
||||
<option value="founder">Founder</option>
|
||||
<option value="practitioner">Practitioner</option>
|
||||
</select>
|
||||
<select v-model="newMember.contributionTier" class="border border-guild-700 bg-guild-800 text-guild-100 rounded p-2">
|
||||
<select v-model="newMember.contributionTier" class="border rounded p-2">
|
||||
<option value="0">$0/month</option>
|
||||
<option value="5">$5/month</option>
|
||||
<option value="15">$15/month</option>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<option value="50">$50/month</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="createMember" :disabled="creating" class="mt-4 bg-candlelight-600 text-white px-4 py-2 rounded hover:bg-candlelight-700 disabled:opacity-50">
|
||||
<button @click="createMember" :disabled="creating" class="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50">
|
||||
{{ creating ? 'Adding...' : 'Add Member' }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-white border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Member Management</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Member Management</h1>
|
||||
<p class="text-gray-600">
|
||||
Manage Ghost Guild members, their contributions, and access levels
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search members..."
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 placeholder-guild-500 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="border border-gray-300 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<select
|
||||
v-model="circleFilter"
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 rounded-lg px-4 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">All Circles</option>
|
||||
<option value="community">Community</option>
|
||||
|
|
@ -32,80 +32,80 @@
|
|||
</div>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Add Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Members Table -->
|
||||
<div class="bg-guild-900 rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center text-guild-100">
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center">
|
||||
<div class="inline-flex items-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mr-3"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"
|
||||
></div>
|
||||
Loading members...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="p-8 text-center text-ember-400">
|
||||
<div v-else-if="error" class="p-8 text-center text-red-600">
|
||||
Error loading members: {{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full">
|
||||
<thead class="bg-guild-950">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Circle
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Contribution
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Slack Status
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Joined
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-guild-900 divide-y divide-guild-700">
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr
|
||||
v-for="member in filteredMembers"
|
||||
:key="member._id"
|
||||
class="hover:bg-guild-800"
|
||||
class="hover:bg-gray-50"
|
||||
>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-guild-100">
|
||||
<div class="text-sm font-medium text-gray-900">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-guild-400">{{ member.email }}</div>
|
||||
<div class="text-sm text-gray-600">{{ member.email }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-candlelight-900/20 text-candlelight-400"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800"
|
||||
>
|
||||
${{ member.contributionTier }}/month
|
||||
</span>
|
||||
|
|
@ -126,28 +126,28 @@
|
|||
<span
|
||||
:class="
|
||||
member.slackInvited
|
||||
? 'bg-candlelight-900/20 text-candlelight-400'
|
||||
: 'bg-guild-800 text-guild-300'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
>
|
||||
{{ member.slackInvited ? "Invited" : "Pending" }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-400 text-ui-mono">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-400">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="sendSlackInvite(member)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
Slack Invite
|
||||
</button>
|
||||
<button
|
||||
@click="editMember(member)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary-600 hover:text-primary-900"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
|
||||
<div
|
||||
v-if="!pending && !error && filteredMembers.length === 0"
|
||||
class="p-8 text-center text-guild-500"
|
||||
class="p-8 text-center text-gray-500"
|
||||
>
|
||||
No members found matching your criteria
|
||||
</div>
|
||||
|
|
@ -171,26 +171,26 @@
|
|||
v-if="showCreateModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-guild-900 rounded-lg shadow-xl max-w-md w-full mx-4 border border-guild-700">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">Add New Member</h3>
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div class="px-6 py-4 border-b">
|
||||
<h3 class="text-lg font-semibold">Add New Member</h3>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createMember" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Name</label
|
||||
>
|
||||
<input
|
||||
v-model="newMember.name"
|
||||
placeholder="Full name"
|
||||
required
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Email</label
|
||||
>
|
||||
<input
|
||||
|
|
@ -198,17 +198,17 @@
|
|||
type="email"
|
||||
placeholder="email@example.com"
|
||||
required
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Circle</label
|
||||
>
|
||||
<select
|
||||
v-model="newMember.circle"
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="community">Community</option>
|
||||
<option value="founder">Founder</option>
|
||||
|
|
@ -217,12 +217,12 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>Contribution Tier</label
|
||||
>
|
||||
<select
|
||||
v-model="newMember.contributionTier"
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="0">$0/month</option>
|
||||
<option value="5">$5/month</option>
|
||||
|
|
@ -236,14 +236,14 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ creating ? "Creating..." : "Create Member" }}
|
||||
</button>
|
||||
|
|
@ -296,11 +296,11 @@ const filteredMembers = computed(() => {
|
|||
|
||||
const getCircleClasses = (circle) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
founder: "bg-purple-100 text-purple-800",
|
||||
practitioner: "bg-green-100 text-green-800",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
return classes[circle] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b border-default">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Member Management</h1>
|
||||
<p class="text-guild-400">
|
||||
<h1 class="text-2xl font-bold text-highlighted">Member Management</h1>
|
||||
<p class="text-muted">
|
||||
Manage Ghost Guild members, their contributions, and access levels
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -18,11 +18,11 @@
|
|||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search members..."
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 placeholder-guild-500 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<select
|
||||
v-model="circleFilter"
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 rounded-lg px-4 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="border border-default bg-elevated text-default rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">All Circles</option>
|
||||
<option value="community">Community</option>
|
||||
|
|
@ -30,114 +30,82 @@
|
|||
<option value="practitioner">Practitioner</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="showImportModal = true"
|
||||
class="border border-guild-600 text-guild-100 px-4 py-2 rounded-lg hover:bg-guild-800 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
>
|
||||
Import CSV
|
||||
</button>
|
||||
<button
|
||||
:disabled="selectedMemberIds.length === 0"
|
||||
@click="openInviteModal"
|
||||
class="border border-guild-600 text-guild-100 px-4 py-2 rounded-lg hover:bg-guild-800 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send Invites ({{ selectedMemberIds.length }})
|
||||
</button>
|
||||
<button
|
||||
@click="showCreateModal = true"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Add Member
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members Table -->
|
||||
<div class="bg-guild-900 rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center text-guild-100">
|
||||
<div class="bg-elevated rounded-lg shadow overflow-hidden">
|
||||
<div v-if="pending" class="p-8 text-center text-default">
|
||||
<div class="inline-flex items-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-candlelight-500 mr-3"
|
||||
class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mr-3"
|
||||
></div>
|
||||
Loading members...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="p-8 text-center text-ember-400">
|
||||
<div v-else-if="error" class="p-8 text-center text-red-600">
|
||||
Error loading members: {{ error }}
|
||||
</div>
|
||||
|
||||
<table v-else class="w-full">
|
||||
<thead class="bg-guild-950">
|
||||
<thead class="bg-muted">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left">
|
||||
<UCheckbox
|
||||
:model-value="allVisibleSelected ? true : (someVisibleSelected ? 'indeterminate' : false)"
|
||||
@update:model-value="toggleSelectAll"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Circle
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Contribution
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Invite
|
||||
Slack Status
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
>
|
||||
Slack
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Joined
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-guild-500 uppercase tracking-wider"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-dimmed uppercase tracking-wider"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-guild-900 divide-y divide-guild-700">
|
||||
<tbody class="bg-elevated divide-y divide-default">
|
||||
<tr
|
||||
v-for="member in filteredMembers"
|
||||
:key="member._id"
|
||||
class="hover:bg-guild-800"
|
||||
class="hover:bg-muted"
|
||||
>
|
||||
<td class="px-4 py-4">
|
||||
<UCheckbox
|
||||
:model-value="selectedMemberIds.includes(member._id)"
|
||||
@update:model-value="toggleSelect(member._id)"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-guild-100">
|
||||
<div class="text-sm font-medium text-highlighted">
|
||||
{{ member.name }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-guild-400">
|
||||
<div class="text-sm text-muted">
|
||||
{{ member.email }}
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -151,49 +119,37 @@
|
|||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-candlelight-900/20 text-candlelight-400"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800"
|
||||
>
|
||||
${{ member.contributionTier }}/month
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="
|
||||
member.inviteEmailSent
|
||||
? 'bg-candlelight-900/20 text-candlelight-400'
|
||||
: 'bg-guild-800 text-guild-500'
|
||||
"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
>
|
||||
{{ member.inviteEmailSent ? 'Sent' : 'Not sent' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
:class="
|
||||
member.slackInvited
|
||||
? 'bg-candlelight-900/20 text-candlelight-400'
|
||||
: 'bg-guild-800 text-guild-300'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-accented text-default'
|
||||
"
|
||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full"
|
||||
>
|
||||
{{ member.slackInvited ? "Invited" : "Pending" }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-400 text-ui-mono">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
|
||||
{{ formatDate(member.createdAt) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-guild-400">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-muted">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="sendSlackInvite(member)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary hover:text-primary"
|
||||
>
|
||||
Slack Invite
|
||||
</button>
|
||||
<button
|
||||
@click="editMember(member)"
|
||||
class="text-candlelight-400 hover:text-candlelight-300"
|
||||
class="text-primary hover:text-primary"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
|
@ -205,7 +161,7 @@
|
|||
|
||||
<div
|
||||
v-if="!pending && !error && filteredMembers.length === 0"
|
||||
class="p-8 text-center text-guild-500"
|
||||
class="p-8 text-center text-dimmed"
|
||||
>
|
||||
No members found matching your criteria
|
||||
</div>
|
||||
|
|
@ -217,26 +173,26 @@
|
|||
v-if="showCreateModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-guild-900 rounded-lg shadow-xl max-w-md w-full mx-4 border border-guild-700">
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">Add New Member</h3>
|
||||
<div class="bg-elevated rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<h3 class="text-lg font-semibold text-highlighted">Add New Member</h3>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="createMember" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Name</label
|
||||
>
|
||||
<input
|
||||
v-model="newMember.name"
|
||||
placeholder="Full name"
|
||||
required
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Email</label
|
||||
>
|
||||
<input
|
||||
|
|
@ -244,12 +200,12 @@
|
|||
type="email"
|
||||
placeholder="email@example.com"
|
||||
required
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded-lg px-3 py-2 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Circle</label
|
||||
>
|
||||
<USelect
|
||||
|
|
@ -264,7 +220,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-1"
|
||||
<label class="block text-sm font-medium text-default mb-1"
|
||||
>Contribution Tier</label
|
||||
>
|
||||
<USelect
|
||||
|
|
@ -284,14 +240,14 @@
|
|||
<button
|
||||
type="button"
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
class="px-4 py-2 text-muted hover:text-default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ creating ? "Creating..." : "Create Member" }}
|
||||
</button>
|
||||
|
|
@ -299,197 +255,6 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Import Modal -->
|
||||
<div
|
||||
v-if="showImportModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-guild-900 rounded-lg shadow-xl max-w-2xl w-full mx-4 border border-guild-700 max-h-[90vh] flex flex-col">
|
||||
<div class="px-6 py-4 border-b border-guild-700 flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">Import Members from CSV</h3>
|
||||
<button @click="closeImportModal" class="text-guild-500 hover:text-guild-300">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4 overflow-y-auto">
|
||||
<!-- File input -->
|
||||
<div v-if="!csvRows.length">
|
||||
<p class="text-sm text-guild-400 mb-3">
|
||||
Upload a CSV file with columns: <code class="text-guild-200">name,email,circle,contributionTier</code>
|
||||
</p>
|
||||
<p class="text-sm text-guild-500 mb-4">
|
||||
Valid circles: community, founder, practitioner. Valid tiers: 0, 5, 15, 30, 50.
|
||||
</p>
|
||||
<input
|
||||
ref="csvFileInput"
|
||||
type="file"
|
||||
accept=".csv"
|
||||
@change="handleCsvFile"
|
||||
class="block w-full text-sm text-guild-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-candlelight-600 file:text-white hover:file:bg-candlelight-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Parse error -->
|
||||
<div v-if="csvParseError" class="p-3 bg-ember-500/10 border border-ember-500/30 rounded-lg">
|
||||
<p class="text-ember-400 text-sm">{{ csvParseError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Preview table -->
|
||||
<div v-if="csvRows.length" class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-sm text-guild-300">
|
||||
{{ csvRows.length }} row{{ csvRows.length !== 1 ? 's' : '' }} parsed.
|
||||
<span v-if="csvValidRows.length !== csvRows.length" class="text-ember-400">
|
||||
{{ csvRows.length - csvValidRows.length }} with errors.
|
||||
</span>
|
||||
</p>
|
||||
<button
|
||||
@click="resetCsvImport"
|
||||
class="text-sm text-guild-400 hover:text-guild-200"
|
||||
>
|
||||
Choose different file
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto max-h-64 overflow-y-auto rounded border border-guild-700">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-guild-950 sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left text-xs text-guild-500 uppercase">Status</th>
|
||||
<th class="px-3 py-2 text-left text-xs text-guild-500 uppercase">Name</th>
|
||||
<th class="px-3 py-2 text-left text-xs text-guild-500 uppercase">Email</th>
|
||||
<th class="px-3 py-2 text-left text-xs text-guild-500 uppercase">Circle</th>
|
||||
<th class="px-3 py-2 text-left text-xs text-guild-500 uppercase">Tier</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-guild-800">
|
||||
<tr v-for="(row, i) in csvRows" :key="i" :class="row.error ? 'bg-ember-500/5' : ''">
|
||||
<td class="px-3 py-2">
|
||||
<span v-if="row.error" class="text-ember-400 text-xs">{{ row.error }}</span>
|
||||
<span v-else class="text-candlelight-400 text-xs">OK</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 text-guild-200">{{ row.name }}</td>
|
||||
<td class="px-3 py-2 text-guild-400">{{ row.email }}</td>
|
||||
<td class="px-3 py-2 text-guild-400">{{ row.circle }}</td>
|
||||
<td class="px-3 py-2 text-guild-400">${{ row.contributionTier }}/mo</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Import results -->
|
||||
<div v-if="importResults" class="p-4 rounded-lg border border-guild-700 bg-guild-800">
|
||||
<p class="text-guild-100 font-medium mb-2">Import complete</p>
|
||||
<p class="text-sm text-candlelight-400">{{ importResults.created }} created</p>
|
||||
<p v-if="importResults.failed" class="text-sm text-ember-400">{{ importResults.failed }} failed</p>
|
||||
<div v-if="importResults.results?.some(r => !r.success)" class="mt-2 space-y-1">
|
||||
<p
|
||||
v-for="fail in importResults.results.filter(r => !r.success)"
|
||||
:key="fail.email"
|
||||
class="text-xs text-ember-400"
|
||||
>
|
||||
{{ fail.email }}: {{ fail.error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-guild-700 flex justify-end gap-3">
|
||||
<button
|
||||
@click="closeImportModal"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
>
|
||||
{{ importResults ? 'Done' : 'Cancel' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="csvValidRows.length && !importResults"
|
||||
:disabled="importing"
|
||||
@click="submitImport"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ importing ? 'Importing...' : `Import ${csvValidRows.length} member${csvValidRows.length !== 1 ? 's' : ''}` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Invites Modal -->
|
||||
<div
|
||||
v-if="showInviteModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-guild-900 rounded-lg shadow-xl max-w-2xl w-full mx-4 border border-guild-700 max-h-[90vh] flex flex-col">
|
||||
<div class="px-6 py-4 border-b border-guild-700 flex justify-between items-center">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">Send Invite Emails</h3>
|
||||
<button @click="showInviteModal = false" class="text-guild-500 hover:text-guild-300">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-4 overflow-y-auto">
|
||||
<p class="text-sm text-guild-400">
|
||||
Sending to <strong class="text-guild-200">{{ selectedMemberIds.length }}</strong>
|
||||
member{{ selectedMemberIds.length !== 1 ? 's' : '' }}.
|
||||
Each will receive a unique magic login link valid for 48 hours.
|
||||
</p>
|
||||
|
||||
<!-- Email template editor -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Email template</label>
|
||||
<textarea
|
||||
v-model="inviteTemplate"
|
||||
rows="12"
|
||||
class="w-full border border-guild-700 bg-guild-800 text-guild-100 placeholder-guild-500 rounded-lg px-3 py-2 font-mono text-sm focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
||||
></textarea>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
Available tokens: <code>{name}</code>, <code>{loginLink}</code>, <code>{circle}</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<div v-if="invitePreviewMember">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Preview ({{ invitePreviewMember.name }})</label>
|
||||
<pre class="text-sm text-guild-300 bg-guild-950 border border-guild-700 rounded-lg p-4 whitespace-pre-wrap font-mono overflow-auto max-h-48">{{ invitePreviewText }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Results -->
|
||||
<div v-if="inviteResults" class="p-4 rounded-lg border border-guild-700 bg-guild-800">
|
||||
<p class="text-guild-100 font-medium mb-2">Invites sent</p>
|
||||
<p class="text-sm text-candlelight-400">{{ inviteResults.sent }} sent</p>
|
||||
<p v-if="inviteResults.failed" class="text-sm text-ember-400">{{ inviteResults.failed }} failed</p>
|
||||
<div v-if="inviteResults.results?.some(r => !r.success)" class="mt-2 space-y-1">
|
||||
<p
|
||||
v-for="fail in inviteResults.results.filter(r => !r.success)"
|
||||
:key="fail.email"
|
||||
class="text-xs text-ember-400"
|
||||
>
|
||||
{{ fail.email }}: {{ fail.error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-guild-700 flex justify-end gap-3">
|
||||
<button
|
||||
@click="showInviteModal = false"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
>
|
||||
{{ inviteResults ? 'Done' : 'Cancel' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!inviteResults"
|
||||
:disabled="sendingInvites"
|
||||
@click="submitInvites"
|
||||
class="bg-candlelight-600 text-white px-4 py-2 rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ sendingInvites ? 'Sending...' : `Send ${selectedMemberIds.length} invite${selectedMemberIds.length !== 1 ? 's' : ''}` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -498,8 +263,6 @@ definePageMeta({
|
|||
layout: "admin",
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const {
|
||||
data: members,
|
||||
pending,
|
||||
|
|
@ -512,35 +275,6 @@ const circleFilter = ref("");
|
|||
const showCreateModal = ref(false);
|
||||
const creating = ref(false);
|
||||
|
||||
// Selection
|
||||
const selectedMemberIds = ref([]);
|
||||
|
||||
// CSV import
|
||||
const showImportModal = ref(false);
|
||||
const csvFileInput = ref(null);
|
||||
const csvRows = ref([]);
|
||||
const csvParseError = ref("");
|
||||
const importing = ref(false);
|
||||
const importResults = ref(null);
|
||||
|
||||
// Invite
|
||||
const showInviteModal = ref(false);
|
||||
const sendingInvites = ref(false);
|
||||
const inviteResults = ref(null);
|
||||
|
||||
const DEFAULT_INVITE_TEMPLATE = `Hi {name},
|
||||
|
||||
You've been invited to Ghost Guild as a member of the {circle} circle.
|
||||
|
||||
Sign in here to get started:
|
||||
{loginLink}
|
||||
|
||||
This link expires in 48 hours. After that, you can always request a new login link at https://ghostguild.org/login.
|
||||
|
||||
See you inside.`;
|
||||
|
||||
const inviteTemplate = ref(DEFAULT_INVITE_TEMPLATE);
|
||||
|
||||
const newMember = reactive({
|
||||
name: "",
|
||||
email: "",
|
||||
|
|
@ -564,72 +298,19 @@ const filteredMembers = computed(() => {
|
|||
});
|
||||
});
|
||||
|
||||
// Selection helpers
|
||||
const allVisibleSelected = computed(() => {
|
||||
if (!filteredMembers.value.length) return false;
|
||||
return filteredMembers.value.every((m) =>
|
||||
selectedMemberIds.value.includes(m._id)
|
||||
);
|
||||
});
|
||||
|
||||
const someVisibleSelected = computed(() => {
|
||||
return filteredMembers.value.some((m) =>
|
||||
selectedMemberIds.value.includes(m._id)
|
||||
);
|
||||
});
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (allVisibleSelected.value) {
|
||||
const visibleIds = new Set(filteredMembers.value.map((m) => m._id));
|
||||
selectedMemberIds.value = selectedMemberIds.value.filter(
|
||||
(id) => !visibleIds.has(id)
|
||||
);
|
||||
} else {
|
||||
const currentSet = new Set(selectedMemberIds.value);
|
||||
for (const m of filteredMembers.value) {
|
||||
currentSet.add(m._id);
|
||||
}
|
||||
selectedMemberIds.value = [...currentSet];
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelect = (id) => {
|
||||
const idx = selectedMemberIds.value.indexOf(id);
|
||||
if (idx >= 0) {
|
||||
selectedMemberIds.value.splice(idx, 1);
|
||||
} else {
|
||||
selectedMemberIds.value.push(id);
|
||||
}
|
||||
};
|
||||
|
||||
// Invite preview
|
||||
const invitePreviewMember = computed(() => {
|
||||
if (!selectedMemberIds.value.length || !members.value) return null;
|
||||
return members.value.find((m) => m._id === selectedMemberIds.value[0]);
|
||||
});
|
||||
|
||||
const invitePreviewText = computed(() => {
|
||||
if (!invitePreviewMember.value) return "";
|
||||
return inviteTemplate.value
|
||||
.replace(/\{name\}/g, invitePreviewMember.value.name)
|
||||
.replace(/\{loginLink\}/g, "https://ghostguild.org/api/auth/verify?token=...")
|
||||
.replace(/\{circle\}/g, invitePreviewMember.value.circle);
|
||||
});
|
||||
|
||||
const getCircleClasses = (circle) => {
|
||||
const classes = {
|
||||
community: "bg-candlelight-900/20 text-candlelight-400",
|
||||
founder: "bg-earth-900/20 text-earth-400",
|
||||
practitioner: "bg-candlelight-900/20 text-candlelight-400",
|
||||
community: "bg-blue-100 text-blue-800",
|
||||
founder: "bg-purple-100 text-purple-800",
|
||||
practitioner: "bg-green-100 text-green-800",
|
||||
};
|
||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
||||
return classes[circle] || "bg-gray-100 text-gray-800";
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
};
|
||||
|
||||
// --- Create Member ---
|
||||
const createMember = async () => {
|
||||
creating.value = true;
|
||||
try {
|
||||
|
|
@ -647,171 +328,15 @@ const createMember = async () => {
|
|||
});
|
||||
|
||||
await refresh();
|
||||
toast.add({ title: "Member created", color: "green" });
|
||||
} catch (err) {
|
||||
console.error("Failed to create member:", err);
|
||||
toast.add({
|
||||
title: "Failed to create member",
|
||||
description: err.data?.statusMessage || err.message,
|
||||
color: "red",
|
||||
});
|
||||
alert("Member created successfully!");
|
||||
} catch (error) {
|
||||
console.error("Failed to create member:", error);
|
||||
alert("Failed to create member");
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- CSV Import ---
|
||||
const VALID_CIRCLES = ["community", "founder", "practitioner"];
|
||||
const VALID_TIERS = ["0", "5", "15", "30", "50"];
|
||||
|
||||
const handleCsvFile = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
csvParseError.value = "";
|
||||
csvRows.value = [];
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const text = e.target.result;
|
||||
parseCsv(text);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const parseCsv = (text) => {
|
||||
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
||||
if (lines.length < 2) {
|
||||
csvParseError.value = "CSV must have a header row and at least one data row.";
|
||||
return;
|
||||
}
|
||||
|
||||
const header = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
const nameIdx = header.indexOf("name");
|
||||
const emailIdx = header.indexOf("email");
|
||||
const circleIdx = header.indexOf("circle");
|
||||
const tierIdx = header.indexOf("contributiontier");
|
||||
|
||||
if (nameIdx === -1 || emailIdx === -1 || circleIdx === -1 || tierIdx === -1) {
|
||||
csvParseError.value = `Missing required columns. Found: ${header.join(", ")}. Need: name, email, circle, contributionTier`;
|
||||
return;
|
||||
}
|
||||
|
||||
const seenEmails = new Set();
|
||||
const rows = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cols = lines[i].split(",").map((c) => c.trim());
|
||||
const name = cols[nameIdx] || "";
|
||||
const email = (cols[emailIdx] || "").toLowerCase();
|
||||
const circle = (cols[circleIdx] || "").toLowerCase();
|
||||
const contributionTier = cols[tierIdx] || "";
|
||||
|
||||
let error = null;
|
||||
if (!name) error = "Missing name";
|
||||
else if (!email || !email.includes("@")) error = "Invalid email";
|
||||
else if (!VALID_CIRCLES.includes(circle)) error = `Invalid circle: ${circle}`;
|
||||
else if (!VALID_TIERS.includes(contributionTier)) error = `Invalid tier: ${contributionTier}`;
|
||||
else if (seenEmails.has(email)) error = "Duplicate email in CSV";
|
||||
|
||||
if (!error) seenEmails.add(email);
|
||||
|
||||
rows.push({ name, email, circle, contributionTier, error });
|
||||
}
|
||||
|
||||
csvRows.value = rows;
|
||||
};
|
||||
|
||||
const csvValidRows = computed(() => csvRows.value.filter((r) => !r.error));
|
||||
|
||||
const resetCsvImport = () => {
|
||||
csvRows.value = [];
|
||||
csvParseError.value = "";
|
||||
importResults.value = null;
|
||||
if (csvFileInput.value) csvFileInput.value.value = "";
|
||||
};
|
||||
|
||||
const closeImportModal = () => {
|
||||
showImportModal.value = false;
|
||||
if (importResults.value) {
|
||||
resetCsvImport();
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const submitImport = async () => {
|
||||
importing.value = true;
|
||||
try {
|
||||
const payload = csvValidRows.value.map(({ name, email, circle, contributionTier }) => ({
|
||||
name,
|
||||
email,
|
||||
circle,
|
||||
contributionTier,
|
||||
}));
|
||||
|
||||
const result = await $fetch("/api/admin/members/import", {
|
||||
method: "POST",
|
||||
body: { members: payload },
|
||||
});
|
||||
|
||||
importResults.value = result;
|
||||
toast.add({
|
||||
title: `Imported ${result.created} member${result.created !== 1 ? "s" : ""}`,
|
||||
description: result.failed ? `${result.failed} failed` : undefined,
|
||||
color: result.failed ? "orange" : "green",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Import failed:", err);
|
||||
toast.add({
|
||||
title: "Import failed",
|
||||
description: err.data?.statusMessage || err.message,
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Send Invites ---
|
||||
const openInviteModal = () => {
|
||||
inviteResults.value = null;
|
||||
inviteTemplate.value = DEFAULT_INVITE_TEMPLATE;
|
||||
showInviteModal.value = true;
|
||||
};
|
||||
|
||||
const submitInvites = async () => {
|
||||
sendingInvites.value = true;
|
||||
try {
|
||||
const result = await $fetch("/api/admin/members/invite", {
|
||||
method: "POST",
|
||||
body: {
|
||||
memberIds: selectedMemberIds.value,
|
||||
emailTemplate: inviteTemplate.value,
|
||||
},
|
||||
});
|
||||
|
||||
inviteResults.value = result;
|
||||
await refresh();
|
||||
selectedMemberIds.value = [];
|
||||
|
||||
toast.add({
|
||||
title: `Sent ${result.sent} invite${result.sent !== 1 ? "s" : ""}`,
|
||||
description: result.failed ? `${result.failed} failed` : undefined,
|
||||
color: result.failed ? "orange" : "green",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to send invites:", err);
|
||||
toast.add({
|
||||
title: "Failed to send invites",
|
||||
description: err.data?.statusMessage || err.message,
|
||||
color: "red",
|
||||
});
|
||||
} finally {
|
||||
sendingInvites.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Existing actions ---
|
||||
const sendSlackInvite = (member) => {
|
||||
alert(`Slack invite functionality would send invite to ${member.email}`);
|
||||
console.log("Send Slack invite to:", member.email);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-elevated border-b border-default">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<h1 class="text-display font-bold text-guild-100">Series Management</h1>
|
||||
<p class="text-guild-400">Manage event series and their relationships</p>
|
||||
<h1 class="text-2xl font-bold text-highlighted">Series Management</h1>
|
||||
<p class="text-muted">Manage event series and their relationships</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -13,49 +13,49 @@
|
|||
<!-- Series Overview -->
|
||||
<div class="mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-earth-900/20 rounded-full">
|
||||
<div class="p-3 bg-purple-100 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:squares-2x2"
|
||||
class="w-6 h-6 text-earth-400"
|
||||
class="w-6 h-6 text-purple-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-guild-500">Active Series</p>
|
||||
<p class="text-2xl font-semibold text-guild-100 text-ui-mono">
|
||||
<p class="text-sm text-dimmed">Active Series</p>
|
||||
<p class="text-2xl font-semibold text-highlighted">
|
||||
{{ activeSeries.length }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-candlelight-900/20 rounded-full">
|
||||
<div class="p-3 bg-blue-100 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:calendar-days"
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-blue-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-guild-500">Total Series Events</p>
|
||||
<p class="text-2xl font-semibold text-guild-100 text-ui-mono">
|
||||
<p class="text-sm text-dimmed">Total Series Events</p>
|
||||
<p class="text-2xl font-semibold text-highlighted">
|
||||
{{ totalSeriesEvents }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
||||
<div class="bg-elevated rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-candlelight-900/20 rounded-full">
|
||||
<div class="p-3 bg-green-100 rounded-full">
|
||||
<Icon
|
||||
name="heroicons:chart-bar"
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-guild-500">Avg Events/Series</p>
|
||||
<p class="text-2xl font-semibold text-guild-100 text-ui-mono">
|
||||
<p class="text-sm text-dimmed">Avg Events/Series</p>
|
||||
<p class="text-2xl font-semibold text-highlighted">
|
||||
{{
|
||||
activeSeries.length > 0
|
||||
? Math.round(totalSeriesEvents / activeSeries.length)
|
||||
|
|
@ -74,11 +74,11 @@
|
|||
<input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search series..."
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 placeholder-guild-500 rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 w-80 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
<select
|
||||
v-model="statusFilter"
|
||||
class="border border-guild-700 bg-guild-900 text-guild-100 rounded-lg px-4 py-2 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="border border-default bg-elevated text-default rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
|
|
@ -89,14 +89,14 @@
|
|||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="showBulkModal = true"
|
||||
class="bg-guild-600 text-white px-4 py-2 rounded-lg hover:bg-guild-700 inline-flex items-center"
|
||||
class="bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 inline-flex items-center"
|
||||
>
|
||||
<Icon name="heroicons:cog-6-tooth" class="w-4 h-4 mr-2" />
|
||||
Bulk Operations
|
||||
</button>
|
||||
<NuxtLink
|
||||
to="/admin/series/create"
|
||||
class="bg-earth-600 text-white px-4 py-2 rounded-lg hover:bg-earth-700 inline-flex items-center"
|
||||
class="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 inline-flex items-center"
|
||||
>
|
||||
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
||||
Create Series
|
||||
|
|
@ -107,19 +107,19 @@
|
|||
<!-- Series List -->
|
||||
<div v-if="pending" class="text-center py-12">
|
||||
<div
|
||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-earth-500 mx-auto mb-4"
|
||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500 mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-guild-400">Loading series...</p>
|
||||
<p class="text-muted">Loading series...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredSeries.length > 0" class="space-y-6">
|
||||
<div
|
||||
v-for="series in filteredSeries"
|
||||
:key="series.id"
|
||||
class="bg-guild-900 rounded-lg shadow overflow-hidden"
|
||||
class="bg-elevated rounded-lg shadow overflow-hidden"
|
||||
>
|
||||
<!-- Series Header -->
|
||||
<div class="px-6 py-4 bg-guild-950 border-b border-guild-700">
|
||||
<div class="px-6 py-4 bg-muted border-b border-default">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
|
|
@ -131,10 +131,10 @@
|
|||
{{ formatSeriesType(series.type) }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
{{ series.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-guild-400">{{ series.description }}</p>
|
||||
<p class="text-sm text-muted">{{ series.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -142,15 +142,15 @@
|
|||
:class="[
|
||||
'inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
|
||||
series.status === 'active'
|
||||
? 'bg-candlelight-900/20 text-candlelight-400'
|
||||
? 'bg-green-100 text-green-700'
|
||||
: series.status === 'upcoming'
|
||||
? 'bg-earth-900/20 text-earth-400'
|
||||
: 'bg-guild-800 text-guild-300',
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-accented text-default',
|
||||
]"
|
||||
>
|
||||
{{ series.status }}
|
||||
</span>
|
||||
<span class="text-sm text-guild-500">
|
||||
<span class="text-sm text-dimmed">
|
||||
{{ series.eventCount }} events
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -158,24 +158,24 @@
|
|||
</div>
|
||||
|
||||
<!-- Series Events -->
|
||||
<div class="divide-y divide-guild-700">
|
||||
<div class="divide-y divide-default">
|
||||
<div
|
||||
v-for="event in series.events"
|
||||
:key="event.id"
|
||||
class="px-6 py-4 hover:bg-guild-800"
|
||||
class="px-6 py-4 hover:bg-muted"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="w-8 h-8 bg-earth-800 text-earth-400 rounded-full flex items-center justify-center text-sm font-semibold"
|
||||
class="w-8 h-8 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center text-sm font-semibold"
|
||||
>
|
||||
{{ event.series?.position || "?" }}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-guild-100">
|
||||
<h4 class="text-sm font-medium text-highlighted">
|
||||
{{ event.title }}
|
||||
</h4>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
{{ formatEventDate(event.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -192,21 +192,21 @@
|
|||
<div class="flex gap-1">
|
||||
<NuxtLink
|
||||
:to="`/events/${event.slug || event.id}`"
|
||||
class="p-1 text-guild-500 hover:text-guild-100 rounded"
|
||||
class="p-1 text-muted hover:text-default rounded"
|
||||
title="View Event"
|
||||
>
|
||||
<Icon name="heroicons:eye" class="w-4 h-4" />
|
||||
</NuxtLink>
|
||||
<button
|
||||
@click="editEvent(event)"
|
||||
class="p-1 text-candlelight-400 hover:text-candlelight-300 rounded"
|
||||
class="p-1 text-muted hover:text-primary rounded"
|
||||
title="Edit Event"
|
||||
>
|
||||
<Icon name="heroicons:pencil-square" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
@click="removeFromSeries(event)"
|
||||
class="p-1 text-ember-400 hover:text-ember-300 rounded"
|
||||
class="p-1 text-muted hover:text-red-600 rounded"
|
||||
title="Remove from Series"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||
|
|
@ -220,16 +220,16 @@
|
|||
<!-- Series Ticketing Info -->
|
||||
<div
|
||||
v-if="series.tickets?.enabled"
|
||||
class="px-6 py-3 bg-candlelight-900/20 border-t border-guild-700"
|
||||
class="px-6 py-3 bg-blue-50 dark:bg-blue-950/20 border-t border-default"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<Icon name="heroicons:ticket" class="w-5 h-5 text-candlelight-400" />
|
||||
<Icon name="heroicons:ticket" class="w-5 h-5 text-blue-600" />
|
||||
<div>
|
||||
<span class="text-sm font-medium text-guild-100">
|
||||
<span class="text-sm font-medium text-default">
|
||||
Series Pass Ticketing Enabled
|
||||
</span>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
<span v-if="series.tickets.public?.available">
|
||||
Public: ${{ series.tickets.public.price || 0 }}
|
||||
</span>
|
||||
|
|
@ -246,7 +246,7 @@
|
|||
</div>
|
||||
<button
|
||||
@click="manageSeriesTickets(series)"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-sm text-blue-600 hover:text-blue-700 font-medium"
|
||||
>
|
||||
Manage Tickets
|
||||
</button>
|
||||
|
|
@ -254,39 +254,39 @@
|
|||
</div>
|
||||
|
||||
<!-- Series Actions -->
|
||||
<div class="px-6 py-3 bg-guild-950 border-t border-guild-700">
|
||||
<div class="px-6 py-3 bg-muted border-t border-default">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm text-guild-500">
|
||||
<div class="text-sm text-dimmed">
|
||||
{{ formatDateRange(series.startDate, series.endDate) }}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="manageSeriesTickets(series)"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
>
|
||||
Ticketing
|
||||
</button>
|
||||
<button
|
||||
@click="editSeries(series)"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
>
|
||||
Edit Series
|
||||
</button>
|
||||
<button
|
||||
@click="addEventToSeries(series)"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
>
|
||||
Add Event
|
||||
</button>
|
||||
<button
|
||||
@click="duplicateSeries(series)"
|
||||
class="text-sm text-candlelight-400 hover:text-candlelight-300 font-medium"
|
||||
class="text-sm text-primary hover:text-primary font-medium"
|
||||
>
|
||||
Duplicate Series
|
||||
</button>
|
||||
<button
|
||||
@click="deleteSeries(series)"
|
||||
class="text-sm text-ember-400 hover:text-ember-300 font-medium"
|
||||
class="text-sm text-red-600 hover:text-red-700 font-medium"
|
||||
>
|
||||
Delete Series
|
||||
</button>
|
||||
|
|
@ -296,13 +296,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-center py-12 bg-guild-900 rounded-lg shadow">
|
||||
<div v-else class="text-center py-12 bg-elevated rounded-lg shadow">
|
||||
<Icon
|
||||
name="heroicons:squares-2x2"
|
||||
class="w-12 h-12 text-guild-400 mx-auto mb-3"
|
||||
class="w-12 h-12 text-muted mx-auto mb-3"
|
||||
/>
|
||||
<p class="text-guild-400">No event series found</p>
|
||||
<p class="text-sm text-guild-500 mt-2">
|
||||
<p class="text-muted">No event series found</p>
|
||||
<p class="text-sm text-dimmed mt-2">
|
||||
Create events and group them into series to get started
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -311,17 +311,17 @@
|
|||
<!-- Edit Series Modal -->
|
||||
<div
|
||||
v-if="editingSeriesId"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-guild-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
class="bg-elevated rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">Edit Series</h3>
|
||||
<h3 class="text-lg font-semibold text-highlighted">Edit Series</h3>
|
||||
<button
|
||||
@click="cancelEditSeries"
|
||||
class="text-guild-400 hover:text-guild-100"
|
||||
class="text-muted hover:text-default"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
|
|
@ -330,31 +330,31 @@
|
|||
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Series Title</label
|
||||
>
|
||||
<input
|
||||
v-model="editingSeriesData.title"
|
||||
type="text"
|
||||
class="w-full border border-guild-700 bg-guild-900 text-guild-100 placeholder-guild-500 rounded-lg px-4 py-2 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
placeholder="e.g., Co-op Game Dev Workshop Series"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Description</label
|
||||
>
|
||||
<textarea
|
||||
v-model="editingSeriesData.description"
|
||||
rows="3"
|
||||
class="w-full border border-guild-700 bg-guild-900 text-guild-100 placeholder-guild-500 rounded-lg px-4 py-2 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="w-full border border-default bg-elevated text-default placeholder-dimmed rounded-lg px-4 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
placeholder="Brief description of this series"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Series Type</label
|
||||
>
|
||||
<USelect
|
||||
|
|
@ -370,7 +370,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Total Events (optional)</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -383,16 +383,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-guild-700 flex justify-end gap-3">
|
||||
<div class="px-6 py-4 border-t border-default flex justify-end gap-3">
|
||||
<button
|
||||
@click="cancelEditSeries"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
class="px-4 py-2 text-muted hover:text-default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveSeriesEdit"
|
||||
class="px-4 py-2 bg-earth-600 text-white rounded-lg hover:bg-earth-700"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
|
|
@ -403,19 +403,19 @@
|
|||
<!-- Bulk Operations Modal -->
|
||||
<div
|
||||
v-if="showBulkModal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-guild-900 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
class="bg-elevated rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Bulk Series Operations
|
||||
</h3>
|
||||
<button
|
||||
@click="showBulkModal = false"
|
||||
class="text-guild-400 hover:text-guild-100"
|
||||
class="text-muted hover:text-default"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
|
|
@ -424,24 +424,24 @@
|
|||
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-guild-100 mb-3">
|
||||
<h4 class="text-sm font-medium text-highlighted mb-3">
|
||||
Series Management Tools
|
||||
</h4>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
@click="reorderAllSeries"
|
||||
class="w-full text-left p-3 border border-guild-700 rounded-lg hover:bg-guild-800"
|
||||
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name="heroicons:arrows-up-down"
|
||||
class="w-5 h-5 text-guild-400 mr-3"
|
||||
class="w-5 h-5 text-muted mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-guild-100">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
Auto-Reorder Series
|
||||
</p>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Fix position numbers based on event dates
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -450,18 +450,18 @@
|
|||
|
||||
<button
|
||||
@click="validateAllSeries"
|
||||
class="w-full text-left p-3 border border-guild-700 rounded-lg hover:bg-guild-800"
|
||||
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name="heroicons:check-circle"
|
||||
class="w-5 h-5 text-guild-400 mr-3"
|
||||
class="w-5 h-5 text-muted mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-guild-100">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
Validate Series Data
|
||||
</p>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Check for consistency issues
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -470,18 +470,18 @@
|
|||
|
||||
<button
|
||||
@click="exportSeriesData"
|
||||
class="w-full text-left p-3 border border-guild-700 rounded-lg hover:bg-guild-800"
|
||||
class="w-full text-left p-3 border border-default rounded-lg hover:bg-muted"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name="heroicons:document-arrow-down"
|
||||
class="w-5 h-5 text-guild-400 mr-3"
|
||||
class="w-5 h-5 text-muted mr-3"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-guild-100">
|
||||
<p class="text-sm font-medium text-highlighted">
|
||||
Export Series Data
|
||||
</p>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Download series information as JSON
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -491,10 +491,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-guild-700 flex justify-end">
|
||||
<div class="px-6 py-4 border-t border-default flex justify-end">
|
||||
<button
|
||||
@click="showBulkModal = false"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
class="px-4 py-2 text-muted hover:text-default"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
|
@ -505,22 +505,22 @@
|
|||
<!-- Series Ticketing Modal -->
|
||||
<div
|
||||
v-if="editingTicketsSeriesId"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
class="fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-guild-900 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
class="bg-elevated rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div class="px-6 py-4 border-b border-guild-700">
|
||||
<div class="px-6 py-4 border-b border-default">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
||||
<h3 class="text-lg font-semibold text-highlighted">
|
||||
Series Pass Ticketing
|
||||
</h3>
|
||||
<p class="text-sm text-guild-400">{{ editingTicketsData.title }}</p>
|
||||
<p class="text-sm text-muted">{{ editingTicketsData.title }}</p>
|
||||
</div>
|
||||
<button
|
||||
@click="cancelTicketsEdit"
|
||||
class="text-guild-400 hover:text-guild-100"
|
||||
class="text-muted hover:text-default"
|
||||
>
|
||||
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
||||
</button>
|
||||
|
|
@ -529,18 +529,18 @@
|
|||
|
||||
<div class="p-6 space-y-6">
|
||||
<!-- Enable Ticketing Toggle -->
|
||||
<div class="p-4 bg-guild-950 rounded-lg">
|
||||
<div class="p-4 bg-muted rounded-lg">
|
||||
<label class="flex items-start cursor-pointer">
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.enabled"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Enable Series Pass Ticketing</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Allow users to purchase a pass for all events in this series
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -550,7 +550,7 @@
|
|||
<div v-if="editingTicketsData.tickets.enabled" class="space-y-6">
|
||||
<!-- Ticketing Behavior -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-semibold text-guild-100">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Ticketing Behavior
|
||||
</h4>
|
||||
|
||||
|
|
@ -558,13 +558,13 @@
|
|||
<input
|
||||
v-model="editingTicketsData.tickets.requiresSeriesTicket"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Require Series Pass</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Users must buy the series pass; individual event tickets are
|
||||
not available
|
||||
</p>
|
||||
|
|
@ -577,14 +577,14 @@
|
|||
editingTicketsData.tickets.allowIndividualEventTickets
|
||||
"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500 mt-1"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500 mt-1"
|
||||
:disabled="editingTicketsData.tickets.requiresSeriesTicket"
|
||||
/>
|
||||
<div class="ml-3">
|
||||
<span class="text-sm font-medium text-guild-100"
|
||||
<span class="text-sm font-medium text-default"
|
||||
>Allow Individual Event Tickets</span
|
||||
>
|
||||
<p class="text-xs text-guild-500">
|
||||
<p class="text-xs text-dimmed">
|
||||
Users can attend single events without buying the full
|
||||
series pass
|
||||
</p>
|
||||
|
|
@ -593,17 +593,17 @@
|
|||
</div>
|
||||
|
||||
<!-- Member Tickets -->
|
||||
<div class="border border-guild-700 rounded-lg p-4">
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-guild-100">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Member Series Pass
|
||||
</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-guild-400 mr-2">Available</span>
|
||||
<span class="text-xs text-muted mr-2">Available</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.member.available"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -614,7 +614,7 @@
|
|||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Pass Name</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -625,7 +625,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Price (CAD)</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -644,16 +644,16 @@
|
|||
<input
|
||||
v-model="editingTicketsData.tickets.member.isFree"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="ml-1 text-xs text-guild-400">Free</span>
|
||||
<span class="ml-1 text-xs text-muted">Free</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Description</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -667,17 +667,17 @@
|
|||
</div>
|
||||
|
||||
<!-- Public Tickets -->
|
||||
<div class="border border-guild-700 rounded-lg p-4">
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-guild-100">
|
||||
<h4 class="text-sm font-semibold text-highlighted">
|
||||
Public Series Pass
|
||||
</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-guild-400 mr-2">Available</span>
|
||||
<span class="text-xs text-muted mr-2">Available</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.public.available"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -688,7 +688,7 @@
|
|||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Pass Name</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -699,7 +699,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Price (CAD)</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -714,7 +714,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Description</label
|
||||
>
|
||||
<UTextarea
|
||||
|
|
@ -727,7 +727,7 @@
|
|||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Quantity Available</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -739,7 +739,7 @@
|
|||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{ editingTicketsData.tickets.public.sold || 0 }} sold,
|
||||
{{ editingTicketsData.tickets.public.reserved || 0 }}
|
||||
reserved
|
||||
|
|
@ -747,7 +747,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Price (Optional)</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -766,7 +766,7 @@
|
|||
<div
|
||||
v-if="editingTicketsData.tickets.public.earlyBirdPrice > 0"
|
||||
>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Early Bird Deadline</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -776,7 +776,7 @@
|
|||
type="datetime-local"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Price increases to ${{
|
||||
editingTicketsData.tickets.public.price
|
||||
}}
|
||||
|
|
@ -787,14 +787,14 @@
|
|||
</div>
|
||||
|
||||
<!-- Capacity Management -->
|
||||
<div class="border border-guild-700 rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-guild-100 mb-4">
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-highlighted mb-4">
|
||||
Capacity Management
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Total Capacity</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -804,13 +804,13 @@
|
|||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Maximum series pass holders across all types
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Currently Reserved</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -820,9 +820,9 @@
|
|||
type="number"
|
||||
min="0"
|
||||
disabled
|
||||
class="w-full bg-guild-800"
|
||||
class="w-full bg-accented"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
Auto-calculated during checkout
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -830,21 +830,21 @@
|
|||
</div>
|
||||
|
||||
<!-- Waitlist Configuration -->
|
||||
<div class="border border-guild-700 rounded-lg p-4">
|
||||
<div class="border border-default rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-sm font-semibold text-guild-100">Waitlist</h4>
|
||||
<h4 class="text-sm font-semibold text-highlighted">Waitlist</h4>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<span class="text-xs text-guild-400 mr-2">Enable Waitlist</span>
|
||||
<span class="text-xs text-muted mr-2">Enable Waitlist</span>
|
||||
<input
|
||||
v-model="editingTicketsData.tickets.waitlist.enabled"
|
||||
type="checkbox"
|
||||
class="rounded border-guild-700 text-candlelight-500 focus:ring-candlelight-500"
|
||||
class="rounded border-default text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="editingTicketsData.tickets.waitlist.enabled">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2"
|
||||
<label class="block text-sm font-medium text-default mb-2"
|
||||
>Max Waitlist Size</label
|
||||
>
|
||||
<UInput
|
||||
|
|
@ -854,7 +854,7 @@
|
|||
placeholder="Leave blank for unlimited"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-guild-500 mt-1">
|
||||
<p class="text-xs text-dimmed mt-1">
|
||||
{{ editingTicketsData.tickets.waitlist.entries?.length || 0 }}
|
||||
people currently on waitlist
|
||||
</p>
|
||||
|
|
@ -863,16 +863,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-guild-700 flex justify-end gap-3">
|
||||
<div class="px-6 py-4 border-t border-default flex justify-end gap-3">
|
||||
<button
|
||||
@click="cancelTicketsEdit"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100"
|
||||
class="px-4 py-2 text-muted hover:text-default"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="saveTicketsEdit"
|
||||
class="px-4 py-2 bg-candlelight-600 text-white rounded-lg hover:bg-candlelight-700"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Save Ticketing Settings
|
||||
</button>
|
||||
|
|
@ -986,12 +986,12 @@ const formatSeriesType = (type) => {
|
|||
|
||||
const getSeriesTypeBadgeClass = (type) => {
|
||||
const classes = {
|
||||
workshop_series: "bg-candlelight-900/20 text-candlelight-400",
|
||||
recurring_meetup: "bg-candlelight-900/20 text-candlelight-400",
|
||||
multi_day: "bg-earth-900/20 text-earth-400",
|
||||
course: "bg-candlelight-900/20 text-candlelight-400",
|
||||
workshop_series: "bg-emerald-100 text-emerald-700",
|
||||
recurring_meetup: "bg-blue-100 text-blue-700",
|
||||
multi_day: "bg-purple-100 text-purple-700",
|
||||
course: "bg-amber-100 text-amber-700",
|
||||
};
|
||||
return classes[type] || "bg-guild-800 text-guild-300";
|
||||
return classes[type] || "bg-gray-100 text-gray-700";
|
||||
};
|
||||
|
||||
const formatEventDate = (date) => {
|
||||
|
|
@ -1029,11 +1029,11 @@ const getEventStatus = (event) => {
|
|||
const getEventStatusClass = (event) => {
|
||||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Ongoing: "bg-candlelight-900/20 text-candlelight-400",
|
||||
Completed: "bg-guild-800 text-guild-300",
|
||||
Upcoming: "bg-blue-100 text-blue-700",
|
||||
Ongoing: "bg-green-100 text-green-700",
|
||||
Completed: "bg-gray-100 text-gray-700",
|
||||
};
|
||||
return classes[status] || "bg-guild-800 text-guild-300";
|
||||
return classes[status] || "bg-gray-100 text-gray-700";
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="bg-guild-900 border-b border-guild-700">
|
||||
<div class="bg-white border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-6">
|
||||
<div class="flex items-center gap-4 mb-2">
|
||||
<NuxtLink to="/admin/series-management" class="text-guild-500 hover:text-guild-100">
|
||||
<NuxtLink to="/admin/series-management" class="text-gray-500 hover:text-gray-700">
|
||||
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
|
||||
</NuxtLink>
|
||||
<h1 class="text-display font-bold text-guild-100">Create New Series</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Create New Series</h1>
|
||||
</div>
|
||||
<p class="text-guild-400">Create a new event series to group related events together</p>
|
||||
<p class="text-gray-600">Create a new event series to group related events together</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Error Summary -->
|
||||
<div v-if="formErrors.length > 0" class="mb-6 p-4 bg-ember-900/20 border border-ember-800 rounded-lg">
|
||||
<div v-if="formErrors.length > 0" class="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 mr-3 mt-0.5" />
|
||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-red-500 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-ember-400 mb-2">Please fix the following errors:</h3>
|
||||
<ul class="text-sm text-ember-400 space-y-1">
|
||||
<h3 class="text-sm font-medium text-red-800 mb-2">Please fix the following errors:</h3>
|
||||
<ul class="text-sm text-red-700 space-y-1">
|
||||
<li v-for="error in formErrors" :key="error">• {{ error }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -29,11 +29,11 @@
|
|||
</div>
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="showSuccessMessage" class="mb-6 p-4 bg-candlelight-900/20 border border-candlelight-800 rounded-lg">
|
||||
<div v-if="showSuccessMessage" class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400 mr-3 mt-0.5" />
|
||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-green-500 mr-3 mt-0.5" />
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-candlelight-400">Series created successfully!</h3>
|
||||
<h3 class="text-sm font-medium text-green-800">Series created successfully!</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -41,56 +41,56 @@
|
|||
<form @submit.prevent="createSeries">
|
||||
<!-- Series Information -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">Series Information</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Series Information</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Title <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Series Title <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="seriesForm.title"
|
||||
type="text"
|
||||
placeholder="e.g., Cooperative Game Development Fundamentals"
|
||||
required
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.title }"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.title }"
|
||||
@input="generateSlugFromTitle"
|
||||
/>
|
||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-ember-400">{{ fieldErrors.title }}</p>
|
||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-red-600">{{ fieldErrors.title }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="generatedSlug">
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Generated Series ID</label>
|
||||
<div class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 font-mono text-sm">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Generated Series ID</label>
|
||||
<div class="w-full bg-gray-100 border border-gray-300 rounded-lg px-3 py-2 text-gray-700 font-mono text-sm">
|
||||
{{ generatedSlug }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-guild-500">
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
This unique identifier will be automatically generated from your title
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
||||
Series Description <span class="text-ember-400">*</span>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Series Description <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="seriesForm.description"
|
||||
placeholder="Describe what the series covers and its goals"
|
||||
required
|
||||
rows="4"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.description }"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
:class="{ 'border-red-300 focus:ring-red-500': fieldErrors.description }"
|
||||
></textarea>
|
||||
<p v-if="fieldErrors.description" class="mt-1 text-sm text-ember-400">{{ fieldErrors.description }}</p>
|
||||
<p v-if="fieldErrors.description" class="mt-1 text-sm text-red-600">{{ fieldErrors.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Series Type</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Series Type</label>
|
||||
<select
|
||||
v-model="seriesForm.type"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="workshop_series">Workshop Series</option>
|
||||
<option value="recurring_meetup">Recurring Meetup</option>
|
||||
|
|
@ -101,25 +101,25 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-guild-100 mb-2">Total Events Planned</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Total Events Planned</label>
|
||||
<input
|
||||
v-model.number="seriesForm.totalEvents"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="e.g., 4"
|
||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-earth-500 focus:border-transparent"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
<p class="text-sm text-guild-500 mt-1">How many events will be in this series? (optional)</p>
|
||||
<p class="text-sm text-gray-500 mt-1">How many events will be in this series? (optional)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-between items-center pt-6 border-t border-guild-700">
|
||||
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
|
||||
<NuxtLink
|
||||
to="/admin/series-management"
|
||||
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-900 font-medium"
|
||||
>
|
||||
Cancel
|
||||
</NuxtLink>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
type="button"
|
||||
@click="createAndAddEvent"
|
||||
:disabled="creating"
|
||||
class="px-4 py-2 bg-candlelight-600 text-white rounded-lg hover:bg-candlelight-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ creating ? 'Creating...' : 'Create & Add Event' }}
|
||||
</button>
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
:disabled="creating"
|
||||
class="px-6 py-2 bg-earth-600 text-white rounded-lg hover:bg-earth-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
class="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
{{ creating ? 'Creating...' : 'Create Series' }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,314 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const uid = route.query.uid as string;
|
||||
|
||||
const email = ref("");
|
||||
const sent = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
async function sendMagicLink() {
|
||||
if (!email.value || !uid) return;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
await $fetch("/oidc/interaction/login", {
|
||||
method: "POST",
|
||||
body: { email: email.value, uid },
|
||||
});
|
||||
sent.value = true;
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.statusMessage || "Something went wrong. Please try again.";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wiki-login">
|
||||
<div class="wiki-login-card">
|
||||
<div class="wiki-login-header">
|
||||
<span class="wiki-login-overline">Ghost Guild</span>
|
||||
<h1 class="wiki-login-title">Wiki</h1>
|
||||
</div>
|
||||
|
||||
<div class="wiki-login-divider" />
|
||||
|
||||
<Transition name="wiki-fade" mode="out-in">
|
||||
<form v-if="!sent" key="form" @submit.prevent="sendMagicLink" class="wiki-login-form">
|
||||
<label for="email" class="wiki-login-label">Email address</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="you@example.com"
|
||||
class="wiki-login-input"
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<p v-if="error" class="wiki-login-error">{{ error }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading || !email"
|
||||
class="wiki-login-button"
|
||||
>
|
||||
<span v-if="loading" class="wiki-login-spinner" />
|
||||
{{ loading ? "Sending" : "Continue" }}
|
||||
</button>
|
||||
|
||||
<p class="wiki-login-hint">
|
||||
We'll email you a sign-in link. No password needed.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div v-else key="sent" class="wiki-login-sent">
|
||||
<p class="wiki-login-sent-heading">Check your inbox</p>
|
||||
<p class="wiki-login-sent-detail">
|
||||
A sign-in link was sent to <strong>{{ email }}</strong>
|
||||
</p>
|
||||
<button
|
||||
@click="sent = false; email = '';"
|
||||
class="wiki-login-link"
|
||||
>
|
||||
Try a different email
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wiki-login {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 1.5rem;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 70%, rgba(184, 135, 58, 0.06) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 70% 30%, rgba(178, 104, 64, 0.04) 0%, transparent 60%),
|
||||
var(--color-guild-900);
|
||||
}
|
||||
|
||||
.dark .wiki-login {
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 70%, rgba(224, 184, 110, 0.05) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 70% 30%, rgba(218, 154, 114, 0.03) 0%, transparent 60%),
|
||||
var(--color-guild-900);
|
||||
}
|
||||
|
||||
.wiki-login-card {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
padding: 2.5rem 2rem 2rem;
|
||||
background: var(--color-guild-800);
|
||||
border: 1px solid var(--color-guild-700);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.06),
|
||||
0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.dark .wiki-login-card {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.2),
|
||||
0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.wiki-login-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wiki-login-overline {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-guild-400);
|
||||
}
|
||||
|
||||
.wiki-login-title {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-candlelight-400);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.wiki-login-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--color-guild-600),
|
||||
transparent
|
||||
);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.wiki-login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wiki-login-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-guild-300);
|
||||
}
|
||||
|
||||
.wiki-login-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-guild-100);
|
||||
background: var(--color-guild-900);
|
||||
border: 1px solid var(--color-guild-600);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.wiki-login-input::placeholder {
|
||||
color: var(--color-guild-500);
|
||||
}
|
||||
|
||||
.wiki-login-input:focus {
|
||||
border-color: var(--color-candlelight-500);
|
||||
box-shadow: 0 0 0 3px rgba(184, 135, 58, 0.15);
|
||||
}
|
||||
|
||||
.wiki-login-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wiki-login-error {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-ember-400);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wiki-login-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-guild-50);
|
||||
background: var(--color-candlelight-500);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.wiki-login-button:hover:not(:disabled) {
|
||||
background: var(--color-candlelight-400);
|
||||
}
|
||||
|
||||
.wiki-login-button:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.wiki-login-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.wiki-login-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: wiki-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wiki-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.wiki-login-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-guild-500);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wiki-login-sent {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.wiki-login-sent-heading {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-guild-100);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wiki-login-sent-detail {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-guild-400);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wiki-login-sent-detail strong {
|
||||
color: var(--color-guild-200);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wiki-login-link {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-candlelight-500);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-top: 0.5rem;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.wiki-login-link:hover {
|
||||
color: var(--color-candlelight-400);
|
||||
}
|
||||
|
||||
/* Transition */
|
||||
.wiki-fade-enter-active,
|
||||
.wiki-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.wiki-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.wiki-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,84 +1,9 @@
|
|||
<template>
|
||||
<div class="min-h-screen w-full flex flex-col items-center justify-center px-4">
|
||||
<h1 class="text-display-xl font-bold mb-2 uppercase font-sans!">Ghost Guild</h1>
|
||||
<p
|
||||
v-if="!isAuthenticated"
|
||||
class="text-display-sm text-guild-400 mb-10 uppercase py-4 text-center font-sans!">
|
||||
Coming Soon
|
||||
</p>
|
||||
|
||||
<!-- Logged-in state -->
|
||||
<div v-if="isAuthenticated" class="w-full max-w-sm flex flex-col items-center space-y-4 text-center mt-8">
|
||||
<p class="text-guild-200 font-sans py-4 text-center">
|
||||
Welcome, <strong class="text-guild-100">{{ memberData.name || memberData.email }}</strong>
|
||||
</p>
|
||||
<a
|
||||
href="https://wiki.ghostguild.org"
|
||||
class="block w-full py-3 px-6 bg-candlelight-500 hover:bg-candlelight-600 text-guild-900 font-semibold rounded-full uppercase tracking-wide transition-colors font-sans text-center">
|
||||
Go to Wiki
|
||||
<div class="min-h-screen w-full flex items-center justify-center">
|
||||
<a href="https://babyghosts.fund/ghost-guild" class="text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-4">Ghost Guild</h1>
|
||||
<p class="text-xl md:text-2xl">Coming Soon</p>
|
||||
</a>
|
||||
<button
|
||||
class="block w-full text-sm text-candlelight-400 hover:text-candlelight-300 font-medium uppercase tracking-wide transition-colors"
|
||||
@click="handleLogout">
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Login form -->
|
||||
<div v-else class="w-full max-w-sm">
|
||||
<!-- Success state -->
|
||||
<div v-if="loginSuccess" class="text-center py-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-candlelight-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<UIcon name="i-heroicons-check-circle" class="w-10 h-10 text-candlelight-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-guild-100 mb-2">
|
||||
Check your email
|
||||
</h3>
|
||||
<p class="text-guild-300">
|
||||
We've sent a magic link to
|
||||
<strong class="text-guild-100">{{ email }}</strong>.
|
||||
Click the link to sign in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<UForm v-else :state="{ email }" @submit="handleLogin">
|
||||
<UFormField name="email" required class="mb-4">
|
||||
<UInput
|
||||
v-model="email"
|
||||
type="email"
|
||||
size="lg"
|
||||
class="w-full"
|
||||
placeholder="your.email@example.com" />
|
||||
</UFormField>
|
||||
|
||||
<div v-if="loginError" class="mb-4 p-3 bg-ember-500/10 border border-ember-500/30 rounded-lg">
|
||||
<p class="text-ember-400 text-sm">{{ loginError }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<UButton
|
||||
type="submit"
|
||||
:loading="isLoggingIn"
|
||||
:disabled="!isFormValid"
|
||||
size="lg"
|
||||
class="rounded-full uppercase tracking-wide font-semibold whitespace-nowrap">
|
||||
Send Magic Link
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="text-center pt-6 border-t border-guild-700 mt-6">
|
||||
<p class="text-guild-400 text-sm">
|
||||
<a
|
||||
href="https://babyghosts.fund/ghost-guild/"
|
||||
class="text-candlelight-400 hover:text-candlelight-300 font-medium uppercase tracking-wide">
|
||||
Pre-Register
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</UForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -86,44 +11,4 @@
|
|||
definePageMeta({
|
||||
layout: "coming-soon",
|
||||
});
|
||||
|
||||
const { isAuthenticated, memberData, checkMemberStatus, logout } = useAuth();
|
||||
|
||||
const email = ref("");
|
||||
const isLoggingIn = ref(false);
|
||||
const loginSuccess = ref(false);
|
||||
const loginError = ref("");
|
||||
|
||||
const isFormValid = computed(() => email.value && email.value.includes("@"));
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (isLoggingIn.value) return;
|
||||
|
||||
isLoggingIn.value = true;
|
||||
loginError.value = "";
|
||||
|
||||
try {
|
||||
const response = await $fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: { email: email.value },
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loginSuccess.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.statusCode === 500) {
|
||||
loginError.value = "Failed to send login email. Please try again later.";
|
||||
} else {
|
||||
loginError.value =
|
||||
err.statusMessage || "Something went wrong. Please try again.";
|
||||
}
|
||||
} finally {
|
||||
isLoggingIn.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"
|
||||
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-guild-200">Loading event details...</p>
|
||||
</div>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
<p class="text-guild-300 mb-6">
|
||||
The event you're looking for doesn't exist.
|
||||
</p>
|
||||
<NuxtLink to="/events" class="text-candlelight-500 hover:underline">
|
||||
<NuxtLink to="/events" class="text-blue-400 hover:underline">
|
||||
← Back to Events
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<!-- Members Only Banner -->
|
||||
<div
|
||||
v-if="event.membersOnly"
|
||||
class="absolute top-0 left-0 right-0 z-10 bg-candlelight-500/95 backdrop-blur-sm py-2"
|
||||
class="absolute top-0 left-0 right-0 z-10 bg-purple-600/95 backdrop-blur-sm py-2"
|
||||
>
|
||||
<UContainer>
|
||||
<div class="flex items-center justify-center">
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
<div class="absolute inset-0 flex items-center">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl">
|
||||
<h1 class="text-display-xl font-bold text-white mb-4">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
{{ event.title }}
|
||||
</h1>
|
||||
</div>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Page Header (fallback when no image) -->
|
||||
<PageHeader v-else :title="event.title" size="medium" />
|
||||
<PageHeader v-else :title="event.title" theme="blue" size="medium" />
|
||||
|
||||
<!-- Event Details Section -->
|
||||
<section class="py-16 bg-guild-900">
|
||||
|
|
@ -81,21 +81,21 @@
|
|||
<div class="mb-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p class="text-ui-label text-guild-400">Date</p>
|
||||
<p class="text-ui-mono font-semibold text-guild-100">
|
||||
<p class="text-sm text-guild-400">Date</p>
|
||||
<p class="font-semibold text-guild-100">
|
||||
{{ formatDate(event.startDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-ui-label text-guild-400">Time</p>
|
||||
<p class="text-ui-mono font-semibold text-guild-100">
|
||||
<p class="text-sm text-guild-400">Time</p>
|
||||
<p class="font-semibold text-guild-100">
|
||||
{{ formatTime(event.startDate, event.endDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-ui-label text-guild-400">Location</p>
|
||||
<p class="text-sm text-guild-400">Location</p>
|
||||
<p class="font-semibold text-guild-100">
|
||||
{{ event.location }}
|
||||
</p>
|
||||
|
|
@ -119,14 +119,14 @@
|
|||
|
||||
<!-- Event Cancelled Notice -->
|
||||
<div v-if="event.isCancelled" class="mb-8">
|
||||
<div class="p-6 bg-ember-900/20 rounded-xl border border-ember-800">
|
||||
<h3 class="text-lg font-semibold text-ember-300 mb-2">
|
||||
<div class="p-6 bg-red-900/20 rounded-xl border border-red-800">
|
||||
<h3 class="text-lg font-semibold text-red-300 mb-2">
|
||||
Event Cancelled
|
||||
</h3>
|
||||
<p class="text-ember-400" v-if="event.cancellationMessage">
|
||||
<p class="text-red-400" v-if="event.cancellationMessage">
|
||||
{{ event.cancellationMessage }}
|
||||
</p>
|
||||
<p class="text-ember-400" v-else>
|
||||
<p class="text-red-400" v-else>
|
||||
This event has been cancelled. We apologize for any
|
||||
inconvenience.
|
||||
</p>
|
||||
|
|
@ -150,14 +150,14 @@
|
|||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
class="text-sm font-medium text-guild-200"
|
||||
class="text-sm font-medium text-gray-800 dark:text-guild-200"
|
||||
>Recommended for:</span
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="circle in event.targetCircles"
|
||||
:key="circle"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-candlelight-900/20 dark:bg-candlelight-900/30 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700 dark:border-candlelight-800/50"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border border-blue-300 dark:border-blue-800/50"
|
||||
>
|
||||
{{ formatCircleName(circle) }}
|
||||
</span>
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
|
||||
<!-- Event Description -->
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none mb-12">
|
||||
<h2 class="text-display-sm font-bold text-guild-100 mb-4">
|
||||
<h2 class="text-2xl font-bold text-guild-100 mb-4">
|
||||
About This Event
|
||||
</h2>
|
||||
|
||||
|
|
@ -191,7 +191,7 @@
|
|||
</p>
|
||||
|
||||
<div v-if="event.agenda && event.agenda.length > 0" class="mt-8">
|
||||
<h3 class="text-display-sm font-semibold text-guild-100 mb-4">
|
||||
<h3 class="text-xl font-semibold text-guild-100 mb-4">
|
||||
Event Agenda
|
||||
</h3>
|
||||
<ul class="space-y-3">
|
||||
|
|
@ -201,7 +201,7 @@
|
|||
class="flex items-start"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-6 h-6 bg-candlelight-500 text-white text-sm rounded-full flex items-center justify-center mr-3 mt-0.5"
|
||||
class="inline-block w-6 h-6 bg-blue-500 text-white text-sm rounded-full flex items-center justify-center mr-3 mt-0.5"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
|
|
@ -214,7 +214,7 @@
|
|||
v-if="event.speakers && event.speakers.length > 0"
|
||||
class="mt-8"
|
||||
>
|
||||
<h3 class="text-display-sm font-semibold text-guild-100 mb-4">
|
||||
<h3 class="text-xl font-semibold text-guild-100 mb-4">
|
||||
Speakers
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
|
@ -257,18 +257,18 @@
|
|||
<!-- Already Registered Status -->
|
||||
<div v-if="registrationStatus === 'registered'">
|
||||
<div
|
||||
class="p-4 bg-candlelight-900/20 dark:bg-candlelight-900/20 rounded-lg border border-candlelight-700 dark:border-candlelight-800 mb-6"
|
||||
class="p-4 bg-green-100 dark:bg-green-900/20 rounded-lg border border-green-400 dark:border-green-800 mb-6"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:items-start md:justify-between gap-4"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="font-semibold text-candlelight-500 dark:text-candlelight-400"
|
||||
class="font-semibold text-green-800 dark:text-green-300"
|
||||
>
|
||||
You're registered!
|
||||
</p>
|
||||
<p class="text-sm text-candlelight-600 dark:text-candlelight-500">
|
||||
<p class="text-sm text-green-700 dark:text-green-400">
|
||||
We've sent a confirmation to your email
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -324,7 +324,7 @@
|
|||
v-else-if="isCancelled"
|
||||
to="/member/profile#account"
|
||||
>
|
||||
<UButton color="primary" size="lg" class="px-8">
|
||||
<UButton color="blue" size="lg" class="px-8">
|
||||
Reactivate Membership
|
||||
</UButton>
|
||||
</NuxtLink>
|
||||
|
|
@ -383,7 +383,7 @@
|
|||
|
||||
<!-- Not Logged In - Show Registration Form -->
|
||||
<div v-else>
|
||||
<h3 class="text-display-sm font-bold text-guild-100 mb-6">
|
||||
<h3 class="text-xl font-bold text-guild-100 mb-6">
|
||||
Register for This Event
|
||||
</h3>
|
||||
<form @submit.prevent="handleRegistration" class="space-y-4">
|
||||
|
|
@ -465,7 +465,7 @@
|
|||
class="w-24 h-2 bg-guild-700 rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-candlelight-500 rounded-full"
|
||||
class="h-full bg-blue-500 rounded-full"
|
||||
:style="`width: ${((event.registeredCount || 0) / event.maxAttendees) * 100}%`"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -541,7 +541,7 @@
|
|||
</p>
|
||||
<a
|
||||
href="mailto:events@ghostguild.org"
|
||||
class="text-candlelight-500 hover:underline"
|
||||
class="text-blue-400 hover:underline"
|
||||
>
|
||||
events@ghostguild.org
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -129,10 +129,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-display font-bold text-guild-100">
|
||||
<div class="text-2xl font-bold text-guild-100">
|
||||
{{ event.start.getDate() }}
|
||||
</div>
|
||||
<div class="text-ui-label text-guild-400">
|
||||
<div class="text-xs text-guild-400 uppercase">
|
||||
{{
|
||||
event.start.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
|
|
@ -151,7 +151,7 @@
|
|||
<Icon
|
||||
v-if="event.membersOnly"
|
||||
name="heroicons:lock-closed"
|
||||
class="w-4 h-4 text-candlelight-500 flex-shrink-0 mt-1"
|
||||
class="w-4 h-4 text-purple-500 flex-shrink-0 mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
|
||||
<!-- Event Series -->
|
||||
<div v-if="activeSeries.length > 0" class="text-center my-12">
|
||||
<h2 class="text-display font-bold text-guild-100 mb-8">
|
||||
<h2 class="text-3xl font-bold text-guild-100 mb-8">
|
||||
Current Event Series
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -261,10 +261,10 @@
|
|||
:class="[
|
||||
'series-list-item__status inline-flex items-center px-2 py-1 rounded-full text-xs font-medium',
|
||||
series.status === 'active'
|
||||
? 'bg-candlelight-900/20 text-candlelight-500 dark:bg-candlelight-900/30 dark:text-candlelight-400'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: series.status === 'upcoming'
|
||||
? 'bg-guild-800 text-guild-300 dark:bg-guild-700/30 dark:text-guild-300'
|
||||
: 'bg-guild-800 text-guild-400 dark:bg-guild-700/30 dark:text-guild-400',
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400',
|
||||
]"
|
||||
>
|
||||
{{ series.status }}
|
||||
|
|
@ -326,7 +326,7 @@
|
|||
<section class="py-20 bg-guild-800 dark:bg-guild-900">
|
||||
<UContainer>
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-display font-bold text-guild-100 mb-8">
|
||||
<h2 class="text-3xl font-bold text-guild-100 mb-8">
|
||||
Attend Our Events
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -613,18 +613,18 @@ const formatSeriesType = (type) => {
|
|||
const getSeriesTypeBadgeClass = (type) => {
|
||||
const classes = {
|
||||
workshop_series:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:bg-candlelight-900/30 dark:text-candlelight-400",
|
||||
"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
|
||||
recurring_meetup:
|
||||
"bg-guild-800 text-guild-300 dark:bg-guild-700/30 dark:text-guild-300",
|
||||
"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
multi_day:
|
||||
"bg-ember-900/20 text-ember-500 dark:bg-ember-900/30 dark:text-ember-400",
|
||||
"bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400",
|
||||
course:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:bg-candlelight-900/30 dark:text-candlelight-400",
|
||||
tournament: "bg-ember-900/20 text-ember-500 dark:bg-ember-900/30 dark:text-ember-400",
|
||||
"bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
|
||||
tournament: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
||||
};
|
||||
return (
|
||||
classes[type] ||
|
||||
"bg-guild-800 text-guild-400 dark:bg-guild-700/30 dark:text-guild-400"
|
||||
"bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400"
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
|
@ -661,27 +661,27 @@ const getSeriesTypeBadgeClass = (type) => {
|
|||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__event.event-community) {
|
||||
background-color: var(--color-guild-400);
|
||||
background-color: #2563eb;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-guild-500);
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__event.event-workshop) {
|
||||
background-color: var(--color-candlelight-500);
|
||||
background-color: #059669;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-candlelight-600);
|
||||
border-color: #047857;
|
||||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__event.event-social) {
|
||||
background-color: var(--color-guild-400);
|
||||
background-color: #7c3aed;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-guild-500);
|
||||
border-color: #6d28d9;
|
||||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__event.event-showcase) {
|
||||
background-color: var(--color-ember-400);
|
||||
background-color: #d97706;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-ember-500);
|
||||
border-color: #b45309;
|
||||
}
|
||||
|
||||
#event-calendar {
|
||||
|
|
@ -698,9 +698,9 @@ const getSeriesTypeBadgeClass = (type) => {
|
|||
|
||||
.month-view .vuecal__cell--today,
|
||||
.vuecal__cell--today {
|
||||
background-color: rgba(184, 135, 58, 0.15);
|
||||
background-color: rgba(59, 130, 246, 0.15);
|
||||
color: #f5f5f4;
|
||||
border: 2px solid var(--color-candlelight-500) !important;
|
||||
border: 2px solid #3b82f6 !important;
|
||||
|
||||
.day-of-month {
|
||||
display: flex;
|
||||
|
|
@ -714,7 +714,7 @@ const getSeriesTypeBadgeClass = (type) => {
|
|||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
display: none;
|
||||
color: var(--color-candlelight-500);
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
|
@ -775,27 +775,27 @@ const getSeriesTypeBadgeClass = (type) => {
|
|||
}
|
||||
|
||||
&.event-community {
|
||||
background-color: var(--color-guild-400);
|
||||
background-color: #2563eb;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-guild-500);
|
||||
border-color: #1d4ed8;
|
||||
}
|
||||
|
||||
&.event-workshop {
|
||||
background-color: var(--color-candlelight-500);
|
||||
background-color: #059669;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-candlelight-600);
|
||||
border-color: #047857;
|
||||
}
|
||||
|
||||
&.event-social {
|
||||
background-color: var(--color-guild-400);
|
||||
background-color: #7c3aed;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-guild-500);
|
||||
border-color: #6d28d9;
|
||||
}
|
||||
|
||||
&.event-showcase {
|
||||
background-color: var(--color-ember-400);
|
||||
background-color: #d97706;
|
||||
color: #f5f5f4;
|
||||
border-color: var(--color-ember-500);
|
||||
border-color: #b45309;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -882,8 +882,8 @@ const getSeriesTypeBadgeClass = (type) => {
|
|||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__cell--today) {
|
||||
background-color: rgba(184, 135, 58, 0.1);
|
||||
border: 2px solid var(--color-candlelight-500);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 2px solid #3b82f6;
|
||||
}
|
||||
|
||||
.guild-calendar :deep(.vuecal__cell--out-of-scope) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div class="max-w-6xl mx-auto px-6 md:px-8">
|
||||
<!-- Hero Section -->
|
||||
<section class="py-16 md:py-24 ink-grain">
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="max-w-2xl">
|
||||
<h1
|
||||
class="text-display-xl font-light text-guild-100 leading-tight mb-2"
|
||||
class="text-4xl md:text-5xl font-light text-guild-100 leading-tight mb-2"
|
||||
>
|
||||
Build your co-op studio
|
||||
</h1>
|
||||
<p
|
||||
class="text-display-xl font-light text-guild-500 leading-tight mb-8"
|
||||
class="text-4xl md:text-5xl font-light text-guild-500 leading-tight mb-8"
|
||||
>
|
||||
with people who get it.
|
||||
</p>
|
||||
|
|
@ -54,27 +54,25 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="submitError"
|
||||
class="mt-4 p-3 bg-ember-900/20 border border-ember-500/30 rounded-lg"
|
||||
class="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"
|
||||
>
|
||||
<p class="text-ember-400 text-sm">{{ submitError }}</p>
|
||||
<p class="text-red-400 text-sm">{{ submitError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<GuildDivider variant="woodcut" />
|
||||
|
||||
<!-- Value Props Section -->
|
||||
<section class="py-16">
|
||||
<section class="py-16 border-t border-guild-800">
|
||||
<div class="grid md:grid-cols-3 gap-8 md:gap-12">
|
||||
<div>
|
||||
<p class="text-ui-label text-candlelight-400 mb-3">Peer Support</p>
|
||||
<p class="text-sm font-medium text-primary-400 mb-3">Peer Support</p>
|
||||
<p class="text-guild-400 leading-relaxed">
|
||||
Connect with founders at your stage and practitioners who've been
|
||||
there. Real conversations, real help.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-ui-label text-candlelight-400 mb-3">
|
||||
<p class="text-sm font-medium text-primary-400 mb-3">
|
||||
Shared Knowledge
|
||||
</p>
|
||||
<p class="text-guild-400 leading-relaxed">
|
||||
|
|
@ -83,7 +81,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-ui-label text-candlelight-400 mb-3">
|
||||
<p class="text-sm font-medium text-primary-400 mb-3">
|
||||
Solidarity Economics
|
||||
</p>
|
||||
<p class="text-guild-400 leading-relaxed">
|
||||
|
|
@ -94,11 +92,9 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<GuildDivider variant="woodcut" />
|
||||
|
||||
<!-- Circles Section -->
|
||||
<section class="py-16">
|
||||
<p class="text-ui-label text-guild-600 mb-8">Find your people</p>
|
||||
<section class="py-16 border-t border-guild-800">
|
||||
<p class="text-sm text-guild-600 mb-8">Find your people</p>
|
||||
|
||||
<div class="space-y-4 mb-8">
|
||||
<NuxtLink
|
||||
|
|
@ -124,12 +120,10 @@
|
|||
</p>
|
||||
</section>
|
||||
|
||||
<GuildDivider variant="woodcut" />
|
||||
|
||||
<!-- Bottom CTA Section -->
|
||||
<section class="py-24 text-center">
|
||||
<p class="text-ui-label text-guild-600 mb-4">Part of the Baby Ghosts family</p>
|
||||
<h2 class="text-display font-light text-guild-200 mb-8">
|
||||
<section class="py-24 border-t border-guild-800 text-center">
|
||||
<p class="text-sm text-guild-600 mb-4">Part of the Baby Ghosts family</p>
|
||||
<h2 class="text-3xl md:text-4xl font-light text-guild-200 mb-8">
|
||||
Ready to find your people?
|
||||
</h2>
|
||||
<UButton
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@
|
|||
v-if="!isAuthenticated"
|
||||
title="Join Ghost Guild"
|
||||
subtitle=""
|
||||
theme="gray"
|
||||
size="large"
|
||||
/>
|
||||
<PageHeader
|
||||
v-else
|
||||
title="You're Already a Member!"
|
||||
:subtitle="`Welcome back, ${memberData?.name || 'member'}. You're already part of Ghost Guild in the ${memberData?.circle || 'community'} circle.`"
|
||||
theme="gray"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
|
|
@ -18,7 +20,7 @@
|
|||
<section class="py-20 bg-[--ui-bg-elevated]">
|
||||
<UContainer>
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-display font-bold text-[--ui-text] mb-6">
|
||||
<h2 class="text-3xl font-bold text-[--ui-text] mb-6">
|
||||
How Membership Works
|
||||
</h2>
|
||||
<p class="text-lg text-[--ui-text] mb-4">
|
||||
|
|
@ -39,7 +41,7 @@
|
|||
<section v-if="!isAuthenticated" class="py-20 bg-[--ui-bg]">
|
||||
<UContainer class="max-w-4xl">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-display font-bold text-[--ui-text] mb-4">
|
||||
<h2 class="text-3xl font-bold text-[--ui-text] mb-4">
|
||||
Membership Sign Up
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -50,10 +52,10 @@
|
|||
<div class="flex items-center">
|
||||
<div
|
||||
:class="[
|
||||
'w-10 h-10 rounded-full flex items-center justify-center text-ui-mono font-semibold',
|
||||
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||
currentStep >= 1
|
||||
? 'bg-guild-800 text-candlelight-400'
|
||||
: 'bg-guild-700 text-guild-500',
|
||||
? 'bg-neutral-900 text-neutral-50'
|
||||
: 'bg-neutral-200 text-neutral-500',
|
||||
]"
|
||||
>
|
||||
1
|
||||
|
|
@ -61,16 +63,16 @@
|
|||
<span
|
||||
class="ml-2 font-medium"
|
||||
:class="
|
||||
currentStep === 1 ? 'text-[--ui-text]' : 'text-guild-500'
|
||||
currentStep === 1 ? 'text-[--ui-text]' : 'text-neutral-500'
|
||||
"
|
||||
>
|
||||
Information
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="needsPayment" class="w-16 h-1 bg-guild-700">
|
||||
<div v-if="needsPayment" class="w-16 h-1 bg-neutral-200">
|
||||
<div
|
||||
class="h-full bg-candlelight-500 transition-all"
|
||||
class="h-full bg-neutral-900 transition-all"
|
||||
:style="{ width: currentStep >= 2 ? '100%' : '0%' }"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -78,10 +80,10 @@
|
|||
<div v-if="needsPayment" class="flex items-center">
|
||||
<div
|
||||
:class="[
|
||||
'w-10 h-10 rounded-full flex items-center justify-center text-ui-mono font-semibold',
|
||||
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||
currentStep >= 2
|
||||
? 'bg-guild-800 text-candlelight-400'
|
||||
: 'bg-guild-700 text-guild-500',
|
||||
? 'bg-neutral-900 text-neutral-50'
|
||||
: 'bg-neutral-200 text-neutral-500',
|
||||
]"
|
||||
>
|
||||
2
|
||||
|
|
@ -89,16 +91,16 @@
|
|||
<span
|
||||
class="ml-2 font-medium"
|
||||
:class="
|
||||
currentStep === 2 ? 'text-[--ui-text]' : 'text-guild-500'
|
||||
currentStep === 2 ? 'text-[--ui-text]' : 'text-neutral-500'
|
||||
"
|
||||
>
|
||||
Payment
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="w-16 h-1 bg-guild-700">
|
||||
<div class="w-16 h-1 bg-neutral-200">
|
||||
<div
|
||||
class="h-full bg-candlelight-500 transition-all"
|
||||
class="h-full bg-neutral-900 transition-all"
|
||||
:style="{ width: currentStep >= 3 ? '100%' : '0%' }"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -106,10 +108,10 @@
|
|||
<div class="flex items-center">
|
||||
<div
|
||||
:class="[
|
||||
'w-10 h-10 rounded-full flex items-center justify-center text-ui-mono font-semibold',
|
||||
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
|
||||
currentStep >= 3
|
||||
? 'bg-guild-800 text-candlelight-400'
|
||||
: 'bg-guild-700 text-guild-500',
|
||||
? 'bg-neutral-900 text-neutral-50'
|
||||
: 'bg-neutral-200 text-neutral-500',
|
||||
]"
|
||||
>
|
||||
<span v-if="needsPayment">3</span>
|
||||
|
|
@ -118,7 +120,7 @@
|
|||
<span
|
||||
class="ml-2 font-medium"
|
||||
:class="
|
||||
currentStep === 3 ? 'text-[--ui-text]' : 'text-guild-500'
|
||||
currentStep === 3 ? 'text-[--ui-text]' : 'text-neutral-500'
|
||||
"
|
||||
>
|
||||
Confirmation
|
||||
|
|
@ -229,7 +231,7 @@
|
|||
class="bg-[--ui-bg-elevated] rounded-xl p-8"
|
||||
>
|
||||
<div class="mb-6">
|
||||
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
||||
<h3 class="text-2xl font-bold text-[--ui-text] mb-2">
|
||||
Payment Information
|
||||
</h3>
|
||||
<p class="text-[--ui-text-muted]">
|
||||
|
|
@ -271,10 +273,10 @@
|
|||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-20 h-20 bg-candlelight-900/20 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-candlelight-400"
|
||||
class="w-10 h-10 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -288,7 +290,7 @@
|
|||
</svg>
|
||||
</div>
|
||||
|
||||
<h3 class="text-display-sm font-bold text-[--ui-text] mb-4">
|
||||
<h3 class="text-2xl font-bold text-[--ui-text] mb-4">
|
||||
Welcome to Ghost Guild!
|
||||
</h3>
|
||||
|
||||
|
|
@ -351,7 +353,7 @@
|
|||
<section v-if="isAuthenticated" class="py-20 bg-[--ui-bg]">
|
||||
<UContainer class="max-w-4xl">
|
||||
<div class="bg-[--ui-bg-elevated] rounded-xl p-8 mb-8">
|
||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-6">
|
||||
<h2 class="text-2xl font-bold text-[--ui-text] mb-6">
|
||||
Your Membership
|
||||
</h2>
|
||||
|
||||
|
|
@ -385,7 +387,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-guild-800 rounded-lg p-6">
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-[--ui-text] mb-3">
|
||||
Want to change your circle or contribution?
|
||||
</h3>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<script setup>
|
||||
await navigateTo("/coming-soon", { redirectCode: 301 });
|
||||
</script>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
<PageHeader
|
||||
title="Member Dashboard"
|
||||
:subtitle="`Welcome back, ${memberData?.name || 'Member'}!`"
|
||||
theme="blue"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
<template #header>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<h1 class="text-display-lg text-guild-100 warm-text">
|
||||
<h1 class="text-2xl font-bold text-guild-100 warm-text">
|
||||
Welcome to Ghost Guild, {{ memberData?.name }}!
|
||||
</h1>
|
||||
<p
|
||||
|
|
@ -87,13 +88,13 @@
|
|||
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
<div class="bg-guild-800 border border-guild-600 px-4 py-2">
|
||||
<span class="text-ui-label text-guild-200">Circle:</span>
|
||||
<span class="text-guild-200">Circle:</span>
|
||||
<span class="font-medium text-stone-50 ml-1 capitalize">{{
|
||||
memberData?.circle
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="bg-guild-800 border border-guild-600 px-4 py-2">
|
||||
<span class="text-ui-label text-guild-200">Contribution:</span>
|
||||
<span class="text-guild-200">Contribution:</span>
|
||||
<span class="font-medium text-stone-50 ml-1"
|
||||
>${{ memberData?.contributionTier }} CAD/month</span
|
||||
>
|
||||
|
|
@ -110,7 +111,7 @@
|
|||
}"
|
||||
>
|
||||
<template #header>
|
||||
<h2 class="text-display-sm text-guild-100 warm-text">
|
||||
<h2 class="text-xl font-bold text-guild-100 warm-text">
|
||||
Quick Links
|
||||
</h2>
|
||||
</template>
|
||||
|
|
@ -194,7 +195,7 @@
|
|||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-display-sm text-guild-100 warm-text">
|
||||
<h2 class="text-xl font-bold text-guild-100 warm-text">
|
||||
Your Upcoming Events
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -262,7 +263,7 @@
|
|||
<h3 class="font-semibold text-guild-100 mb-1">
|
||||
{{ evt.title }}
|
||||
</h3>
|
||||
<div class="flex items-center gap-4 text-ui-mono text-guild-400">
|
||||
<div class="flex items-center gap-4 text-sm text-guild-400">
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="heroicons:calendar" class="w-4 h-4" />
|
||||
{{ formatEventDate(evt.startDate) }}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PageHeader
|
||||
title="My Updates"
|
||||
subtitle="View and manage your updates"
|
||||
theme="stone"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PageHeader
|
||||
title="Your Profile"
|
||||
subtitle="Manage your profile information and privacy settings"
|
||||
theme="blue"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
|
|
@ -15,9 +16,9 @@
|
|||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-candlelight-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
||||
class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-guild-400">
|
||||
<p class="text-gray-600 dark:text-guild-400">
|
||||
Loading your profile...
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -51,7 +52,7 @@
|
|||
<!-- Basic Information -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-8 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Basic Information
|
||||
</h2>
|
||||
|
|
@ -122,7 +123,7 @@
|
|||
class="relative aspect-square rounded-lg border-2 transition-all hover:scale-105"
|
||||
:class="
|
||||
formData.avatar === ghost.value
|
||||
? 'border-candlelight-400 bg-candlelight-900/20'
|
||||
? 'border-blue-400 bg-blue-500/20'
|
||||
: 'border-guild-700 bg-guild-800/50 hover:border-guild-600'
|
||||
"
|
||||
@click="formData.avatar = ghost.value"
|
||||
|
|
@ -134,7 +135,7 @@
|
|||
/>
|
||||
<span
|
||||
v-if="formData.avatar === ghost.value"
|
||||
class="absolute -top-2 -right-2 w-6 h-6 bg-candlelight-500 rounded-full flex items-center justify-center text-white text-xs"
|
||||
class="absolute -top-2 -right-2 w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
|
|
@ -152,7 +153,7 @@
|
|||
<!-- Professional Info -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-8 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Professional Information
|
||||
</h2>
|
||||
|
|
@ -221,7 +222,7 @@
|
|||
<!-- Community Connections -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-8 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Community Connections
|
||||
</h2>
|
||||
|
|
@ -237,7 +238,7 @@
|
|||
<!-- Tags input -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-guild-200 mb-2"
|
||||
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
|
||||
>
|
||||
Skills & Topics
|
||||
</label>
|
||||
|
|
@ -255,7 +256,7 @@
|
|||
<span
|
||||
v-for="(tag, index) in formData.offering.tags"
|
||||
:key="tag"
|
||||
class="px-3 py-1 bg-guild-800 text-guild-200 rounded-full text-sm border border-guild-600 flex items-center gap-2 group hover:bg-guild-700 transition-colors cursor-pointer"
|
||||
class="px-3 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded-full text-sm border border-blue-300 dark:border-blue-500/30 flex items-center gap-2 group hover:bg-blue-200 dark:hover:bg-blue-500/30 transition-colors cursor-pointer"
|
||||
@click="removeOfferingTag(index)"
|
||||
>
|
||||
{{ tag }}
|
||||
|
|
@ -269,7 +270,7 @@
|
|||
<!-- Description textarea -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-guild-200 mb-2"
|
||||
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
|
||||
>
|
||||
Details
|
||||
</label>
|
||||
|
|
@ -299,7 +300,7 @@
|
|||
<!-- Tags input -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-guild-200 mb-2"
|
||||
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
|
||||
>
|
||||
Skills & Topics
|
||||
</label>
|
||||
|
|
@ -317,7 +318,7 @@
|
|||
<span
|
||||
v-for="(tag, index) in formData.lookingFor.tags"
|
||||
:key="tag"
|
||||
class="px-3 py-1 bg-candlelight-900/20 text-candlelight-500 rounded-full text-sm border border-candlelight-500/30 flex items-center gap-2 group hover:bg-candlelight-900/30 transition-colors cursor-pointer"
|
||||
class="px-3 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full text-sm border border-purple-300 dark:border-purple-500/30 flex items-center gap-2 group hover:bg-purple-200 dark:hover:bg-purple-500/30 transition-colors cursor-pointer"
|
||||
@click="removeLookingForTag(index)"
|
||||
>
|
||||
{{ tag }}
|
||||
|
|
@ -331,7 +332,7 @@
|
|||
<!-- Description textarea -->
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-guild-200 mb-2"
|
||||
class="block text-sm font-medium text-gray-800 dark:text-guild-200 mb-2"
|
||||
>
|
||||
Details
|
||||
</label>
|
||||
|
|
@ -356,7 +357,7 @@
|
|||
<!-- Peer Support -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-8 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Peer Support
|
||||
</h2>
|
||||
|
|
@ -367,12 +368,12 @@
|
|||
<USwitch v-model="formData.peerSupportEnabled" />
|
||||
<div>
|
||||
<p
|
||||
class="font-medium text-guild-200"
|
||||
class="font-medium text-gray-800 dark:text-guild-200"
|
||||
>
|
||||
Offer Peer Support
|
||||
</p>
|
||||
<p
|
||||
class="text-sm text-guild-400 mt-1"
|
||||
class="text-sm text-gray-600 dark:text-guild-400 mt-1"
|
||||
>
|
||||
Make yourself available to support other members
|
||||
</p>
|
||||
|
|
@ -382,7 +383,7 @@
|
|||
<!-- Conditional Fields -->
|
||||
<div
|
||||
v-if="formData.peerSupportEnabled"
|
||||
class="space-y-6 pl-4 border-l-2 border-candlelight-500/30"
|
||||
class="space-y-6 pl-4 border-l-2 border-purple-500/30"
|
||||
>
|
||||
<!-- Skill-Based Topics -->
|
||||
<UFormField
|
||||
|
|
@ -407,7 +408,7 @@
|
|||
topic, index
|
||||
) in formData.peerSupportSkillTopics"
|
||||
:key="topic"
|
||||
class="px-3 py-1 bg-guild-800 text-guild-200 rounded-full text-sm border border-guild-600 flex items-center gap-2 group hover:bg-guild-700 transition-colors cursor-pointer"
|
||||
class="px-3 py-1 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded-full text-sm border border-blue-300 dark:border-blue-500/30 flex items-center gap-2 group hover:bg-blue-200 dark:hover:bg-blue-500/30 transition-colors cursor-pointer"
|
||||
@click="removePeerSkillTopic(index)"
|
||||
>
|
||||
{{ topic }}
|
||||
|
|
@ -416,7 +417,7 @@
|
|||
>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-guild-500 mt-2">
|
||||
<p class="text-xs text-stone-500 mt-2">
|
||||
Suggested from your offerings:
|
||||
<span
|
||||
v-for="tag in formData.offering.tags?.filter(
|
||||
|
|
@ -477,7 +478,7 @@
|
|||
/>
|
||||
<template #hint>
|
||||
<span
|
||||
class="text-xs text-guild-500"
|
||||
class="text-xs text-gray-500 dark:text-guild-500"
|
||||
>
|
||||
{{ formData.peerSupportMessage?.length || 0 }}/200
|
||||
characters
|
||||
|
|
@ -505,7 +506,7 @@
|
|||
<!-- Directory Settings -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-8 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-8 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Directory Visibility
|
||||
</h2>
|
||||
|
|
@ -513,10 +514,10 @@
|
|||
<div class="flex items-start gap-4">
|
||||
<USwitch v-model="formData.showInDirectory" />
|
||||
<div>
|
||||
<p class="font-medium text-guild-200">
|
||||
<p class="font-medium text-gray-800 dark:text-guild-200">
|
||||
Show in Member Directory
|
||||
</p>
|
||||
<p class="text-sm text-guild-400 mt-1">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400 mt-1">
|
||||
Allow other members to discover and connect with you
|
||||
through the directory
|
||||
</p>
|
||||
|
|
@ -527,16 +528,16 @@
|
|||
<!-- Success/Error Messages -->
|
||||
<div
|
||||
v-if="saveSuccess"
|
||||
class="backdrop-blur-sm bg-candlelight-900/20 border border-candlelight-500/30 rounded-lg p-4"
|
||||
class="backdrop-blur-sm bg-green-500/10 border border-green-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-candlelight-500">✓ Profile updated successfully!</p>
|
||||
<p class="text-green-300">✓ Profile updated successfully!</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="saveError"
|
||||
class="backdrop-blur-sm bg-ember-900/20 border border-ember-400/30 rounded-lg p-4"
|
||||
class="backdrop-blur-sm bg-red-500/10 border border-red-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-ember-400">
|
||||
<p class="text-red-300">
|
||||
{{ saveError }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -573,20 +574,20 @@
|
|||
<!-- Current Membership -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-6 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Current Membership
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="backdrop-blur-sm bg-guild-800/50 border border-guild-700 rounded-lg p-6 space-y-4"
|
||||
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6 space-y-4"
|
||||
>
|
||||
<!-- Status Badge -->
|
||||
<div
|
||||
class="flex items-center justify-between pb-4 border-b border-guild-700"
|
||||
class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-guild-700"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400">
|
||||
Membership Status
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
|
|
@ -619,21 +620,21 @@
|
|||
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400">
|
||||
Circle
|
||||
</p>
|
||||
<p
|
||||
class="text-lg font-medium text-guild-100 capitalize"
|
||||
class="text-lg font-medium text-gray-900 dark:text-guild-100 capitalize"
|
||||
>
|
||||
{{ memberData.circle }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400">
|
||||
Contribution Level
|
||||
</p>
|
||||
<p
|
||||
class="text-lg font-medium text-guild-100"
|
||||
class="text-lg font-medium text-gray-900 dark:text-guild-100"
|
||||
>
|
||||
${{ contributionTierDetails?.amount }}/month
|
||||
</p>
|
||||
|
|
@ -641,10 +642,10 @@
|
|||
</div>
|
||||
|
||||
<div v-if="memberData.subscriptionStartDate">
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400">
|
||||
Member Since
|
||||
</p>
|
||||
<p class="text-guild-100">
|
||||
<p class="text-gray-900 dark:text-guild-100">
|
||||
{{ formatDate(memberData.subscriptionStartDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -655,10 +656,10 @@
|
|||
memberData.contributionTier !== '0'
|
||||
"
|
||||
>
|
||||
<p class="text-sm text-guild-400">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400">
|
||||
Next Billing Date
|
||||
</p>
|
||||
<p class="text-guild-100">
|
||||
<p class="text-gray-900 dark:text-guild-100">
|
||||
{{ formatDate(memberData.nextBillingDate) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -668,15 +669,15 @@
|
|||
<!-- Change Contribution Level -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-6 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Change Contribution Level
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="backdrop-blur-sm bg-guild-800/50 border border-guild-700 rounded-lg p-6"
|
||||
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6"
|
||||
>
|
||||
<p class="text-guild-300 mb-6">
|
||||
<p class="text-gray-700 dark:text-guild-300 mb-6">
|
||||
Choose a new contribution level that works for you.
|
||||
Changes will take effect on your next billing cycle.
|
||||
</p>
|
||||
|
|
@ -689,22 +690,22 @@
|
|||
:class="[
|
||||
'w-full text-left p-4 rounded-lg border-2 transition-all',
|
||||
selectedContributionTier === tier.value
|
||||
? 'border-candlelight-400 bg-candlelight-900/20'
|
||||
: 'border-guild-600 bg-guild-900/30 hover:border-guild-500',
|
||||
? 'border-blue-400 bg-blue-500/20'
|
||||
: 'border-gray-300 dark:border-guild-600 bg-gray-50 dark:bg-guild-900/30 hover:border-blue-300 dark:hover:border-guild-500',
|
||||
]"
|
||||
@click="selectedContributionTier = tier.value"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p
|
||||
class="font-medium text-guild-100"
|
||||
class="font-medium text-gray-900 dark:text-guild-100"
|
||||
>
|
||||
{{ tier.label }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedContributionTier === tier.value"
|
||||
class="w-6 h-6 bg-candlelight-500 rounded-full flex items-center justify-center text-white text-xs"
|
||||
class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white text-xs"
|
||||
>
|
||||
✓
|
||||
</div>
|
||||
|
|
@ -714,16 +715,16 @@
|
|||
|
||||
<div
|
||||
v-if="contributionChangeError"
|
||||
class="mb-4 backdrop-blur-sm bg-ember-900/20 border border-ember-400/30 rounded-lg p-4"
|
||||
class="mb-4 backdrop-blur-sm bg-red-500/10 border border-red-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-ember-400">{{ contributionChangeError }}</p>
|
||||
<p class="text-red-300">{{ contributionChangeError }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="contributionChangeSuccess"
|
||||
class="mb-4 backdrop-blur-sm bg-candlelight-900/20 border border-candlelight-500/30 rounded-lg p-4"
|
||||
class="mb-4 backdrop-blur-sm bg-green-500/10 border border-green-500/30 rounded-lg p-4"
|
||||
>
|
||||
<p class="text-candlelight-500">
|
||||
<p class="text-green-300">
|
||||
✓ Contribution level updated successfully!
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -744,20 +745,20 @@
|
|||
<!-- Cancel Membership -->
|
||||
<div>
|
||||
<h2
|
||||
class="text-display-sm mb-6 text-guild-100 warm-text"
|
||||
class="text-2xl font-semibold mb-6 text-gray-900 dark:text-guild-100 warm-text"
|
||||
>
|
||||
Cancel Membership
|
||||
</h2>
|
||||
|
||||
<div
|
||||
class="backdrop-blur-sm bg-guild-800/50 border border-guild-700 rounded-lg p-6"
|
||||
class="backdrop-blur-sm bg-white/80 dark:bg-guild-800/50 border border-gray-200 dark:border-guild-700 rounded-lg p-6"
|
||||
>
|
||||
<p class="text-guild-300 mb-4">
|
||||
<p class="text-gray-700 dark:text-guild-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-guild-400 mb-6">
|
||||
<p class="text-sm text-gray-600 dark:text-guild-400 mb-6">
|
||||
Need a break? Consider switching to the free tier instead.
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PageHeader
|
||||
title="Member Directory"
|
||||
subtitle="Connect with members of the Ghost Guild community"
|
||||
theme="purple"
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
|
|
@ -55,8 +56,8 @@
|
|||
class="px-3 py-1 rounded-full text-sm transition-all border"
|
||||
:class="
|
||||
selectedSkills.includes(skill)
|
||||
? 'bg-candlelight-900/20 text-candlelight-500 border-candlelight-500/50'
|
||||
: 'bg-guild-800/50 text-guild-400 border-guild-700 hover:border-guild-600'
|
||||
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
|
||||
: 'bg-gray-100 dark:bg-guild-800/50 text-gray-700 dark:text-guild-400 border-gray-300 dark:border-guild-700 hover:border-gray-400 dark:hover:border-guild-600'
|
||||
"
|
||||
@click="toggleSkill(skill)"
|
||||
>
|
||||
|
|
@ -93,8 +94,8 @@
|
|||
class="px-3 py-1 rounded-full text-sm transition-all border"
|
||||
:class="
|
||||
selectedTopics.includes(topic)
|
||||
? 'bg-candlelight-900/20 text-candlelight-500 border-candlelight-500/50'
|
||||
: 'bg-guild-800/50 text-guild-400 border-guild-700 hover:border-guild-600'
|
||||
? 'bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-300 dark:border-purple-500/50'
|
||||
: 'bg-gray-100 dark:bg-guild-800/50 text-gray-700 dark:text-guild-400 border-gray-300 dark:border-guild-700 hover:border-gray-400 dark:hover:border-guild-600'
|
||||
"
|
||||
@click="toggleTopic(topic)"
|
||||
>
|
||||
|
|
@ -128,7 +129,7 @@
|
|||
<span class="text-guild-400">Active filters:</span>
|
||||
<span
|
||||
v-if="selectedCircle && selectedCircle !== 'all'"
|
||||
class="px-2 py-1 bg-candlelight-900/20 text-candlelight-500 rounded-full border border-candlelight-500/30 flex items-center gap-1"
|
||||
class="px-2 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full border border-purple-300 dark:border-purple-500/30 flex items-center gap-1"
|
||||
>
|
||||
{{ circleLabels[selectedCircle] }}
|
||||
<button
|
||||
|
|
@ -141,7 +142,7 @@
|
|||
</span>
|
||||
<span
|
||||
v-if="peerSupportFilter && peerSupportFilter !== 'all'"
|
||||
class="px-2 py-1 bg-candlelight-900/20 text-candlelight-500 rounded-full border border-candlelight-500/30 flex items-center gap-1"
|
||||
class="px-2 py-1 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded-full border border-purple-300 dark:border-purple-500/30 flex items-center gap-1"
|
||||
>
|
||||
Offering Peer Support
|
||||
<button
|
||||
|
|
@ -170,7 +171,7 @@
|
|||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-8 h-8 border-4 border-candlelight-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
||||
class="w-8 h-8 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"
|
||||
></div>
|
||||
<p class="text-guild-400">Loading members...</p>
|
||||
</div>
|
||||
|
|
@ -186,7 +187,7 @@
|
|||
<div
|
||||
v-for="member in members"
|
||||
:key="member._id"
|
||||
class="relative backdrop-blur-sm bg-guild-900/50 border border-guild-700/50 rounded-lg p-6 hover:border-candlelight-500/50 transition-all group"
|
||||
class="relative backdrop-blur-sm bg-guild-900/50 border border-guild-700/50 rounded-lg p-6 hover:border-purple-500/50 transition-all group"
|
||||
>
|
||||
<!-- Peer Support Sticker Badge -->
|
||||
<PeerSupportBadge
|
||||
|
|
@ -198,7 +199,7 @@
|
|||
<div class="flex items-start gap-4 mb-4">
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
class="w-16 h-16 rounded-lg bg-guild-800 border border-guild-700 flex items-center justify-center flex-shrink-0 group-hover:border-candlelight-500/50 transition-colors"
|
||||
class="w-16 h-16 rounded-lg bg-guild-800 border border-guild-700 flex items-center justify-center flex-shrink-0 group-hover:border-purple-500/50 transition-colors"
|
||||
>
|
||||
<img
|
||||
v-if="member.avatar"
|
||||
|
|
@ -222,7 +223,7 @@
|
|||
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
:class="['px-2 py-0.5 rounded text-xs', circleBadgeClass(member.circle)]"
|
||||
class="px-2 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded text-xs border border-purple-300 dark:border-purple-500/30"
|
||||
>
|
||||
{{ circleLabels[member.circle] }}
|
||||
</span>
|
||||
|
|
@ -249,10 +250,10 @@
|
|||
<!-- Peer Support Section -->
|
||||
<div
|
||||
v-if="member.peerSupport?.enabled"
|
||||
class="mb-4 p-4 bg-candlelight-900/20 border border-candlelight-500/30 rounded-lg"
|
||||
class="mb-4 p-4 bg-purple-500/10 border border-purple-500/30 rounded-lg"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<p class="text-candlelight-500 font-medium text-sm mb-2">
|
||||
<p class="text-purple-300 font-medium text-sm mb-2">
|
||||
{{ member.name }} offers 1:1 chats on:
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -269,7 +270,7 @@
|
|||
<span
|
||||
v-for="topic in member.peerSupport.topics"
|
||||
:key="topic"
|
||||
class="px-2 py-0.5 bg-candlelight-900/20 text-candlelight-400 rounded text-xs border border-candlelight-500/40"
|
||||
class="px-2 py-0.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-200 rounded text-xs border border-purple-300 dark:border-purple-500/40"
|
||||
>
|
||||
{{ topic }}
|
||||
</span>
|
||||
|
|
@ -294,13 +295,13 @@
|
|||
|
||||
<!-- Contact Section -->
|
||||
<div v-if="member.peerSupport.slackUsername" class="space-y-2">
|
||||
<p class="text-sm text-candlelight-500 font-medium">
|
||||
<p class="text-sm text-purple-300 font-medium">
|
||||
Book a Peer Support call now:
|
||||
</p>
|
||||
<a
|
||||
:href="`slack://user?team=T03A96LV4&id=${member.slackUserId}`"
|
||||
@click.prevent="openSlackDM(member)"
|
||||
class="inline-block px-3 py-1.5 bg-candlelight-900/20 text-candlelight-500 rounded border border-candlelight-500/30 hover:bg-candlelight-900/30 transition-colors text-sm font-medium cursor-pointer"
|
||||
class="inline-block px-3 py-1.5 bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300 rounded border border-purple-300 dark:border-purple-500/30 hover:bg-purple-200 dark:hover:bg-purple-500/30 transition-colors text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Message {{ member.peerSupport.slackUsername }} on Slack
|
||||
</a>
|
||||
|
|
@ -313,14 +314,14 @@
|
|||
class="space-y-4"
|
||||
>
|
||||
<h4
|
||||
class="text-ui-label text-candlelight-500"
|
||||
class="text-sm font-semibold text-purple-300 uppercase tracking-wide"
|
||||
>
|
||||
Skills Exchange
|
||||
</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Offering -->
|
||||
<div v-if="member.offering" class="space-y-2">
|
||||
<h5 class="text-ui-label text-candlelight-400 text-xs">
|
||||
<h5 class="text-xs font-semibold text-purple-400 uppercase">
|
||||
Can share
|
||||
</h5>
|
||||
<p
|
||||
|
|
@ -338,7 +339,7 @@
|
|||
<span
|
||||
v-for="tag in member.offering.tags"
|
||||
:key="tag"
|
||||
class="px-2 py-0.5 bg-candlelight-900/20 text-candlelight-500 rounded text-xs border border-candlelight-500/30"
|
||||
class="px-2 py-0.5 bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300 rounded text-xs border border-green-300 dark:border-green-500/30"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
|
|
@ -347,7 +348,7 @@
|
|||
|
||||
<!-- Looking For -->
|
||||
<div v-if="member.lookingFor" class="space-y-2">
|
||||
<h5 class="text-ui-label text-candlelight-400 text-xs">
|
||||
<h5 class="text-xs font-semibold text-purple-400 uppercase">
|
||||
Looking to learn
|
||||
</h5>
|
||||
<p
|
||||
|
|
@ -366,7 +367,7 @@
|
|||
<span
|
||||
v-for="tag in member.lookingFor.tags"
|
||||
:key="tag"
|
||||
class="px-2 py-0.5 bg-guild-800 text-guild-200 rounded text-xs border border-guild-600"
|
||||
class="px-2 py-0.5 bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300 rounded text-xs border border-blue-300 dark:border-blue-500/30"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
|
|
@ -409,9 +410,9 @@
|
|||
<!-- Not Authenticated Notice -->
|
||||
<div
|
||||
v-if="!isAuthenticated && members.length > 0"
|
||||
class="mt-8 backdrop-blur-sm bg-candlelight-900/20 border border-candlelight-500/30 rounded-lg p-6 text-center"
|
||||
class="mt-8 backdrop-blur-sm bg-purple-500/10 border border-purple-500/30 rounded-lg p-6 text-center"
|
||||
>
|
||||
<p class="text-guild-200 mb-4">
|
||||
<p class="text-purple-200 mb-4">
|
||||
🔒 Some member information is visible to members only
|
||||
</p>
|
||||
<div class="flex gap-3 justify-center">
|
||||
|
|
@ -457,16 +458,6 @@ const circleLabels = {
|
|||
practitioner: "Practitioner",
|
||||
};
|
||||
|
||||
// Map circle names to badge classes
|
||||
const circleBadgeClass = (circle) => {
|
||||
const classes = {
|
||||
community: 'circle-badge-community',
|
||||
founder: 'circle-badge-founder',
|
||||
practitioner: 'circle-badge-practitioner',
|
||||
};
|
||||
return classes[circle] || 'circle-badge-community';
|
||||
};
|
||||
|
||||
// Peer support filter options
|
||||
const peerSupportOptions = [
|
||||
{ label: "All Members", value: "all" },
|
||||
|
|
|
|||
96
app/pages/oidc/login.vue
Normal file
96
app/pages/oidc/login.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const uid = route.query.uid as string;
|
||||
|
||||
const email = ref("");
|
||||
const sent = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
async function sendMagicLink() {
|
||||
if (!email.value || !uid) return;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
await $fetch("/oidc/interaction/login", {
|
||||
method: "POST",
|
||||
body: { email: email.value, uid },
|
||||
});
|
||||
sent.value = true;
|
||||
} catch (e: any) {
|
||||
error.value = e?.data?.statusMessage || "Something went wrong. Please try again.";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50 px-4">
|
||||
<div class="w-full max-w-sm">
|
||||
<div class="bg-white rounded-lg shadow-md p-8">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Ghost Guild Wiki</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Sign in with your Ghost Guild account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="!sent">
|
||||
<form @submit.prevent="sendMagicLink" class="space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="text-sm text-red-600">{{ error }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading || !email"
|
||||
class="w-full py-2 px-4 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ loading ? "Sending..." : "Send magic link" }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-xs text-center text-gray-500">
|
||||
We'll send a sign-in link to your email.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="text-center space-y-3">
|
||||
<div class="text-4xl">✉️</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Check your email</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
We sent a sign-in link to <strong>{{ email }}</strong>.
|
||||
Click the link in the email to continue.
|
||||
</p>
|
||||
<button
|
||||
@click="sent = false; email = '';"
|
||||
class="mt-4 text-sm text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
Use a different email
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div v-else>
|
||||
<!-- Page Header -->
|
||||
<PageHeader :title="series.title" size="large" />
|
||||
<PageHeader :title="series.title" theme="purple" size="large" />
|
||||
|
||||
<!-- Series Meta -->
|
||||
<section class="py-20 bg-[--ui-bg]">
|
||||
|
|
@ -106,9 +106,9 @@
|
|||
<!-- Status Message -->
|
||||
<div
|
||||
v-if="series?.statistics?.isOngoing"
|
||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded mb-8"
|
||||
class="p-4 bg-green-500/10 border border-green-500/30 rounded mb-8"
|
||||
>
|
||||
<p class="text-candlelight-500 dark:text-candlelight-400 font-semibold mb-1">
|
||||
<p class="text-green-600 dark:text-green-400 font-semibold mb-1">
|
||||
This series is currently ongoing!
|
||||
</p>
|
||||
<p class="text-sm text-[--ui-text-muted]">
|
||||
|
|
@ -118,9 +118,9 @@
|
|||
|
||||
<div
|
||||
v-else-if="series?.statistics?.isUpcoming"
|
||||
class="p-4 bg-guild-500/10 border border-guild-500/30 rounded mb-8"
|
||||
class="p-4 bg-blue-500/10 border border-blue-500/30 rounded mb-8"
|
||||
>
|
||||
<p class="text-guild-300 dark:text-guild-300 font-semibold mb-1">
|
||||
<p class="text-blue-600 dark:text-blue-400 font-semibold mb-1">
|
||||
This series is starting soon!
|
||||
</p>
|
||||
<p class="text-sm text-[--ui-text-muted]">
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
|
||||
<div
|
||||
v-else-if="series?.statistics?.isCompleted"
|
||||
class="p-4 bg-guild-500/10 border border-guild-500/30 rounded mb-8"
|
||||
class="p-4 bg-gray-500/10 border border-gray-500/30 rounded mb-8"
|
||||
>
|
||||
<p class="text-[--ui-text] font-semibold mb-1">
|
||||
This series has concluded.
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
<section v-if="series?.tickets?.enabled" class="py-20 bg-[--ui-bg]">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-8">
|
||||
<h2 class="text-2xl font-bold text-[--ui-text] mb-8">
|
||||
Get Your Series Pass
|
||||
</h2>
|
||||
<SeriesPassPurchase
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
<section class="py-20 bg-[--ui-bg-elevated]">
|
||||
<UContainer>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-8">
|
||||
<h2 class="text-2xl font-bold text-[--ui-text] mb-8">
|
||||
Event Schedule
|
||||
</h2>
|
||||
|
||||
|
|
@ -357,19 +357,19 @@ const formatSeriesType = (type) => {
|
|||
const getSeriesTypeBadgeClass = (type) => {
|
||||
const classes = {
|
||||
workshop_series:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
|
||||
recurring_meetup:
|
||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
|
||||
multi_day:
|
||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
||||
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
|
||||
course:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/30",
|
||||
tournament:
|
||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
||||
"bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/30",
|
||||
};
|
||||
return (
|
||||
classes[type] ||
|
||||
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border border-earth-700/30"
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -383,14 +383,14 @@ const getSeriesStatusText = () => {
|
|||
|
||||
const getSeriesStatusClass = () => {
|
||||
if (!series.value?.statistics)
|
||||
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
|
||||
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30";
|
||||
if (series.value.statistics.isOngoing)
|
||||
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
|
||||
return "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30";
|
||||
if (series.value.statistics.isUpcoming)
|
||||
return "bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30";
|
||||
return "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30";
|
||||
if (series.value.statistics.isCompleted)
|
||||
return "bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30";
|
||||
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
|
||||
return "bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30";
|
||||
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30";
|
||||
};
|
||||
|
||||
const formatEventDate = (date) => {
|
||||
|
|
@ -436,15 +436,15 @@ const getEventStatusClass = (event) => {
|
|||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming:
|
||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
|
||||
Ongoing:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
|
||||
Completed:
|
||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30",
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
|
||||
};
|
||||
return (
|
||||
classes[status] ||
|
||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30"
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -452,15 +452,15 @@ const getEventTimelineColor = (event) => {
|
|||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming:
|
||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border-guild-500/30",
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30",
|
||||
Ongoing:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border-candlelight-700/30",
|
||||
"bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30",
|
||||
Completed:
|
||||
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border-earth-700/30",
|
||||
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30",
|
||||
};
|
||||
return (
|
||||
classes[status] ||
|
||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border-guild-500/30"
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<PageHeader
|
||||
title="Event Series"
|
||||
subtitle="Discover our multi-event series designed to take you on a journey of learning and growth"
|
||||
theme="purple"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
|
|
@ -45,16 +46,16 @@
|
|||
:class="[
|
||||
'inline-flex items-center px-2 py-1 rounded text-xs font-medium',
|
||||
series.status === 'active'
|
||||
? 'bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30'
|
||||
? 'bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30'
|
||||
: series.status === 'upcoming'
|
||||
? 'bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30'
|
||||
: 'bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30',
|
||||
? 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30'
|
||||
: 'bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30',
|
||||
]"
|
||||
>
|
||||
{{ series.status }}
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="text-display-sm font-bold text-[--ui-text] mb-2">
|
||||
<h2 class="text-2xl font-bold text-[--ui-text] mb-2">
|
||||
{{ series.title }}
|
||||
</h2>
|
||||
<p class="text-[--ui-text-muted] leading-relaxed">
|
||||
|
|
@ -86,7 +87,7 @@
|
|||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4 flex-1 min-w-0">
|
||||
<div
|
||||
class="w-8 h-8 bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 border border-candlelight-700/30"
|
||||
class="w-8 h-8 bg-purple-500/10 text-purple-600 dark:text-purple-400 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0 border border-purple-500/30"
|
||||
>
|
||||
{{ event.series?.position || "?" }}
|
||||
</div>
|
||||
|
|
@ -178,7 +179,7 @@
|
|||
name="heroicons:squares-2x2"
|
||||
class="w-16 h-16 text-[--ui-text-muted] mx-auto mb-4 opacity-50"
|
||||
/>
|
||||
<h3 class="text-display-sm font-semibold text-[--ui-text] mb-2">
|
||||
<h3 class="text-xl font-semibold text-[--ui-text] mb-2">
|
||||
No Event Series Available
|
||||
</h3>
|
||||
<p class="text-[--ui-text-muted] max-w-md mx-auto">
|
||||
|
|
@ -232,19 +233,19 @@ const formatSeriesType = (type) => {
|
|||
const getSeriesTypeBadgeClass = (type) => {
|
||||
const classes = {
|
||||
workshop_series:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30",
|
||||
recurring_meetup:
|
||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
|
||||
multi_day:
|
||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
||||
"bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/30",
|
||||
course:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/30",
|
||||
tournament:
|
||||
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
|
||||
"bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/30",
|
||||
};
|
||||
return (
|
||||
classes[type] ||
|
||||
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border border-earth-700/30"
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -291,15 +292,15 @@ const getEventStatusClass = (event) => {
|
|||
const status = getEventStatus(event);
|
||||
const classes = {
|
||||
Upcoming:
|
||||
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
|
||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/30",
|
||||
Ongoing:
|
||||
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
|
||||
"bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/30",
|
||||
Completed:
|
||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30",
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30",
|
||||
};
|
||||
return (
|
||||
classes[status] ||
|
||||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30"
|
||||
"bg-gray-500/10 text-gray-600 dark:text-gray-400 border border-gray-500/30"
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PageHeader
|
||||
title="Welcome to Ghost Guild"
|
||||
subtitle="You're officially part of the community!"
|
||||
theme="purple"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
|
|
@ -25,7 +26,7 @@
|
|||
class="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="text-display font-bold text-guild-100 mb-4">
|
||||
<h2 class="text-2xl font-bold text-guild-100 mb-4">
|
||||
Hey {{ memberData?.name || "there" }}!
|
||||
</h2>
|
||||
<p class="text-lg text-guild-300 max-w-2xl mx-auto">
|
||||
|
|
@ -37,15 +38,15 @@
|
|||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
|
||||
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:user-circle"
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-purple-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="font-semibold text-guild-100 mb-2">
|
||||
<span class="text-ui-label text-candlelight-400 mr-2">1.</span>Complete Your Profile
|
||||
1. Complete Your Profile
|
||||
</h3>
|
||||
<p class="text-sm text-guild-400 mb-4">
|
||||
Tell the community about yourself, your skills, and what you're
|
||||
|
|
@ -58,15 +59,15 @@
|
|||
|
||||
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:calendar-days"
|
||||
class="w-6 h-6 text-candlelight-400"
|
||||
class="w-6 h-6 text-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<h3 class="font-semibold text-guild-100 mb-2">
|
||||
<span class="text-ui-label text-candlelight-400 mr-2">2.</span>Join an Event
|
||||
2. Join an Event
|
||||
</h3>
|
||||
<p class="text-sm text-guild-400 mb-4">
|
||||
From workshops to game nights, events are the heart of our
|
||||
|
|
@ -79,12 +80,12 @@
|
|||
|
||||
<div class="p-6 bg-guild-800/50 rounded-xl border border-guild-700">
|
||||
<div
|
||||
class="w-12 h-12 bg-candlelight-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center mb-4"
|
||||
>
|
||||
<Icon name="heroicons:users" class="w-6 h-6 text-candlelight-400" />
|
||||
<Icon name="heroicons:users" class="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
<h3 class="font-semibold text-guild-100 mb-2">
|
||||
<span class="text-ui-label text-candlelight-400 mr-2">3.</span>Meet the Community
|
||||
3. Meet the Community
|
||||
</h3>
|
||||
<p class="text-sm text-guild-400 mb-4">
|
||||
Connect with other members and find peers for support and
|
||||
|
|
@ -100,7 +101,7 @@
|
|||
<div
|
||||
class="p-8 bg-guild-800/30 rounded-2xl border border-guild-700 mb-16"
|
||||
>
|
||||
<h3 class="text-display-sm font-bold text-guild-100 mb-4">
|
||||
<h3 class="text-xl font-bold text-guild-100 mb-4">
|
||||
Understanding Circles
|
||||
</h3>
|
||||
<p class="text-guild-300 mb-6">
|
||||
|
|
@ -114,7 +115,7 @@
|
|||
class="p-4 rounded-lg"
|
||||
:class="
|
||||
memberData?.circle === 'community'
|
||||
? 'circle-surface-community border border-[var(--color-circle-community)]/50'
|
||||
? 'bg-purple-500/20 border border-purple-500/50'
|
||||
: 'bg-guild-800/50'
|
||||
"
|
||||
>
|
||||
|
|
@ -122,7 +123,7 @@
|
|||
Community Circle
|
||||
<span
|
||||
v-if="memberData?.circle === 'community'"
|
||||
class="text-candlelight-400 text-sm ml-2"
|
||||
class="text-purple-400 text-sm ml-2"
|
||||
>← You're here</span
|
||||
>
|
||||
</h4>
|
||||
|
|
@ -136,7 +137,7 @@
|
|||
class="p-4 rounded-lg"
|
||||
:class="
|
||||
memberData?.circle === 'founder'
|
||||
? 'circle-surface-founder border border-[var(--color-circle-founder)]/50'
|
||||
? 'bg-purple-500/20 border border-purple-500/50'
|
||||
: 'bg-guild-800/50'
|
||||
"
|
||||
>
|
||||
|
|
@ -144,7 +145,7 @@
|
|||
Founder Circle
|
||||
<span
|
||||
v-if="memberData?.circle === 'founder'"
|
||||
class="text-candlelight-400 text-sm ml-2"
|
||||
class="text-purple-400 text-sm ml-2"
|
||||
>← You're here</span
|
||||
>
|
||||
</h4>
|
||||
|
|
@ -158,7 +159,7 @@
|
|||
class="p-4 rounded-lg"
|
||||
:class="
|
||||
memberData?.circle === 'practitioner'
|
||||
? 'circle-surface-practitioner border border-[var(--color-circle-practitioner)]/50'
|
||||
? 'bg-purple-500/20 border border-purple-500/50'
|
||||
: 'bg-guild-800/50'
|
||||
"
|
||||
>
|
||||
|
|
@ -166,7 +167,7 @@
|
|||
Practitioner Circle
|
||||
<span
|
||||
v-if="memberData?.circle === 'practitioner'"
|
||||
class="text-candlelight-400 text-sm ml-2"
|
||||
class="text-purple-400 text-sm ml-2"
|
||||
>← You're here</span
|
||||
>
|
||||
</h4>
|
||||
|
|
@ -182,17 +183,17 @@
|
|||
<div
|
||||
class="p-8 bg-guild-800/30 rounded-2xl border border-guild-700 mb-16"
|
||||
>
|
||||
<h3 class="text-display-sm font-bold text-guild-100 mb-4">
|
||||
<h3 class="text-xl font-bold text-guild-100 mb-4">
|
||||
Resources & Support
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-10 h-10 bg-candlelight-500/20 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
class="w-10 h-10 bg-amber-500/20 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:book-open"
|
||||
class="w-5 h-5 text-candlelight-400"
|
||||
class="w-5 h-5 text-amber-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -217,11 +218,11 @@
|
|||
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-10 h-10 bg-ember-500/20 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
class="w-10 h-10 bg-pink-500/20 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:chat-bubble-left-right"
|
||||
class="w-5 h-5 text-ember-400"
|
||||
class="w-5 h-5 text-pink-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
export default defineNuxtPlugin(async () => {
|
||||
const { memberData, checkMemberStatus } = useAuth()
|
||||
|
||||
console.log('🚀 Auth init plugin running on CLIENT')
|
||||
|
||||
// Only initialize if we don't already have member data
|
||||
if (!memberData.value) {
|
||||
await checkMemberStatus()
|
||||
console.log(' - No member data, checking auth status...')
|
||||
|
||||
const isAuthenticated = await checkMemberStatus()
|
||||
|
||||
if (isAuthenticated) {
|
||||
console.log(' - ✅ Authentication successful')
|
||||
} else {
|
||||
console.log(' - ❌ No valid authentication')
|
||||
}
|
||||
} else {
|
||||
console.log(' - ✅ Member data already exists:', memberData.value.email)
|
||||
}
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Ghost Guild Security Evaluation
|
||||
|
||||
**Date:** 2026-02-28 (Phases 0-3 complete as of 2026-03-01)
|
||||
**Date:** 2026-02-28 (updated 2026-03-01)
|
||||
**Framework:** OWASP ASVS v4.0, Level 1
|
||||
**Scope:** Full application stack (Nuxt 4 + Nitro server + MongoDB)
|
||||
|
||||
|
|
@ -132,12 +132,14 @@ ASVS Level 1 targets "all software" and is achievable for a small team.
|
|||
|---|---|---|
|
||||
| Rich text member updates | Same XSS pattern as C3 | Fix markdown sanitization (C3) |
|
||||
| Resource library with downloads | Unauthenticated upload (H4), malware distribution | Add upload auth (H4), file validation |
|
||||
| Etherpad integration | External content rendered unsanitized | Build sanitization utility (C3) |
|
||||
| Cal.com integration | API credential exposure | Fix secret management (H3) |
|
||||
| Member-proposed events | No admin role model, no approval workflow | Build RBAC (C1) |
|
||||
| Advanced search/analytics | Regex injection (M5), privacy leakage | Fix regex escaping (M5) |
|
||||
|
||||
---
|
||||
|
||||
## Remediation Summary (Phases 0-2)
|
||||
## Remediation Summary (Phases 0-1 + partial Phase 2)
|
||||
|
||||
All work lives on branch `security/asvs-remediation`.
|
||||
|
||||
|
|
@ -173,22 +175,6 @@ All work lives on branch `security/asvs-remediation`.
|
|||
### Session management
|
||||
- 7-day token expiry with refresh endpoint at `/api/auth/refresh`.
|
||||
|
||||
### Input validation (`server/utils/schemas.js` + `server/utils/validateBody.js`)
|
||||
- Centralized Zod schemas for all API endpoints. `validateBody(event, schema)` replaces `readBody` and returns only validated fields (stripping unknown properties).
|
||||
- `memberCreateSchema`: Explicit allowlist (email, name, circle, contributionTier). Prevents mass assignment of `role`, `status`, `helcimCustomerId`, `_id`.
|
||||
- `emailSchema`: Trims, lowercases, and validates email format.
|
||||
- `memberProfileUpdateSchema`: Type/length validation on all profile fields and privacy enum values.
|
||||
- `updateCreateSchema`: Content length limit (1–50000 chars), image URL array (max 20).
|
||||
- `adminEventCreateSchema`: Required title, description, dates; optional capacity/location/pricing/tickets.
|
||||
- `paymentVerifySchema`: Validates cardToken and customerId presence.
|
||||
|
||||
### Additional hardening
|
||||
- Logout cookie flags now match login flags (`httpOnly: true`, `secure` conditional on NODE_ENV).
|
||||
- Removed 3 unauthenticated test/debug endpoints (`test-connection`, `test-subscription`, `test-bot`).
|
||||
- Removed sensitive `console.log` statements from Helcim and member creation endpoints.
|
||||
- Removed unused `bcryptjs` dependency.
|
||||
- Added 10MB file size limit on image uploads.
|
||||
|
||||
---
|
||||
|
||||
## Remediation Roadmap
|
||||
|
|
@ -217,48 +203,25 @@ All work lives on branch `security/asvs-remediation`.
|
|||
| 13 | H8 | Return identical response for existing/non-existing accounts | V2.1.7 | Done |
|
||||
| 14 | -- | Add `status: 'active'` check to auth endpoints | V4.1.1 | Done |
|
||||
|
||||
### Phase 2: Hardening (within 30 days of launch) -- COMPLETE
|
||||
### Phase 2: Hardening (within 30 days of launch) -- PARTIAL
|
||||
|
||||
| # | Finding | Fix | ASVS | Status |
|
||||
|---|---------|-----|------|--------|
|
||||
| 15 | H7 | Implement Zod validation across all API endpoints | V5.1.3 | Done |
|
||||
| 16 | M5 | Escape regex in directory search | V5.3.4 | Done |
|
||||
| 17 | M4 | Remove sensitive console.log statements | V7.1.1 | Done |
|
||||
| 18 | M3 | Make devtools conditional on NODE_ENV | V7.4.1 | Done |
|
||||
| 15 | H7 | Implement Zod validation across all API endpoints | V5.1.3 | Open |
|
||||
| 16 | M5 | Escape regex in directory search | V5.3.4 | Open |
|
||||
| 17 | M4 | Remove sensitive console.log statements | V7.1.1 | Open |
|
||||
| 18 | M3 | Make devtools conditional on NODE_ENV | V7.4.1 | Open |
|
||||
| 19 | M1 | Shorter session tokens (7d) with refresh endpoint | V2.5.2 | Done |
|
||||
| 20 | -- | Create shared `requireAuth()`/`requireAdmin()` utilities | V4.1.1 | Done |
|
||||
| 21 | -- | Fix mass assignment in member creation (`new Member(body)`) | V5.1.3 | Done |
|
||||
| 22 | -- | Fix logout cookie flags to match login (httpOnly, secure) | V3.2.1 | Done |
|
||||
| 23 | -- | Remove unauthenticated test/debug endpoints | V4.1.1 | Done |
|
||||
| 24 | -- | Remove dead `bcryptjs` dependency | -- | Done |
|
||||
| 25 | -- | Add 10MB file size limit on image uploads | V13.1.1 | Done |
|
||||
|
||||
### Phase 3: Remaining hardening -- COMPLETE
|
||||
|
||||
| # | Severity | Fix | ASVS | Status |
|
||||
|---|----------|-----|------|--------|
|
||||
| 26 | HIGH | `create-plan.post.js` has no auth guard -- anyone can create Helcim payment plans | V4.1.1 | Done |
|
||||
| 27 | HIGH | `plans.get.js` and `subscriptions.get.js` have no auth -- expose Helcim plan/subscription data | V4.1.1 | Done |
|
||||
| 28 | MEDIUM | `create.post.js` returns full Mongoose member document in response (leaks `role`, `status`, `helcimCustomerId`, internal fields) | V5.1.2 | Done |
|
||||
| 29 | MEDIUM | Helcim error text forwarded to client in `statusMessage` across 7 endpoints (`create-plan`, `customer-code`, `initialize-payment`, `subscription`, `update-billing`, `customer`, `get-or-create-customer`) | V7.4.1 | Done |
|
||||
| 30 | MEDIUM | `tickets/purchase.post.js` and 24 other endpoints still use raw `readBody` without Zod validation | V5.1.3 | Done |
|
||||
| 31 | LOW | `server/api/test/peer-support-debug.get.js` is a debug endpoint in a `test/` directory -- has auth but should be removed before production | V14.2.2 | Done |
|
||||
|
||||
**Phase 3 remediation details:**
|
||||
- Items 26-27: Added `await requireAdmin(event)` to `create-plan.post.js`, `plans.get.js`, and `subscriptions.get.js`.
|
||||
- Item 28: Member creation response now returns an explicit projection (`id`, `email`, `name`, `circle`, `contributionTier`, `status`). Also fixed `subscription.post.js` (5 return paths) and `update-contribution.post.js` (4 return paths) to not expose full member documents. Fixed `get-or-create-customer.post.js` error text forwarding.
|
||||
- Item 29: Replaced all `` `Failed to X: ${errorText}` `` patterns with generic client messages. Also fixed outer catch blocks that forwarded `error.message` -- these now re-throw known `createError` instances and use generic messages for unknown errors.
|
||||
- Item 30: Added 25 new Zod schemas to `server/utils/schemas.js` and migrated all 25 endpoints from `readBody` to `validateBody`. Total schema count: 32 (7 from Phase 2 + 25 from Phase 3). All POST/PUT/PATCH/DELETE endpoints with request bodies now use Zod validation.
|
||||
- Item 31: Deleted `server/api/test/peer-support-debug.get.js` and the `server/api/test/` directory.
|
||||
|
||||
### Phase 4: Before building planned features
|
||||
### Phase 3: Before building planned features
|
||||
|
||||
| # | Fix | Status |
|
||||
|---|-----|--------|
|
||||
| 32 | Build sanitization utility (DOMPurify wrapper) for all user-generated HTML | Open |
|
||||
| 33 | Design admin role model with granular permissions | Open |
|
||||
| 34 | Implement file validation pipeline (type, size, virus scanning) | Open |
|
||||
| 35 | Design credential management patterns (encrypted at rest) | Open |
|
||||
| 21 | Build sanitization utility (DOMPurify wrapper) for all user-generated HTML | Open |
|
||||
| 22 | Design admin role model with granular permissions | Open |
|
||||
| 23 | Implement file validation pipeline (type, size, virus scanning) | Open |
|
||||
| 24 | Design credential management patterns (encrypted at rest) | Open |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -281,7 +244,7 @@ npm run test:run # Single run (CI)
|
|||
- `tests/server/setup.js` -- Stubs real h3 functions (`getCookie`, `setCookie`, `getMethod`, `getHeader`, `setHeader`, `getRequestURL`, `createError`, `defineEventHandler`, `readBody`, etc.) as globals to simulate Nitro auto-imports. Also stubs `useRuntimeConfig`.
|
||||
- `tests/server/helpers/createMockEvent.js` -- Factory that builds real h3 events from Node.js `IncomingMessage`/`ServerResponse` pairs. Accepts `method`, `path`, `headers`, `cookies`, `body`, and `remoteAddress`. Captures response headers via `event._testSetHeaders` for assertions.
|
||||
|
||||
### Test Coverage (213 tests across 12 files)
|
||||
### Test Coverage (79 tests across 8 files)
|
||||
|
||||
| File | Tests | Security Controls Verified |
|
||||
|------|-------|---------------------------|
|
||||
|
|
@ -291,12 +254,8 @@ npm run test:run # Single run (CI)
|
|||
| `tests/server/middleware/security-headers.test.js` | 12 | X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy always set; HSTS + CSP production-only; CSP includes Helcim/Cloudinary/Plausible sources (H9, V9.1.1) |
|
||||
| `tests/server/middleware/rate-limit.test.js` | 4 | Auth endpoint 5/5min limit, payment endpoint 10/min limit, IP isolation between clients (H1, V13.2.5) |
|
||||
| `tests/server/utils/auth.test.js` | 8 | No cookie = 401, invalid JWT = 401, member not found = 401, suspended = 403, cancelled = 403, active = returns member, non-admin = 403, admin = returns member (C1, V4.1.1) |
|
||||
| `tests/server/api/auth-login.test.js` | 4 | Existing and non-existing emails return identical response shape and message, missing email = 400 via Zod (H8, V2.1.7) |
|
||||
| `tests/server/api/auth-login.test.js` | 4 | Existing and non-existing emails return identical response shape and message, missing email = 400 (H8, V2.1.7) |
|
||||
| `tests/server/api/members-profile-patch.test.js` | 7 | `helcimCustomerId`, `role`, `status`, `email`, `_id` blocked from `$set`; allowed fields (`pronouns`, `bio`, `studio`, etc.) and nested objects (`offering`, `lookingFor`) pass through (H6, V5.1.3) |
|
||||
| `tests/server/api/validation.test.js` | 38 | Mass assignment: `role`, `status`, `helcimCustomerId`, `_id` stripped from member create; email format validation on login/register/create; content length limits; image array limits; payment token validation; enum validation for circles, tiers, privacy; `validateBody` integration (H7, V5.1.3) |
|
||||
| `tests/server/api/helcim-auth.test.js` | 6 | `create-plan.post.js`, `plans.get.js`, `subscriptions.get.js` all call `requireAdmin` before business logic (Items 26-27, V4.1.1) |
|
||||
| `tests/server/api/members-create-response.test.js` | 9 | Response projection: no raw `member` return, explicit `_id`/`email`/`name`/`circle`/`status` fields, no `helcimCustomerId` or `role` in response, no `error.message` forwarding (Item 28, V5.1.2) |
|
||||
| `tests/server/api/validation-phase3.test.js` | 81 | 25 new Zod schemas: invalid input rejected, valid input passes, unknown fields stripped, mass assignment prevented; error text forwarding regression checks on 8 Helcim endpoints; validateBody migration verified across all 25 migrated endpoints (Items 29-30, V5.1.3, V7.4.1) |
|
||||
|
||||
### Manual Test Cases
|
||||
|
||||
|
|
@ -312,38 +271,12 @@ These items require browser or network-level verification and are not covered by
|
|||
|
||||
**Item 8 (Payment auth):** Each endpoint with no cookie = 401. Full join flow still completes.
|
||||
|
||||
**Item 21 (Mass assignment):** `curl -X POST /api/members/create -d '{"email":"test@test.com","name":"Test","circle":"community","contributionTier":"0","role":"admin"}' -H 'Content-Type: application/json'` -- response member must have `role: "member"`, not `"admin"`.
|
||||
|
||||
**Item 22 (Logout cookie):** Log in, verify `auth-token` cookie exists. Log out, verify `auth-token` cookie is cleared. `document.cookie` must not contain `auth-token` at any point.
|
||||
|
||||
**Item 23 (Deleted endpoints):** `curl /api/helcim/test-connection`, `/api/helcim/test-subscription`, `/api/slack/test-bot` must all return 404.
|
||||
|
||||
**Item 25 (Upload size):** Upload a file >10MB to `/api/upload/image` -- must return 400 "File too large".
|
||||
|
||||
### Integration verification (after each phase)
|
||||
|
||||
- `npm run test:run` -- all 213 tests pass
|
||||
- `npm run test:run` -- all 79 tests pass
|
||||
- `npm run build` succeeds
|
||||
- Full join flow: free tier + paid tier
|
||||
- Full login flow: magic link request, click, redirect to `/members`
|
||||
- Profile editing: avatar upload, bio update, privacy settings
|
||||
- Admin pages: access control verified
|
||||
- `curl` against hardened endpoints: unauthenticated = rejected
|
||||
|
||||
---
|
||||
|
||||
## Notes for Future Rounds
|
||||
|
||||
### Zod v4 behavior to be aware of
|
||||
|
||||
- **Transform ordering matters.** `.trim().toLowerCase()` must come BEFORE `.email()`. Zod v4 runs transforms in chain order, so `.email().trim()` validates the untrimmed string first and rejects `" user@example.com "`. This is opposite to some other validation libraries.
|
||||
- **`.optional()` does NOT accept `null`.** Only `undefined`. If the frontend sends `null` to clear a field, Zod rejects it. Use `.nullable()` or `.nullish()` if null-clearing is needed. Audit frontend form behavior before migrating remaining endpoints.
|
||||
- **Unknown keys are stripped, not rejected.** `z.object()` silently drops fields not in the schema. This is the desired behavior for mass assignment prevention, but means typo'd field names from the frontend will be silently ignored rather than producing an error. Use `.strict()` on schemas if you want to reject unexpected fields.
|
||||
|
||||
### Patterns that recurred during Phases 2-3
|
||||
|
||||
- **Auth guard gaps.** (Fixed in Phase 3, items 26-27.) Every new endpoint needs `requireAuth` or `requireAdmin`. Recommendation: add a grep check to CI that flags any `server/api/` handler not containing `requireAuth`, `requireAdmin`, or an explicit `// @public` comment.
|
||||
- **Error text forwarding.** (Fixed in Phase 3, item 29.) The pattern `` statusMessage: `Failed to X: ${errorText}` `` leaks upstream API error details to the client. Standard pattern is now: generic client message + `console.error` for server-side logging.
|
||||
- **Response over-exposure.** (Fixed in Phase 3, item 28.) `return { success: true, member }` sends the full Mongoose document. Standard pattern is now: explicit projection to allowlisted fields.
|
||||
- **`readBody` audit.** (Fixed in Phase 3, item 30.) All POST/PUT/PATCH/DELETE endpoints now use `validateBody` with Zod schemas. 32 total schemas across 32 endpoints.
|
||||
- **Debug endpoints.** (Fixed in Phase 3, item 31.) `server/api/test/` directory deleted. Consider a build-time check or `.gitignore` pattern to prevent future `server/api/test/` additions.
|
||||
|
|
|
|||
|
|
@ -10,13 +10,6 @@ export default defineNuxtConfig({
|
|||
domain: "ghostguild.org",
|
||||
},
|
||||
css: ["~/assets/css/main.css"],
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
port: 24678,
|
||||
},
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
// Private keys (server-side only)
|
||||
mongodbUri:
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 433 KiB |
|
|
@ -7,7 +7,15 @@ export default defineEventHandler(async (event) => {
|
|||
await requireAdmin(event)
|
||||
|
||||
const eventId = getRouterParam(event, 'id')
|
||||
const body = await validateBody(event, adminEventUpdateSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.title || !body.description || !body.startDate || !body.endDate) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing required fields'
|
||||
})
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
|
|
@ -55,7 +63,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error('Error updating event:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusMessage: error.message || 'Failed to update event'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,15 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAdmin(event)
|
||||
|
||||
const body = await validateBody(event, adminMemberCreateSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.email || !body.circle || !body.contributionTier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Missing required fields'
|
||||
})
|
||||
}
|
||||
|
||||
await connectDB()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import Member from '../../../../models/member.js'
|
||||
import { connectDB } from '../../../../utils/mongoose.js'
|
||||
import { validateBody } from '../../../../utils/validateBody.js'
|
||||
import { adminRoleUpdateSchema } from '../../../../utils/schemas.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const admin = await requireAdmin(event)
|
||||
await connectDB()
|
||||
|
||||
const { role } = await validateBody(event, adminRoleUpdateSchema)
|
||||
const memberId = getRouterParam(event, 'id')
|
||||
|
||||
// Prevent self-demotion
|
||||
if (admin._id.toString() === memberId && role !== 'admin') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'You cannot remove your own admin role.'
|
||||
})
|
||||
}
|
||||
|
||||
const member = await Member.findByIdAndUpdate(
|
||||
memberId,
|
||||
{ role },
|
||||
{ new: true }
|
||||
)
|
||||
|
||||
if (!member) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Member not found.'
|
||||
})
|
||||
}
|
||||
|
||||
return { success: true, member }
|
||||
})
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
const { members } = await validateBody(event, bulkMemberImportSchema)
|
||||
await connectDB()
|
||||
|
||||
// Check for duplicate emails within the batch
|
||||
const batchEmails = members.map(m => m.email)
|
||||
const uniqueEmails = new Set(batchEmails)
|
||||
if (uniqueEmails.size !== batchEmails.length) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Duplicate emails found in import batch'
|
||||
})
|
||||
}
|
||||
|
||||
// Check which emails already exist in the DB
|
||||
const existingMembers = await Member.find(
|
||||
{ email: { $in: batchEmails } },
|
||||
{ email: 1 }
|
||||
)
|
||||
const existingEmails = new Set(existingMembers.map(m => m.email))
|
||||
|
||||
const results = []
|
||||
|
||||
for (const row of members) {
|
||||
if (existingEmails.has(row.email)) {
|
||||
results.push({ email: row.email, success: false, error: 'Email already exists' })
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const member = new Member({
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
circle: row.circle,
|
||||
contributionTier: row.contributionTier,
|
||||
slackInvited: false
|
||||
})
|
||||
const saved = await member.save()
|
||||
results.push({ email: row.email, success: true, memberId: saved._id })
|
||||
} catch (err) {
|
||||
results.push({ email: row.email, success: false, error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
const created = results.filter(r => r.success).length
|
||||
const failed = results.filter(r => !r.success).length
|
||||
|
||||
return { created, failed, results }
|
||||
})
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import jwt from 'jsonwebtoken'
|
||||
import { Resend } from 'resend'
|
||||
import Member from '../../../models/member.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireAdmin(event)
|
||||
const { memberIds, emailTemplate } = await validateBody(event, memberInviteSchema)
|
||||
await connectDB()
|
||||
|
||||
const config = useRuntimeConfig(event)
|
||||
const headers = getHeaders(event)
|
||||
const baseUrl =
|
||||
process.env.BASE_URL ||
|
||||
`${headers.host?.includes('localhost') ? 'http' : 'https'}://${headers.host}`
|
||||
|
||||
const members = await Member.find({ _id: { $in: memberIds } })
|
||||
|
||||
if (members.length === 0) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'No members found for the provided IDs'
|
||||
})
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
||||
for (const member of members) {
|
||||
try {
|
||||
// Generate 48-hour magic login token (same format as login.post.js)
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
config.jwtSecret,
|
||||
{ expiresIn: '48h' }
|
||||
)
|
||||
|
||||
const loginLink = `${baseUrl}/api/auth/verify?token=${token}`
|
||||
|
||||
// Interpolate template variables
|
||||
const emailText = emailTemplate
|
||||
.replace(/\{name\}/g, member.name)
|
||||
.replace(/\{loginLink\}/g, loginLink)
|
||||
.replace(/\{circle\}/g, member.circle)
|
||||
|
||||
// Build HTML version: escape user content, linkify plain URLs, then insert button (unescaped) last
|
||||
const loginButton = `<a href="${loginLink}" style="display:inline-block;padding:12px 24px;background-color:#d4a017;color:#1a1a1a;text-decoration:none;border-radius:6px;font-weight:bold;">Sign in to Ghost Guild</a>`
|
||||
const emailHtml = emailTemplate
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\{name\}/g, member.name)
|
||||
.replace(/\{circle\}/g, member.circle)
|
||||
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1">$1</a>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\{loginLink\}/g, loginButton)
|
||||
|
||||
const { error: sendError } = await resend.emails.send({
|
||||
from: 'Ghost Guild <welcome@babyghosts.org>',
|
||||
to: [member.email],
|
||||
subject: 'You\'re invited to Ghost Guild',
|
||||
text: emailText,
|
||||
html: emailHtml
|
||||
})
|
||||
|
||||
if (sendError) {
|
||||
results.push({ memberId: member._id, email: member.email, success: false, error: sendError.message })
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark member as active and record invite sent
|
||||
member.status = 'active'
|
||||
member.inviteEmailSent = true
|
||||
member.inviteEmailSentAt = new Date()
|
||||
await member.save()
|
||||
|
||||
results.push({ memberId: member._id, email: member.email, success: true })
|
||||
} catch (err) {
|
||||
results.push({ memberId: member._id, email: member.email, success: false, error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
const sent = results.filter(r => r.success).length
|
||||
const failed = results.filter(r => !r.success).length
|
||||
|
||||
return { sent, failed, results }
|
||||
})
|
||||
|
|
@ -7,7 +7,15 @@ export default defineEventHandler(async (event) => {
|
|||
const admin = await requireAdmin(event)
|
||||
await connectDB()
|
||||
|
||||
const body = await validateBody(event, adminSeriesCreateSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.id || !body.title || !body.description) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID, title, and description are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Create new series
|
||||
const newSeries = new Series({
|
||||
|
|
@ -35,10 +43,9 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusMessage: error.message || 'Failed to create series'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -8,9 +8,16 @@ export default defineEventHandler(async (event) => {
|
|||
await requireAdmin(event)
|
||||
await connectDB()
|
||||
|
||||
const body = await validateBody(event, adminSeriesUpdateSchema)
|
||||
const body = await readBody(event)
|
||||
const { id, title, description, type, totalEvents } = body
|
||||
|
||||
if (!id || !title) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID and title are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Update the series record
|
||||
const updatedSeries = await Series.findOneAndUpdate(
|
||||
{ id },
|
||||
|
|
@ -48,11 +55,10 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
return updatedSeries
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error updating series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusMessage: 'Failed to update series'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
|||
await connectDB()
|
||||
|
||||
const id = getRouterParam(event, 'id')
|
||||
const body = await validateBody(event, adminSeriesItemUpdateSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
|
|
@ -55,11 +55,10 @@ export default defineEventHandler(async (event) => {
|
|||
data: series
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error updating series:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusMessage: error.message || 'Failed to update series'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -8,9 +8,23 @@ export default defineEventHandler(async (event) => {
|
|||
await requireAdmin(event)
|
||||
await connectDB()
|
||||
|
||||
const body = await validateBody(event, adminSeriesTicketsSchema)
|
||||
const body = await readBody(event)
|
||||
const { id, tickets } = body
|
||||
|
||||
if (!id) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Series ID is required'
|
||||
})
|
||||
}
|
||||
|
||||
if (!tickets) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Tickets configuration is required'
|
||||
})
|
||||
}
|
||||
|
||||
// Find the series
|
||||
const series = await Series.findOne({ id })
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { emailSchema } from "../../utils/schemas.js";
|
|||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Connect to database
|
||||
await connectDB();
|
||||
|
||||
const { email } = await validateBody(event, emailSchema);
|
||||
|
|
@ -18,12 +19,14 @@ export default defineEventHandler(async (event) => {
|
|||
const member = await Member.findOne({ email });
|
||||
|
||||
if (!member) {
|
||||
// Return same response shape to prevent enumeration
|
||||
return {
|
||||
success: true,
|
||||
message: GENERIC_MESSAGE,
|
||||
};
|
||||
}
|
||||
|
||||
// Generate magic link token (use runtime config for consistency with verify/requireAuth)
|
||||
const config = useRuntimeConfig(event);
|
||||
const token = jwt.sign(
|
||||
{ memberId: member._id },
|
||||
|
|
@ -31,22 +34,33 @@ export default defineEventHandler(async (event) => {
|
|||
{ expiresIn: "15m" },
|
||||
);
|
||||
|
||||
// Get the base URL for the magic link
|
||||
const headers = getHeaders(event);
|
||||
const baseUrl =
|
||||
process.env.BASE_URL ||
|
||||
`${headers.host?.includes("localhost") ? "http" : "https"}://${headers.host}`;
|
||||
|
||||
// Send magic link via Resend
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: "Ghost Guild <ghostguild@babyghosts.org>",
|
||||
to: email,
|
||||
subject: "Your Ghost Guild login link",
|
||||
text: `Hi,
|
||||
|
||||
Sign in to Ghost Guild:
|
||||
${baseUrl}/api/auth/verify?token=${token}
|
||||
|
||||
This link expires in 15 minutes. If you didn't request it, ignore this email.`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">Welcome back to Ghost Guild!</h2>
|
||||
<p>Click the button below to sign in to your account:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${baseUrl}/api/auth/verify?token=${token}"
|
||||
style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
||||
Sign In to Ghost Guild
|
||||
</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
This link will expire in 15 minutes for security. If you didn't request this login link, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -30,13 +30,6 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
if (member.status === 'suspended' || member.status === 'cancelled') {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'Account is ' + member.status
|
||||
})
|
||||
}
|
||||
|
||||
// Create a new session token for the authenticated user
|
||||
const sessionToken = jwt.sign(
|
||||
{ memberId: member._id, email: member.email },
|
||||
|
|
@ -52,14 +45,10 @@ export default defineEventHandler(async (event) => {
|
|||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
||||
})
|
||||
|
||||
// Admins go to admin dashboard, everyone else goes to coming-soon (with wiki link)
|
||||
const redirectUrl = member.role === 'admin' ? '/admin' : '/coming-soon'
|
||||
await sendRedirect(event, redirectUrl, 302)
|
||||
// Redirect to the members dashboard or home page
|
||||
await sendRedirect(event, '/members', 302)
|
||||
|
||||
} catch (err) {
|
||||
if (err.statusCode && err.statusCode !== 401) {
|
||||
throw err
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid or expired token'
|
||||
|
|
|
|||
|
|
@ -6,9 +6,16 @@ import {
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, cancelRegistrationSchema);
|
||||
const body = await readBody(event);
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if id is a valid ObjectId or treat as slug
|
||||
const isObjectId = /^[0-9a-fA-F]{24}$/.test(id);
|
||||
|
|
|
|||
|
|
@ -2,9 +2,16 @@ import Event from "../../../models/event";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, checkRegistrationSchema);
|
||||
const body = await readBody(event);
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if id is a valid ObjectId or treat as slug
|
||||
const isObjectId = /^[0-9a-fA-F]{24}$/.test(id);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
const body = await validateBody(event, guestRegisterSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
|
|
@ -15,6 +15,14 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Validate required fields for guest registration
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name and email are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB()
|
||||
const identifier = getRouterParam(event, 'id')
|
||||
const body = await validateBody(event, eventPaymentSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
|
|
@ -17,6 +17,14 @@ export default defineEventHandler(async (event) => {
|
|||
})
|
||||
}
|
||||
|
||||
// Validate required payment fields
|
||||
if (!body.name || !body.email || !body.paymentToken) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name, email, and payment token are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ import { connectDB } from "../../../../utils/mongoose.js";
|
|||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await connectDB();
|
||||
const body = await validateBody(event, ticketEligibilitySchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is a member
|
||||
const member = await Member.findOne({
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB();
|
||||
const identifier = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, ticketPurchaseSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
|
|
@ -27,6 +27,14 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData;
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB();
|
||||
const identifier = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, ticketReserveSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!identifier) {
|
||||
throw createError({
|
||||
|
|
@ -25,6 +25,13 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (!body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch the event
|
||||
let eventData;
|
||||
if (mongoose.Types.ObjectId.isValid(identifier)) {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import Event from "../../../models/event";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, waitlistDeleteSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
const { email } = body;
|
||||
|
||||
if (!email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Find event by ID or slug
|
||||
const eventData = await Event.findOne({
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ import Member from "../../../models/member";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, waitlistSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
const { name, email, membershipLevel } = body;
|
||||
|
||||
if (!email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Email is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Find event by ID or slug
|
||||
const eventData = await Event.findOne({
|
||||
|
|
|
|||
|
|
@ -3,9 +3,16 @@ const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await requireAdmin(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, helcimCreatePlanSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.amount || !body.frequency) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name, amount, and frequency are required'
|
||||
})
|
||||
}
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
|
|
@ -31,7 +38,7 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'Payment plan creation failed'
|
||||
statusMessage: `Failed to create payment plan: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -43,11 +50,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error creating Helcim payment plan:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to create payment plan'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default defineEventHandler(async (event) => {
|
|||
const errorText = await response.text()
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'Customer lookup failed'
|
||||
statusMessage: `Failed to get customer: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,11 +74,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error getting customer code:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to get customer code'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB()
|
||||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, helcimCustomerSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name || !body.email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Name and email are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if member already exists
|
||||
const existingMember = await Member.findOne({ email: body.email })
|
||||
|
|
@ -50,7 +58,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error('Connection test failed:', testError)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Payment service unavailable'
|
||||
statusMessage: `Helcim API connection failed: ${testError.message}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +82,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error('Customer creation failed:', customerResponse.status, errorText)
|
||||
throw createError({
|
||||
statusCode: customerResponse.status,
|
||||
statusMessage: 'Customer creation failed'
|
||||
statusMessage: `Failed to create customer: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +133,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error creating Helcim customer:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to create customer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -100,7 +100,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error('Failed to create Helcim customer:', createResponse.status, errorText)
|
||||
throw createError({
|
||||
statusCode: createResponse.status,
|
||||
statusMessage: 'Customer creation failed'
|
||||
statusMessage: `Failed to create Helcim customer: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -118,11 +118,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error in get-or-create-customer:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to get or create customer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAuth(event);
|
||||
const config = useRuntimeConfig(event);
|
||||
const body = await validateBody(event, helcimInitializePaymentSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
|
||||
const helcimToken =
|
||||
|
|
@ -64,7 +64,7 @@ export default defineEventHandler(async (event) => {
|
|||
);
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'Payment initialization failed',
|
||||
statusMessage: `Failed to initialize payment: ${errorText}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -76,11 +76,10 @@ export default defineEventHandler(async (event) => {
|
|||
secretToken: paymentData.secretToken,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Error initializing HelcimPay:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "An unexpected error occurred",
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || "Failed to initialize payment",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await requireAdmin(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
|
|
@ -31,11 +30,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error fetching Helcim payment plans:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to fetch payment plans'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -76,7 +76,22 @@ export default defineEventHandler(async (event) => {
|
|||
await requireAuth(event)
|
||||
await connectDB()
|
||||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, helcimSubscriptionSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.customerId || !body.contributionTier) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Customer ID and contribution tier are required'
|
||||
})
|
||||
}
|
||||
|
||||
if (!body.customerCode) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Customer code is required for subscription creation'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if payment is required
|
||||
if (!requiresPayment(body.contributionTier)) {
|
||||
|
|
@ -97,14 +112,7 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
subscription: null,
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
}
|
||||
member
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,14 +152,7 @@ export default defineEventHandler(async (event) => {
|
|||
status: 'needs_plan_setup',
|
||||
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
},
|
||||
member,
|
||||
warning: `Payment successful but recurring plan needs to be set up in Helcim for the ${body.contributionTier} tier`
|
||||
}
|
||||
}
|
||||
|
|
@ -221,23 +222,17 @@ export default defineEventHandler(async (event) => {
|
|||
subscription: {
|
||||
subscriptionId: 'manual-' + Date.now(),
|
||||
status: 'needs_setup',
|
||||
error: errorText,
|
||||
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
},
|
||||
member,
|
||||
warning: 'Payment successful but recurring subscription needs manual setup'
|
||||
}
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: subscriptionResponse.status,
|
||||
statusMessage: 'Subscription creation failed'
|
||||
statusMessage: `Failed to create subscription: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -272,14 +267,7 @@ export default defineEventHandler(async (event) => {
|
|||
status: subscription.status,
|
||||
nextBillingDate: subscription.nextBillingDate
|
||||
},
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
}
|
||||
member
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error('Error during subscription creation:', fetchError)
|
||||
|
|
@ -306,25 +294,18 @@ export default defineEventHandler(async (event) => {
|
|||
subscription: {
|
||||
subscriptionId: 'manual-' + Date.now(),
|
||||
status: 'needs_setup',
|
||||
error: fetchError.message,
|
||||
nextBillingDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
},
|
||||
member,
|
||||
warning: 'Payment successful but recurring subscription needs manual setup'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error creating Helcim subscription:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to create subscription'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -3,7 +3,6 @@ const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
await requireAdmin(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
|
|
@ -31,11 +30,10 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error fetching Helcim subscriptions:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to fetch subscriptions'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -7,10 +7,26 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await requireAuth(event)
|
||||
const config = useRuntimeConfig(event)
|
||||
const body = await validateBody(event, helcimUpdateBillingSchema)
|
||||
const body = await readBody(event)
|
||||
|
||||
// Validate required fields
|
||||
if (!body.customerId || !body.billingAddress) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Customer ID and billing address are required'
|
||||
})
|
||||
}
|
||||
|
||||
const { billingAddress } = body
|
||||
|
||||
// Validate billing address fields
|
||||
if (!billingAddress.street || !billingAddress.city || !billingAddress.country || !billingAddress.postalCode) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Complete billing address is required'
|
||||
})
|
||||
}
|
||||
|
||||
const helcimToken = config.public.helcimToken || process.env.NUXT_PUBLIC_HELCIM_TOKEN
|
||||
|
||||
// Update customer billing address in Helcim
|
||||
|
|
@ -38,7 +54,7 @@ export default defineEventHandler(async (event) => {
|
|||
console.error('Billing address update failed:', response.status, errorText)
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: 'Billing update failed'
|
||||
statusMessage: `Failed to update billing address: ${errorText}`
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -49,11 +65,10 @@ export default defineEventHandler(async (event) => {
|
|||
customer: customerData
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error
|
||||
console.error('Error updating billing address:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'An unexpected error occurred'
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || 'Failed to update billing address'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -100,24 +100,14 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
// TODO: Send welcome email
|
||||
|
||||
return {
|
||||
success: true,
|
||||
member: {
|
||||
id: member._id,
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
circle: member.circle,
|
||||
contributionTier: member.contributionTier,
|
||||
status: member.status
|
||||
}
|
||||
}
|
||||
return { success: true, member }
|
||||
} catch (error) {
|
||||
if (error.statusCode) {
|
||||
throw error
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Member creation failed'
|
||||
statusMessage: error.message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
const body = await validateBody(event, peerSupportUpdateSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
// Build update object for peer support settings
|
||||
const updateData = {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => {
|
|||
try {
|
||||
await connectDB();
|
||||
const config = useRuntimeConfig(event);
|
||||
const body = await validateBody(event, updateContributionSchema);
|
||||
const body = await readBody(event);
|
||||
const token = getCookie(event, "auth-token");
|
||||
|
||||
if (!token) {
|
||||
|
|
@ -35,6 +35,17 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
// Validate contribution tier
|
||||
if (
|
||||
!body.contributionTier ||
|
||||
!isValidContributionValue(body.contributionTier)
|
||||
) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invalid contribution tier",
|
||||
});
|
||||
}
|
||||
|
||||
// Get member
|
||||
const member = await Member.findById(decoded.memberId);
|
||||
if (!member) {
|
||||
|
|
@ -52,6 +63,7 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Already on this tier",
|
||||
member,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +186,7 @@ export default defineEventHandler(async (event) => {
|
|||
if (!subscriptionResponse.ok) {
|
||||
const errorText = await subscriptionResponse.text();
|
||||
console.error("Failed to create subscription:", errorText);
|
||||
throw new Error('Subscription creation failed');
|
||||
throw new Error(`Failed to create subscription: ${errorText}`);
|
||||
}
|
||||
|
||||
const subscriptionData = await subscriptionResponse.json();
|
||||
|
|
@ -194,6 +206,7 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Successfully upgraded to paid tier",
|
||||
member,
|
||||
subscription: {
|
||||
subscriptionId: subscription.id,
|
||||
status: subscription.status,
|
||||
|
|
@ -249,6 +262,7 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Successfully downgraded to free tier",
|
||||
member,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +311,7 @@ export default defineEventHandler(async (event) => {
|
|||
response.status,
|
||||
errorText,
|
||||
);
|
||||
throw new Error('Subscription update failed');
|
||||
throw new Error(`Failed to update subscription: ${errorText}`);
|
||||
}
|
||||
|
||||
const subscriptionData = await response.json();
|
||||
|
|
@ -309,13 +323,14 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Successfully updated contribution level",
|
||||
member,
|
||||
subscription: subscriptionData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating Helcim subscription:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Subscription update failed",
|
||||
statusMessage: error.message || "Failed to update subscription",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -327,13 +342,13 @@ export default defineEventHandler(async (event) => {
|
|||
return {
|
||||
success: true,
|
||||
message: "Successfully updated contribution level",
|
||||
member,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.statusCode) throw error;
|
||||
console.error("Error updating contribution tier:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "An unexpected error occurred",
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || "Failed to update contribution tier",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import Member from "../../../../models/member.js";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const body = await validateBody(event, seriesTicketEligibilitySchema);
|
||||
const body = await readBody(event);
|
||||
const { email } = body;
|
||||
|
||||
const member = await Member.findOne({ email });
|
||||
if (!email) {
|
||||
return {
|
||||
isMember: false,
|
||||
message: "Email is required",
|
||||
};
|
||||
}
|
||||
|
||||
const member = await Member.findOne({ email: email.toLowerCase() });
|
||||
|
||||
if (!member) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,17 @@ import { sendSeriesPassConfirmation } from "../../../../utils/resend.js";
|
|||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const seriesId = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, seriesTicketPurchaseSchema);
|
||||
const body = await readBody(event);
|
||||
const { name, email, paymentId } = body;
|
||||
|
||||
// Validate input
|
||||
if (!name || !email) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch series
|
||||
// Build query conditions based on whether seriesId looks like ObjectId or string
|
||||
const isObjectId = /^[0-9a-fA-F]{24}$/.test(seriesId);
|
||||
|
|
|
|||
28
server/api/test/peer-support-debug.get.js
Normal file
28
server/api/test/peer-support-debug.get.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import Member from "../../models/member.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await connectDB();
|
||||
|
||||
const token = getCookie(event, "auth-token");
|
||||
if (!token) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Not authenticated" });
|
||||
}
|
||||
|
||||
let memberId;
|
||||
try {
|
||||
const decoded = jwt.verify(token, useRuntimeConfig().jwtSecret);
|
||||
memberId = decoded.memberId;
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 401, statusMessage: "Invalid token" });
|
||||
}
|
||||
|
||||
const member = await Member.findById(memberId).select("name peerSupport slackUserId");
|
||||
|
||||
return {
|
||||
name: member.name,
|
||||
peerSupport: member.peerSupport,
|
||||
slackUserId: member.slackUserId,
|
||||
};
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
const id = getRouterParam(event, "id");
|
||||
const body = await validateBody(event, updatePatchSchema);
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
const update = await Update.findById(id);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,29 @@
|
|||
// server/emails/welcome.js
|
||||
export const welcomeEmail = (member) => ({
|
||||
from: 'Ghost Guild <welcome@babyghosts.org>',
|
||||
from: 'Ghost Guild <welcome@ghostguild.org>',
|
||||
to: member.email,
|
||||
subject: 'Welcome to Ghost Guild',
|
||||
text: `Hi ${member.name},
|
||||
subject: 'Welcome to Ghost Guild! 👻',
|
||||
html: `
|
||||
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h1>Welcome to the community, ${member.name}!</h1>
|
||||
|
||||
Welcome to the ${member.circle} circle.
|
||||
<p>You've joined the <strong>${member.circle} circle</strong>
|
||||
with a ${member.contributionTier}/month contribution.</p>
|
||||
|
||||
Next steps:
|
||||
1. Watch for your Slack invite (within 24 hours)
|
||||
2. Explore the resource library: https://ghostguild.org/members/resources
|
||||
3. Introduce yourself in #introductions`
|
||||
<h2>Your next steps:</h2>
|
||||
<ol>
|
||||
<li>Watch for your Slack invite (within 24 hours)</li>
|
||||
<li>Explore the <a href="https://ghostguild.org/members/resources">resource library</a></li>
|
||||
<li>Introduce yourself in #introductions</li>
|
||||
</ol>
|
||||
|
||||
<p>Thank you for being part of our solidarity economy!</p>
|
||||
|
||||
<hr style="margin: 30px 0; border: 1px solid #eee;">
|
||||
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
Questions? Reply to this email or reach out in Slack.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
export default defineEventHandler((event) => {
|
||||
const path = getRequestURL(event).pathname
|
||||
|
||||
const headers = {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
|
|
@ -12,9 +10,7 @@ export default defineEventHandler((event) => {
|
|||
if (process.env.NODE_ENV === 'production') {
|
||||
headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
|
||||
// Skip CSP for OIDC routes — they serve self-contained HTML
|
||||
// rendered by oidc-provider with its own form actions
|
||||
if (!path.startsWith('/oidc/')) {
|
||||
// CSP: allow self, Cloudinary images, HelcimPay.js, Plausible analytics
|
||||
headers['Content-Security-Policy'] = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://myposjs.helcim.com https://plausible.io",
|
||||
|
|
@ -27,7 +23,6 @@ export default defineEventHandler((event) => {
|
|||
"form-action 'self'",
|
||||
].join('; ')
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
setHeader(event, key, value)
|
||||
|
|
|
|||
|
|
@ -133,9 +133,6 @@ const memberSchema = new mongoose.Schema({
|
|||
},
|
||||
},
|
||||
|
||||
inviteEmailSent: { type: Boolean, default: false },
|
||||
inviteEmailSentAt: Date,
|
||||
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
lastLogin: Date,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
|
||||
// No valid session — redirect to login page
|
||||
return sendRedirect(event, `/auth/wiki-login?uid=${uid}`, 302);
|
||||
return sendRedirect(event, `/oidc/login?uid=${uid}`, 302);
|
||||
}
|
||||
|
||||
// ----- Consent prompt -----
|
||||
|
|
|
|||
|
|
@ -53,13 +53,21 @@ export default defineEventHandler(async (event) => {
|
|||
from: "Ghost Guild <ghostguild@babyghosts.org>",
|
||||
to: email,
|
||||
subject: "Sign in to Ghost Guild Wiki",
|
||||
text: `Sign in to the Ghost Guild Wiki
|
||||
|
||||
Use this link to sign in:
|
||||
|
||||
${baseUrl}/oidc/interaction/verify?token=${token}
|
||||
|
||||
This link expires in 15 minutes. If you didn't request this, you can safely ignore this email.`,
|
||||
html: `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #2563eb;">Sign in to the Ghost Guild Wiki</h2>
|
||||
<p>Click the button below to sign in:</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${baseUrl}/oidc/interaction/verify?token=${token}"
|
||||
style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
||||
Sign In
|
||||
</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">
|
||||
This link expires in 15 minutes. If you didn't request this, you can safely ignore this email.
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return { success: true, message: GENERIC_MESSAGE };
|
||||
|
|
|
|||
|
|
@ -11,121 +11,14 @@ import { MongoAdapter } from "./oidc-mongodb-adapter.js";
|
|||
import Member from "../models/member.js";
|
||||
import { connectDB } from "./mongoose.js";
|
||||
|
||||
/**
|
||||
* Renders a standalone HTML page in the guild dark style.
|
||||
* Used for OIDC logout/error screens that are served outside Nuxt.
|
||||
*/
|
||||
function guildPageShell(title: string, bodyContent: string, extraStyles = "") {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title} — Ghost Guild</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: #1a1510;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(154, 111, 44, 0.06) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(154, 111, 44, 0.04) 0%, transparent 60%);
|
||||
color: #bfb3a2;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.card {
|
||||
background-color: #2a241c;
|
||||
border: 1px solid rgba(154, 111, 44, 0.15);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 30px rgba(208, 158, 78, 0.06);
|
||||
padding: 2.5rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #d09e4e;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
p { line-height: 1.6; margin-bottom: 1rem; }
|
||||
.subtext { font-size: 0.875rem; color: #6b5f4d; }
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background-color: #d09e4e;
|
||||
color: #1a1510;
|
||||
padding: 0.625rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.btn-primary:hover { background-color: #e0b86e; }
|
||||
.btn-secondary {
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
color: #f0ebe4;
|
||||
padding: 0.625rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(208, 158, 78, 0.4);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
border-color: rgba(224, 184, 110, 0.6);
|
||||
color: #f5e6c5;
|
||||
}
|
||||
.actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1.5rem; }
|
||||
.brand {
|
||||
margin-top: 2rem;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-size: 0.75rem;
|
||||
font-variant: small-caps;
|
||||
letter-spacing: 0.05em;
|
||||
color: #6b5f4d;
|
||||
}
|
||||
.error-detail {
|
||||
margin-top: 1rem;
|
||||
background-color: #1a1510;
|
||||
border: 1px solid rgba(154, 111, 44, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
font-family: 'Ubuntu Mono', 'Courier New', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #6b5f4d;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
}
|
||||
${extraStyles}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
${bodyContent}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
let _provider: InstanceType<typeof Provider> | null = null;
|
||||
|
||||
export async function getOidcProvider() {
|
||||
if (_provider) return _provider;
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const issuer = process.env.OIDC_ISSUER || "https://ghostguild.org";
|
||||
const issuer =
|
||||
process.env.OIDC_ISSUER || config.public.appUrl || "https://ghostguild.org";
|
||||
|
||||
_provider = new Provider(issuer, {
|
||||
adapter: MongoAdapter,
|
||||
|
|
@ -197,32 +90,7 @@ export async function getOidcProvider() {
|
|||
enabled: process.env.NODE_ENV !== "production",
|
||||
},
|
||||
revocation: { enabled: true },
|
||||
rpInitiatedLogout: {
|
||||
enabled: true,
|
||||
logoutSource: async (ctx: any, form: string) => {
|
||||
// oidc-provider generates http:// form actions behind reverse proxy
|
||||
const secureForm = form.replace('http://ghostguild.org', 'https://ghostguild.org');
|
||||
ctx.body = guildPageShell("Sign Out", `
|
||||
<h1>Sign Out</h1>
|
||||
<p>Do you want to sign out of your Ghost Guild session?</p>
|
||||
<p class="subtext">This will sign you out of the wiki and any other connected services.</p>
|
||||
${secureForm}
|
||||
<div class="actions">
|
||||
<button class="btn-primary" form="op.logoutForm" type="submit" value="yes" name="logout">Yes, sign me out</button>
|
||||
<a class="btn-secondary" href="https://wiki.ghostguild.org">Stay signed in</a>
|
||||
</div>
|
||||
`, "form#op\\.logoutForm { display: none; }");
|
||||
},
|
||||
postLogoutSuccessSource: async (ctx: any) => {
|
||||
ctx.body = guildPageShell("Signed Out", `
|
||||
<h1>Signed Out</h1>
|
||||
<p>You have been successfully signed out.</p>
|
||||
<div class="actions">
|
||||
<a class="btn-primary" href="https://wiki.ghostguild.org">Return to Wiki</a>
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
},
|
||||
rpInitiatedLogout: { enabled: true },
|
||||
},
|
||||
|
||||
// Mount all OIDC endpoints under /oidc prefix
|
||||
|
|
@ -247,20 +115,6 @@ export async function getOidcProvider() {
|
|||
},
|
||||
},
|
||||
|
||||
renderError: async (ctx: any, out: Record<string, string>, _error: Error) => {
|
||||
const details = Object.entries(out)
|
||||
.map(([key, value]) => `<strong>${key}:</strong> ${value}`)
|
||||
.join("<br>");
|
||||
ctx.body = guildPageShell("Something Went Wrong", `
|
||||
<h1>Something Went Wrong</h1>
|
||||
<p>An error occurred during authentication. Please try again.</p>
|
||||
<div class="error-detail">${details}</div>
|
||||
<div class="actions">
|
||||
<a class="btn-primary" href="https://wiki.ghostguild.org">Return to Wiki</a>
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
|
||||
// Allow Outline to use PKCE but don't require it
|
||||
pkce: {
|
||||
required: () => false,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue