feat: reskin admin pages to zine design system
Migrate the entire admin section from the dark guild-* Tailwind theme to the zine design system (dashed borders, CSS custom properties, Brygada 1918 + Commit Mono, cream/dark mode palette). - Replace admin top-nav layout with sidebar matching default layout - Reskin dashboard, members, events, series management pages - Reskin events/create and series/create form pages - Add dev-only test login endpoint (GET /api/dev/test-login) - Redirect duplicate admin/dashboard.vue to /admin - Update CLAUDE.md design system docs
This commit is contained in:
parent
f16f9ada64
commit
fcd6f4cdf4
23 changed files with 3845 additions and 3827 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ logs
|
||||||
.fleet
|
.fleet
|
||||||
.idea
|
.idea
|
||||||
/docs/
|
/docs/
|
||||||
|
*.md/
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
.env
|
.env
|
||||||
|
|
|
||||||
21
CLAUDE.md
21
CLAUDE.md
|
|
@ -14,6 +14,8 @@ npm run build # Production build
|
||||||
npm run preview # Preview production build
|
npm run preview # Preview production build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Dev helpers:** `GET /api/dev/test-login` — creates a test admin user and sets auth cookie (dev only, blocked in production). Navigate to this URL to access admin pages during development.
|
||||||
|
|
||||||
No test framework is currently configured.
|
No test framework is currently configured.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
@ -35,8 +37,8 @@ No test framework is currently configured.
|
||||||
- `app/composables/` — State management via `useState()` (no Pinia/Vuex). Key composables: `useAuth`, `useHelcim`, `useMemberPayment`, `useMemberStatus`
|
- `app/composables/` — State management via `useState()` (no Pinia/Vuex). Key composables: `useAuth`, `useHelcim`, `useMemberPayment`, `useMemberStatus`
|
||||||
- `app/config/` — Circle definitions (`circles.js`) and contribution tiers (`contributions.js`) used across frontend and forms
|
- `app/config/` — Circle definitions (`circles.js`) and contribution tiers (`contributions.js`) used across frontend and forms
|
||||||
- `app/middleware/` — Route guards: `auth.js` (member pages), `admin.js` (admin pages), `coming-soon.global.js` (launch gate)
|
- `app/middleware/` — Route guards: `auth.js` (member pages), `admin.js` (admin pages), `coming-soon.global.js` (launch gate)
|
||||||
- `app/layouts/` — `default`, `admin`, `landing`, `coming-soon`
|
- `app/layouts/` — `default` (sidebar, member/public), `admin` (sidebar, admin pages), `landing`, `coming-soon`
|
||||||
- `server/api/` — Nitro API routes organized by feature: `auth/`, `events/`, `members/`, `helcim/`, `series/`, `updates/`, `admin/`, `slack/`
|
- `server/api/` — Nitro API routes organized by feature: `auth/`, `events/`, `members/`, `helcim/`, `series/`, `updates/`, `admin/`, `slack/`, `dev/` (dev-only helpers)
|
||||||
- `server/models/` — Mongoose schemas: `Member`, `Event`, `Series`, `Update`
|
- `server/models/` — Mongoose schemas: `Member`, `Event`, `Series`, `Update`
|
||||||
- `server/utils/` — Service integrations: `mongoose.js`, `helcim.js`, `resend.js`, `slack.ts`, `tickets.js`
|
- `server/utils/` — Service integrations: `mongoose.js`, `helcim.js`, `resend.js`, `slack.ts`, `tickets.js`
|
||||||
|
|
||||||
|
|
@ -48,14 +50,15 @@ Member statuses: `pending_payment`, `active`, `suspended`, `cancelled`.
|
||||||
|
|
||||||
Events support ticketing with circle-specific pricing overrides and can be grouped into Series with bundled passes.
|
Events support ticketing with circle-specific pricing overrides and can be grouped into Series with bundled passes.
|
||||||
|
|
||||||
### Design System
|
### Design System (Zine Direction)
|
||||||
|
|
||||||
- **Colors:** `guild-*` (warm neutral), `candlelight-*` (amber/gold accent), `parchment-*` (cream surfaces), `ember-*` (rust accent), `earth-*` (brown/ochre) — defined in `app/assets/css/main.css`
|
- **Palette:** CSS custom properties in `:root` / `.dark` blocks in `app/assets/css/main.css` — `--bg` (cream/#f4efe4), `--surface`, `--border`, `--candle` (gold accent), `--ember` (rust accent), `--text`, `--text-bright`, `--text-dim`, `--text-faint`, `--parch` (inverted blocks), `--c-community`, `--c-founder`, `--c-practitioner`
|
||||||
- **Circle tokens:** `--color-circle-community`, `--color-circle-founder`, `--color-circle-practitioner` with `-light`, `-dark`, `-bg` variants
|
- **Typography:** Brygada 1918 (serif, display/headings) + Commit Mono (monospace, body/UI/everything structural) — loaded via Google Fonts in `nuxt.config.ts`
|
||||||
- **Typography:** Inter (body), Quietism (display/headers, self-hosted from `public/fonts/`), Ubuntu Mono (code)
|
- **Theme:** `primary: amber`, `neutral: stone` — configured in `app/app.config.ts`. Tailwind `@theme` maps `--font-sans` and `--font-mono` to Commit Mono, `--font-display` to Brygada 1918
|
||||||
- **Theme:** `primary: amber`, `neutral: stone` — configured in `app/app.config.ts`
|
- **Key classes:** `.btn` / `.btn-primary` / `.btn-danger` (buttons), `.field` (form groups), `.badge` (circle badges), `.section-label` (10px uppercase headers), `.dashed-box` (bordered containers), `.section-divider`
|
||||||
- **Effects:** `.candlelight-glow`, `.warm-text`, `.ink-grain`, `.paper-texture`, `.woodcut-border`, `.guild-stamp`, `.halftone-texture`, `.dithered-bg`, `.dithered-warm`
|
- **Visual language:** Dashed borders (1px dashed), cream backgrounds, no rounded corners, text-forward density, minimal decoration
|
||||||
- **Content:** `.prose-guild` class for wiki/long-form content with warm palette and Quietism headings
|
- **Color mode:** `@nuxtjs/color-mode` with preference `system`, fallback `light`. Dark mode via `.dark` class on `<html>`
|
||||||
|
- **Layouts:** `default` (sidebar + main, member/public pages), `admin` (sidebar + main, admin pages), `landing` (horizontal nav, unused)
|
||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,4 @@ export default defineAppConfig({
|
||||||
neutral: "stone",
|
neutral: "stone",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
colorMode: {
|
|
||||||
preference: "light",
|
|
||||||
fallback: "light",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
--text-faint: #8a7e6a;
|
--text-faint: #8a7e6a;
|
||||||
--parch: #2a2015;
|
--parch: #2a2015;
|
||||||
--parch-text: #ede4d0;
|
--parch-text: #ede4d0;
|
||||||
|
--parch-text-dim: #b8ae98;
|
||||||
--c-community: #7a4838;
|
--c-community: #7a4838;
|
||||||
--c-founder: #8a4420;
|
--c-founder: #8a4420;
|
||||||
--c-practitioner: #2a4650;
|
--c-practitioner: #2a4650;
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
--text-faint: #5a5040;
|
--text-faint: #5a5040;
|
||||||
--parch: #ede4d0;
|
--parch: #ede4d0;
|
||||||
--parch-text: #2a2015;
|
--parch-text: #2a2015;
|
||||||
|
--parch-text-dim: #5a5040;
|
||||||
--c-community: #a06850;
|
--c-community: #a06850;
|
||||||
--c-founder: #c06030;
|
--c-founder: #c06030;
|
||||||
--c-practitioner: #4a7080;
|
--c-practitioner: #4a7080;
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="shouldShowBanner" class="w-full">
|
<ClientOnly>
|
||||||
<div
|
<div v-if="shouldShowBanner" class="w-full">
|
||||||
:class="[
|
<div
|
||||||
'backdrop-blur-sm border rounded-lg p-4 flex items-start gap-4',
|
:class="[
|
||||||
statusConfig.bgColor,
|
'backdrop-blur-sm border rounded-lg p-4 flex items-start gap-4',
|
||||||
statusConfig.borderColor,
|
statusConfig.bgColor,
|
||||||
]"
|
statusConfig.borderColor,
|
||||||
>
|
]"
|
||||||
<Icon
|
>
|
||||||
:name="statusConfig.icon"
|
<Icon
|
||||||
:class="['w-5 h-5 flex-shrink-0 mt-0.5', statusConfig.textColor]"
|
:name="statusConfig.icon"
|
||||||
/>
|
:class="['w-5 h-5 flex-shrink-0 mt-0.5', statusConfig.textColor]"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h3 :class="['font-semibold mb-1', statusConfig.textColor]">
|
<h3 :class="['font-semibold mb-1', statusConfig.textColor]">
|
||||||
{{ statusConfig.label }}
|
{{ statusConfig.label }}
|
||||||
</h3>
|
</h3>
|
||||||
<p :class="['text-sm', statusConfig.textColor, 'opacity-90']">
|
<p :class="['text-sm', statusConfig.textColor, 'opacity-90']">
|
||||||
{{ bannerMessage }}
|
{{ bannerMessage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 flex-shrink-0">
|
<div class="flex items-center gap-2 flex-shrink-0">
|
||||||
<!-- Payment button for pending payment status -->
|
<!-- Payment button for pending payment status -->
|
||||||
<UButton
|
<UButton
|
||||||
v-if="isPendingPayment && nextAction"
|
v-if="isPendingPayment && nextAction"
|
||||||
:color="getButtonColor(nextAction.color)"
|
:color="getButtonColor(nextAction.color)"
|
||||||
size="sm"
|
size="sm"
|
||||||
:loading="isProcessingPayment"
|
:loading="isProcessingPayment"
|
||||||
@click="handleActionClick"
|
@click="handleActionClick"
|
||||||
class="whitespace-nowrap"
|
class="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ isProcessingPayment ? "Processing..." : nextAction.label }}
|
{{ isProcessingPayment ? "Processing..." : nextAction.label }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|
||||||
<!-- Link button for other actions -->
|
<!-- Link button for other actions -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-else-if="nextAction && nextAction.link"
|
v-else-if="nextAction && nextAction.link"
|
||||||
:to="nextAction.link"
|
:to="nextAction.link"
|
||||||
:class="[
|
:class="[
|
||||||
'px-4 py-2 rounded-lg font-medium text-sm whitespace-nowrap transition-all',
|
'px-4 py-2 rounded-lg font-medium text-sm whitespace-nowrap transition-all',
|
||||||
getActionButtonClass(nextAction.color),
|
getActionButtonClass(nextAction.color),
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ nextAction.label }}
|
{{ nextAction.label }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="dismissible"
|
v-if="dismissible"
|
||||||
@click="isDismissed = true"
|
@click="isDismissed = true"
|
||||||
class="text-guild-400 hover:text-guild-200 transition-colors"
|
class="text-guild-400 hover:text-guild-200 transition-colors"
|
||||||
:aria-label="`Dismiss ${statusConfig.label} banner`"
|
:aria-label="`Dismiss ${statusConfig.label} banner`"
|
||||||
>
|
>
|
||||||
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
<Icon name="heroicons:x-mark" class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ const props = defineProps({
|
||||||
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"',
|
default: 'e.g., "tomorrow at 3pm", "next Friday at 9am", "in 2 hours"',
|
||||||
},
|
},
|
||||||
inputClass: {
|
inputClass: {
|
||||||
type: String,
|
type: [String, Object],
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
required: {
|
required: {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
.parchment-inset :deep(p) {
|
.parchment-inset :deep(p) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #b8ae98;
|
color: var(--parch-text-dim);
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,20 @@
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<slot name="right">
|
<slot name="right">
|
||||||
<template v-if="memberData">
|
<ClientOnly>
|
||||||
Signed in as {{ memberData.name }}
|
<template v-if="memberData">
|
||||||
<template v-if="memberData.circle">
|
Signed in as {{ memberData.name }}
|
||||||
· {{ memberData.circle }}
|
<template v-if="memberData.circle">
|
||||||
|
· {{ memberData.circle }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
<template v-else>
|
A cooperative for game developers
|
||||||
A cooperative for game developers
|
</template>
|
||||||
</template>
|
<template #fallback>
|
||||||
|
A cooperative for game developers
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
</slot>
|
</slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,350 +1,267 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-guild-950">
|
<div class="site">
|
||||||
<!-- Admin Navigation -->
|
<!-- Desktop Sidebar -->
|
||||||
<nav class="bg-guild-900 border-b border-guild-700 shadow-sm">
|
<aside class="sidebar sidebar-desktop">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<NuxtLink to="/" class="sidebar-brand">Ghost Guild</NuxtLink>
|
||||||
<div class="flex justify-between items-center py-4">
|
|
||||||
<div class="flex items-center gap-8">
|
<div class="sidebar-body">
|
||||||
<NuxtLink
|
<div class="sidebar-section">Admin</div>
|
||||||
to="/"
|
<ul class="sidebar-nav">
|
||||||
class="text-xl font-bold font-serif warm-text text-guild-100 hover:text-candlelight-400"
|
<li>
|
||||||
>
|
<NuxtLink to="/admin" :class="{ active: route.path === '/admin' }">
|
||||||
Ghost Guild
|
Dashboard
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/members" :class="{ active: route.path.startsWith('/admin/members') }">
|
||||||
|
Members
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/events" :class="{ active: route.path.startsWith('/admin/events') }">
|
||||||
|
Events
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/series-management" :class="{ active: route.path.includes('/admin/series') }">
|
||||||
|
Series
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center gap-1">
|
<div class="sidebar-section">Site</div>
|
||||||
<NuxtLink
|
<ul class="sidebar-nav">
|
||||||
to="/admin"
|
<li><NuxtLink to="/member/dashboard">Your Dashboard</NuxtLink></li>
|
||||||
:class="[
|
<li><NuxtLink to="/">Public Site</NuxtLink></li>
|
||||||
'px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200',
|
</ul>
|
||||||
$route.path === '/admin'
|
|
||||||
? 'bg-candlelight-900/30 text-candlelight-400 shadow-sm'
|
|
||||||
: 'text-guild-400 hover:text-candlelight-400 hover:bg-candlelight-900/10',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 inline-block mr-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2 2z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 7V3a2 2 0 012-2h4a2 2 0 012 2v4m-6 4h4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Dashboard
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/members"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 inline-block mr-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Members
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/events"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 inline-block mr-2"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Events
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/series-management"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 inline-block mr-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Series
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<!-- User Menu -->
|
|
||||||
<div
|
|
||||||
class="relative"
|
|
||||||
@click="showUserMenu = !showUserMenu"
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-8 h-8 bg-candlelight-600 rounded-full flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 text-white"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span class="hidden md:block text-sm font-medium text-guild-100"
|
|
||||||
>Admin</span
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 text-guild-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M19 9l-7 7-7-7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- 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"
|
|
||||||
>
|
|
||||||
<NuxtLink
|
|
||||||
to="/"
|
|
||||||
class="flex items-center px-4 py-2 text-sm text-guild-100 hover:bg-guild-700"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 mr-3 text-guild-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
View Site
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/settings"
|
|
||||||
class="flex items-center px-4 py-2 text-sm text-guild-100 hover:bg-guild-700"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 mr-3 text-guild-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Settings
|
|
||||||
</NuxtLink>
|
|
||||||
<hr class="my-1 border-guild-700" />
|
|
||||||
<button
|
|
||||||
@click="logout"
|
|
||||||
class="flex items-center w-full px-4 py-2 text-sm text-ember-400 hover:bg-ember-900/20"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 mr-3 text-ember-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile Navigation -->
|
<div class="sidebar-meta">
|
||||||
<div class="md:hidden bg-guild-900 border-b border-guild-700">
|
<span class="admin-tag">admin</span><br>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<a href="#" @click.prevent="logout">Sign out</a>
|
||||||
<div class="flex items-center gap-2 py-3 overflow-x-auto">
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/members"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
Members
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/events"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
Events
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
|
||||||
to="/admin/series-management"
|
|
||||||
: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',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
Series
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Mobile Header -->
|
||||||
|
<div class="mobile-header">
|
||||||
|
<NuxtLink to="/admin" class="brand">Ghost Guild</NuxtLink>
|
||||||
|
<button class="btn" @click="isMobileMenuOpen = true">Menu</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Page Content -->
|
<!-- Main Content -->
|
||||||
<main>
|
<main class="main">
|
||||||
|
<TopStrip :page-path="currentPageName" />
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Mobile Drawer -->
|
||||||
<footer class="bg-guild-900 border-t border-guild-700 mt-auto">
|
<USlideover v-model:open="isMobileMenuOpen" side="left">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<template #body>
|
||||||
<div class="py-8 text-center">
|
<aside class="sidebar sidebar-mobile">
|
||||||
<p class="text-sm text-guild-400">
|
<NuxtLink to="/" class="sidebar-brand" @click="isMobileMenuOpen = false">Ghost Guild</NuxtLink>
|
||||||
© 2025 Ghost Guild. Admin Panel.
|
|
||||||
</p>
|
<div class="sidebar-body">
|
||||||
</div>
|
<div class="sidebar-section">Admin</div>
|
||||||
</div>
|
<ul class="sidebar-nav">
|
||||||
</footer>
|
<li>
|
||||||
|
<NuxtLink to="/admin" :class="{ active: route.path === '/admin' }" @click="isMobileMenuOpen = false">
|
||||||
|
Dashboard
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/members" :class="{ active: route.path.startsWith('/admin/members') }" @click="isMobileMenuOpen = false">
|
||||||
|
Members
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/events" :class="{ active: route.path.startsWith('/admin/events') }" @click="isMobileMenuOpen = false">
|
||||||
|
Events
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NuxtLink to="/admin/series-management" :class="{ active: route.path.includes('/admin/series') }" @click="isMobileMenuOpen = false">
|
||||||
|
Series
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="sidebar-section">Site</div>
|
||||||
|
<ul class="sidebar-nav">
|
||||||
|
<li><NuxtLink to="/member/dashboard" @click="isMobileMenuOpen = false">Your Dashboard</NuxtLink></li>
|
||||||
|
<li><NuxtLink to="/" @click="isMobileMenuOpen = false">Public Site</NuxtLink></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-meta">
|
||||||
|
<span class="admin-tag">admin</span><br>
|
||||||
|
<a href="#" @click.prevent="logout">Sign out</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
</USlideover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const showUserMenu = ref(false);
|
const route = useRoute()
|
||||||
|
const isMobileMenuOpen = ref(false)
|
||||||
|
|
||||||
// Close user menu when clicking outside
|
const currentPageName = computed(() => {
|
||||||
const vClickOutside = {
|
const path = route.path
|
||||||
beforeMount(el, binding) {
|
if (path === '/admin') return 'admin'
|
||||||
el.clickOutsideEvent = (event) => {
|
return path.slice(1).replace(/\//g, ' / ')
|
||||||
if (!(el === event.target || el.contains(event.target))) {
|
})
|
||||||
binding.value();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("click", el.clickOutsideEvent);
|
|
||||||
},
|
|
||||||
unmounted(el) {
|
|
||||||
document.removeEventListener("click", el.clickOutsideEvent);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/auth/logout", { method: "POST" });
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
await navigateTo("/");
|
await navigateTo('/')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Logout failed:", error);
|
console.error('Logout failed:', error)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.site {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin-left: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- SIDEBAR ---- */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 220px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg);
|
||||||
|
border-right: 1px dashed var(--border);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-mobile {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-brand {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--candle);
|
||||||
|
padding: 24px 24px 16px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.sidebar-brand:hover { text-decoration: none; }
|
||||||
|
|
||||||
|
.sidebar-body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin: 20px 0 8px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
list-style: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav li {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a {
|
||||||
|
display: block;
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--surface);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a.active {
|
||||||
|
color: var(--text-bright);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-meta {
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-meta a {
|
||||||
|
color: var(--candle-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- MOBILE ---- */
|
||||||
|
.sidebar-desktop {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--candle);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar-desktop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mobile-header {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--bg);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-guild-900">
|
<div class="landing">
|
||||||
<!-- Horizontal Navigation -->
|
<!-- Horizontal Navigation -->
|
||||||
<nav class="w-full px-6 md:px-8 py-4">
|
<nav class="landing-nav">
|
||||||
<div class="max-w-6xl mx-auto flex items-center justify-between">
|
<div class="landing-nav-inner">
|
||||||
<!-- Logo/Wordmark -->
|
<NuxtLink to="/" class="landing-brand">Ghost Guild</NuxtLink>
|
||||||
<NuxtLink to="/" class="text-display-sm font-bold text-candlelight-400 warm-text tracking-wide">
|
<div class="landing-links">
|
||||||
Ghost Guild
|
<NuxtLink to="/about">About</NuxtLink>
|
||||||
</NuxtLink>
|
<NuxtLink to="/events">Events</NuxtLink>
|
||||||
|
|
||||||
<!-- Desktop Navigation Links -->
|
|
||||||
<div class="hidden md:flex items-center gap-8">
|
|
||||||
<NuxtLink
|
|
||||||
to="/about"
|
|
||||||
class="text-guild-300 hover:text-guild-100 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/events"
|
|
||||||
class="text-guild-300 hover:text-guild-100 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Events
|
|
||||||
</NuxtLink>
|
|
||||||
<template v-if="isAuthenticated">
|
<template v-if="isAuthenticated">
|
||||||
<NuxtLink
|
<NuxtLink to="/member/dashboard" class="landing-cta">Dashboard</NuxtLink>
|
||||||
to="/member/dashboard"
|
|
||||||
class="text-primary-400 hover:text-primary-300 transition-colors text-sm font-medium"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button class="landing-cta" @click="openLoginModal">Sign In</button>
|
||||||
@click="openLoginModal"
|
|
||||||
class="text-primary-400 hover:text-primary-300 transition-colors text-sm font-medium"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn landing-menu-btn" @click="isMobileMenuOpen = true">Menu</button>
|
||||||
<!-- Mobile Menu Button -->
|
|
||||||
<UButton
|
|
||||||
icon="i-lucide-menu"
|
|
||||||
color="neutral"
|
|
||||||
variant="ghost"
|
|
||||||
size="md"
|
|
||||||
class="md:hidden"
|
|
||||||
@click="isMobileMenuOpen = true"
|
|
||||||
aria-label="Open menu"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<main>
|
<main>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="border-t border-guild-800 py-8 px-6 md:px-8">
|
|
||||||
<div class="max-w-6xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
|
||||||
<p class="text-guild-500 text-sm">
|
|
||||||
© {{ currentYear }} Ghost Guild
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="mailto:hello@ghostguild.org"
|
|
||||||
class="text-guild-500 hover:text-guild-300 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
hello@ghostguild.org
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- Mobile Navigation Drawer -->
|
<!-- Mobile Navigation Drawer -->
|
||||||
<USlideover v-model:open="isMobileMenuOpen" side="right">
|
<USlideover v-model:open="isMobileMenuOpen" side="right">
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="p-6 space-y-6">
|
<div class="landing-mobile-nav">
|
||||||
<NuxtLink
|
<NuxtLink to="/about" @click="isMobileMenuOpen = false">About</NuxtLink>
|
||||||
to="/about"
|
<NuxtLink to="/events" @click="isMobileMenuOpen = false">Events</NuxtLink>
|
||||||
class="block text-guild-200 hover:text-guild-100 transition-colors text-lg"
|
|
||||||
@click="isMobileMenuOpen = false"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</NuxtLink>
|
|
||||||
<NuxtLink
|
|
||||||
to="/events"
|
|
||||||
class="block text-guild-200 hover:text-guild-100 transition-colors text-lg"
|
|
||||||
@click="isMobileMenuOpen = false"
|
|
||||||
>
|
|
||||||
Events
|
|
||||||
</NuxtLink>
|
|
||||||
<template v-if="isAuthenticated">
|
<template v-if="isAuthenticated">
|
||||||
<NuxtLink
|
<NuxtLink to="/member/dashboard" @click="isMobileMenuOpen = false">Dashboard</NuxtLink>
|
||||||
to="/member/dashboard"
|
|
||||||
class="block text-primary-400 hover:text-primary-300 transition-colors text-lg font-medium"
|
|
||||||
@click="isMobileMenuOpen = false"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button @click="handleMobileSignIn">Sign In</button>
|
||||||
@click="handleMobileSignIn"
|
|
||||||
class="block text-primary-400 hover:text-primary-300 transition-colors text-lg font-medium"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
|
|
||||||
<!-- Login Modal -->
|
|
||||||
<LoginModal />
|
<LoginModal />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -122,10 +47,101 @@ const { isAuthenticated } = useAuth()
|
||||||
const { openLoginModal } = useLoginModal()
|
const { openLoginModal } = useLoginModal()
|
||||||
|
|
||||||
const isMobileMenuOpen = ref(false)
|
const isMobileMenuOpen = ref(false)
|
||||||
const currentYear = new Date().getFullYear()
|
|
||||||
|
|
||||||
const handleMobileSignIn = () => {
|
const handleMobileSignIn = () => {
|
||||||
isMobileMenuOpen.value = false
|
isMobileMenuOpen.value = false
|
||||||
openLoginModal()
|
openLoginModal()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.landing {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-nav {
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-nav-inner {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-brand {
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--candle);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.landing-brand:hover { text-decoration: none; }
|
||||||
|
|
||||||
|
.landing-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-links a,
|
||||||
|
.landing-links button {
|
||||||
|
font-family: 'Commit Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.landing-links a:hover,
|
||||||
|
.landing-links button:hover {
|
||||||
|
color: var(--text-bright);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-cta {
|
||||||
|
color: var(--candle) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-menu-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landing-mobile-nav {
|
||||||
|
padding: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.landing-mobile-nav a,
|
||||||
|
.landing-mobile-nav button {
|
||||||
|
font-family: 'Commit Mono', monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.landing-mobile-nav a:hover,
|
||||||
|
.landing-mobile-nav button:hover {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.landing-links { display: none; }
|
||||||
|
.landing-menu-btn { display: block; }
|
||||||
|
.landing-nav { padding: 0 20px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,359 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div />
|
||||||
<div class="bg-guild-900 border-b border-guild-700">
|
|
||||||
<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">
|
|
||||||
Manage Ghost Guild members, events, and community operations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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="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">
|
|
||||||
{{ stats.totalMembers || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-guild-900 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">
|
|
||||||
{{ stats.activeEvents || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-guild-900 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">
|
|
||||||
${{ stats.monthlyRevenue || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-guild-900 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">
|
|
||||||
{{ stats.pendingSlackInvites || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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="text-center">
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-display-sm font-semibold text-guild-100 mb-2">
|
|
||||||
Add New Member
|
|
||||||
</h3>
|
|
||||||
<p class="text-guild-400 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"
|
|
||||||
>
|
|
||||||
Manage Members
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-guild-900 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"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-8 h-8 text-candlelight-400"
|
|
||||||
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-display-sm font-semibold text-guild-100 mb-2">
|
|
||||||
Create Event
|
|
||||||
</h3>
|
|
||||||
<p class="text-guild-400 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"
|
|
||||||
>
|
|
||||||
Manage Events
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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="flex justify-between items-center">
|
|
||||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
|
||||||
Recent Members
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
@click="navigateTo('/admin/members-working')"
|
|
||||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
></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"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-guild-100">
|
|
||||||
{{ member.name }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-guild-400">
|
|
||||||
{{ member.email }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<span
|
|
||||||
:class="getCircleBadgeClasses(member.circle)"
|
|
||||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1"
|
|
||||||
>
|
|
||||||
{{ member.circle }}
|
|
||||||
</span>
|
|
||||||
<p class="text-xs text-guild-500 text-ui-mono">
|
|
||||||
{{ formatDate(member.createdAt) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center py-6 text-guild-500">
|
|
||||||
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="flex justify-between items-center">
|
|
||||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
|
||||||
Upcoming Events
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
@click="navigateTo('/admin/events-working')"
|
|
||||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
></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"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-guild-100">
|
|
||||||
{{ event.title }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-guild-400">
|
|
||||||
{{ formatDateTime(event.startDate) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<span
|
|
||||||
:class="getEventTypeBadgeClasses(event.eventType)"
|
|
||||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1"
|
|
||||||
>
|
|
||||||
{{ event.eventType }}
|
|
||||||
</span>
|
|
||||||
<p class="text-xs text-guild-500">
|
|
||||||
{{ event.location || "Online" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center py-6 text-guild-500">
|
|
||||||
No upcoming events
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: 'admin',
|
||||||
});
|
middleware: 'admin',
|
||||||
|
})
|
||||||
|
|
||||||
const { data: dashboardData, pending } = await useFetch("/api/admin/dashboard");
|
// Redirect to the main admin page
|
||||||
|
await navigateTo('/admin', { replace: true })
|
||||||
const stats = computed(() => dashboardData.value?.stats || {});
|
|
||||||
const recentMembers = computed(() => dashboardData.value?.recentMembers || []);
|
|
||||||
const upcomingEvents = computed(
|
|
||||||
() => dashboardData.value?.upcomingEvents || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
return classes[type] || "bg-guild-800 text-guild-300";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
return new Date(dateString).toLocaleDateString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (dateString) => {
|
|
||||||
return new Date(dateString).toLocaleDateString(undefined, {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,312 +1,98 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="admin-dash">
|
||||||
<div class="bg-guild-900 border-b border-guild-700">
|
<!-- Page Header -->
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="page-header">
|
||||||
<div class="py-6">
|
<h1>Admin Dashboard</h1>
|
||||||
<h1 class="text-display font-bold text-guild-100">Admin Dashboard</h1>
|
<p>Members, events, and community operations</p>
|
||||||
<p class="text-guild-400">
|
</div>
|
||||||
Manage Ghost Guild members, events, and community operations
|
|
||||||
</p>
|
<!-- Stats + Quick Actions row -->
|
||||||
|
<div class="content-row">
|
||||||
|
<div class="content-block">
|
||||||
|
<div class="section-label">Overview</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-key">Total Members</span>
|
||||||
|
<span class="stat-val">{{ stats.totalMembers || 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-key">Active Events</span>
|
||||||
|
<span class="stat-val">{{ stats.activeEvents || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-key">Monthly Revenue</span>
|
||||||
|
<span class="stat-val">${{ stats.monthlyRevenue || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-key">Pending Slack Invites</span>
|
||||||
|
<span class="stat-val">{{ stats.pendingSlackInvites || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-block">
|
||||||
|
<div class="section-label">Quick Actions</div>
|
||||||
|
<NuxtLink to="/admin/members" class="action-link">
|
||||||
|
Manage Members<span class="arrow">→</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/admin/events" class="action-link">
|
||||||
|
Manage Events<span class="arrow">→</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/admin/events/create" class="action-link">
|
||||||
|
Create Event<span class="arrow">→</span>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink to="/admin/series/create" class="action-link">
|
||||||
|
Create Series<span class="arrow">→</span>
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<!-- Recent Activity row -->
|
||||||
<!-- Quick Stats -->
|
<div class="content-row">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
<div class="content-block">
|
||||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
<div class="section-label">Recent Members</div>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
<div v-if="pending" class="loading-inline">
|
||||||
<p class="text-sm text-guild-400">Total Members</p>
|
<div class="spinner spinner-sm" />
|
||||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
|
||||||
{{ stats.totalMembers || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
<div v-else-if="recentMembers.length" class="item-list">
|
||||||
<div class="flex items-center justify-between">
|
<div v-for="member in recentMembers" :key="member._id" class="item-row">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-guild-400">Active Events</p>
|
<span class="item-name">{{ member.name }}</span>
|
||||||
<p class="text-2xl font-bold text-candlelight-400 text-ui-mono">
|
<span class="item-sub">{{ member.email }}</span>
|
||||||
{{ stats.activeEvents || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="item-meta">
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
<span class="badge" :class="member.circle">{{ member.circle }}</span>
|
||||||
>
|
<span class="item-date">{{ formatDate(member.createdAt) }}</span>
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="empty-state">No recent members</div>
|
||||||
|
|
||||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
<NuxtLink to="/admin/members" class="section-link">View all members →</NuxtLink>
|
||||||
<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">
|
|
||||||
${{ stats.monthlyRevenue || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-guild-900 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">
|
|
||||||
{{ stats.pendingSlackInvites || 0 }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-12 h-12 bg-candlelight-900/20 rounded-xl flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-candlelight-400"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<div class="content-block">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
<div class="section-label">Upcoming Events</div>
|
||||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
|
||||||
<div class="text-center">
|
<div v-if="pending" class="loading-inline">
|
||||||
<div
|
<div class="spinner spinner-sm" />
|
||||||
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"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-display-sm font-semibold mb-2 text-guild-100">
|
|
||||||
Add New Member
|
|
||||||
</h3>
|
|
||||||
<p class="text-guild-400 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"
|
|
||||||
>
|
|
||||||
Manage Members
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-guild-900 rounded-lg shadow p-6">
|
<div v-else-if="upcomingEvents.length" class="item-list">
|
||||||
<div class="text-center">
|
<div v-for="event in upcomingEvents" :key="event._id" class="item-row">
|
||||||
<div
|
<div>
|
||||||
class="w-16 h-16 bg-candlelight-900/20 rounded-2xl flex items-center justify-center mx-auto mb-4"
|
<span class="item-name">{{ event.title }}</span>
|
||||||
>
|
<span class="item-sub">{{ formatDateTime(event.startDate) }}</span>
|
||||||
<svg
|
|
||||||
class="w-8 h-8 text-candlelight-400"
|
|
||||||
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>
|
</div>
|
||||||
<h3 class="text-display-sm font-semibold mb-2 text-guild-100">
|
<div class="item-meta">
|
||||||
Create Event
|
<span class="badge" :class="event.eventType">{{ event.eventType }}</span>
|
||||||
</h3>
|
<span class="item-date">{{ event.location || 'Online' }}</span>
|
||||||
<p class="text-guild-400 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"
|
|
||||||
>
|
|
||||||
Manage Events
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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="flex justify-between items-center">
|
|
||||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
|
||||||
Recent Members
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
@click="navigateTo('/admin/members')"
|
|
||||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
></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"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-guild-100">
|
|
||||||
{{ member.name }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-guild-400">
|
|
||||||
{{ member.email }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<span
|
|
||||||
:class="getCircleBadgeClasses(member.circle)"
|
|
||||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1"
|
|
||||||
>
|
|
||||||
{{ member.circle }}
|
|
||||||
</span>
|
|
||||||
<p class="text-xs text-guild-500 text-ui-mono">
|
|
||||||
{{ formatDate(member.createdAt) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center py-6 text-guild-500">
|
|
||||||
No recent members
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="empty-state">No upcoming events</div>
|
||||||
|
|
||||||
<div class="bg-guild-900 rounded-lg shadow">
|
<NuxtLink to="/admin/events" class="section-link">View all events →</NuxtLink>
|
||||||
<div class="px-6 py-4 border-b border-guild-700">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h3 class="text-display-sm font-semibold text-guild-100">
|
|
||||||
Upcoming Events
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
@click="navigateTo('/admin/events')"
|
|
||||||
class="text-sm text-candlelight-400 hover:text-candlelight-300"
|
|
||||||
>
|
|
||||||
View All
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
></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"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-guild-100">
|
|
||||||
{{ event.title }}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm text-guild-400">
|
|
||||||
{{ formatDateTime(event.startDate) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<span
|
|
||||||
:class="getEventTypeBadgeClasses(event.eventType)"
|
|
||||||
class="inline-flex px-2 py-1 text-xs font-semibold rounded-full mb-1"
|
|
||||||
>
|
|
||||||
{{ event.eventType }}
|
|
||||||
</span>
|
|
||||||
<p class="text-xs text-guild-500">
|
|
||||||
{{ event.location || "Online" }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="text-center py-6 text-guild-500">
|
|
||||||
No upcoming events
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -314,46 +100,233 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "admin",
|
layout: 'admin',
|
||||||
});
|
middleware: 'admin',
|
||||||
|
})
|
||||||
|
|
||||||
const { data: dashboardData, pending } = await useFetch("/api/admin/dashboard");
|
const { data: dashboardData, pending } = await useFetch('/api/admin/dashboard')
|
||||||
|
|
||||||
const stats = computed(() => dashboardData.value?.stats || {});
|
const stats = computed(() => dashboardData.value?.stats || {})
|
||||||
const recentMembers = computed(() => dashboardData.value?.recentMembers || []);
|
const recentMembers = computed(() => dashboardData.value?.recentMembers || [])
|
||||||
const upcomingEvents = computed(
|
const upcomingEvents = computed(() => dashboardData.value?.upcomingEvents || [])
|
||||||
() => dashboardData.value?.upcomingEvents || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
return classes[circle] || "bg-guild-800 text-guild-300";
|
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
return classes[type] || "bg-guild-800 text-guild-300";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
return new Date(dateString).toLocaleDateString();
|
return new Date(dateString).toLocaleDateString()
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatDateTime = (dateString) => {
|
const formatDateTime = (dateString) => {
|
||||||
return new Date(dateString).toLocaleDateString(undefined, {
|
return new Date(dateString).toLocaleDateString(undefined, {
|
||||||
month: "short",
|
month: 'short',
|
||||||
day: "numeric",
|
day: 'numeric',
|
||||||
hour: "numeric",
|
hour: 'numeric',
|
||||||
minute: "2-digit",
|
minute: '2-digit',
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-dash {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- PAGE HEADER ---- */
|
||||||
|
.page-header {
|
||||||
|
padding: 28px 28px 20px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-bright);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- CONTENT GRID ---- */
|
||||||
|
.content-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding: 24px 28px;
|
||||||
|
border-right: 1px dashed var(--border);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- STATS ---- */
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-key {
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-val {
|
||||||
|
color: var(--text-bright);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- QUICK ACTIONS ---- */
|
||||||
|
.action-link {
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
padding: 14px 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link:hover {
|
||||||
|
border-color: var(--candle-faint);
|
||||||
|
color: var(--candle);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link .arrow {
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-link:hover .arrow {
|
||||||
|
color: var(--candle-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- ITEM LIST (members / events) ---- */
|
||||||
|
.item-list {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
display: block;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-sub {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta {
|
||||||
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-date {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- SECTION LINK ---- */
|
||||||
|
.section-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--candle);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- STATES ---- */
|
||||||
|
.loading-inline {
|
||||||
|
padding: 24px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px dashed var(--candle);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-sm {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 24px 0;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- RESPONSIVE ---- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 24px 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,143 +1,123 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="create-form">
|
||||||
<div class="bg-guild-900 border-b border-guild-700">
|
<div class="page-header">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<NuxtLink to="/admin/series-management" class="back-link">← Series</NuxtLink>
|
||||||
<div class="py-6">
|
<h1>Create New Series</h1>
|
||||||
<div class="flex items-center gap-4 mb-2">
|
<p>Create a new event series to group related events together</p>
|
||||||
<NuxtLink to="/admin/series-management" class="text-guild-500 hover:text-guild-100">
|
|
||||||
<Icon name="heroicons:arrow-left" class="w-5 h-5" />
|
|
||||||
</NuxtLink>
|
|
||||||
<h1 class="text-display font-bold text-guild-100">Create New Series</h1>
|
|
||||||
</div>
|
|
||||||
<p class="text-guild-400">Create a new event series to group related events together</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="form-body">
|
||||||
<!-- Error Summary -->
|
<!-- 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="error-box">
|
||||||
<div class="flex">
|
<Icon name="heroicons:exclamation-circle" class="box-icon" />
|
||||||
<Icon name="heroicons:exclamation-circle" class="w-5 h-5 text-ember-400 mr-3 mt-0.5" />
|
<div>
|
||||||
<div>
|
<strong>Please fix the following errors:</strong>
|
||||||
<h3 class="text-sm font-medium text-ember-400 mb-2">Please fix the following errors:</h3>
|
<ul>
|
||||||
<ul class="text-sm text-ember-400 space-y-1">
|
<li v-for="error in formErrors" :key="error">{{ error }}</li>
|
||||||
<li v-for="error in formErrors" :key="error">• {{ error }}</li>
|
</ul>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Success Message -->
|
<!-- 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="success-box">
|
||||||
<div class="flex">
|
<Icon name="heroicons:check-circle" class="box-icon" />
|
||||||
<Icon name="heroicons:check-circle" class="w-5 h-5 text-candlelight-400 mr-3 mt-0.5" />
|
<div>
|
||||||
<div>
|
<strong>Series created successfully!</strong>
|
||||||
<h3 class="text-sm font-medium text-candlelight-400">Series created successfully!</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="createSeries">
|
<form @submit.prevent="createSeries">
|
||||||
<!-- Series Information -->
|
<!-- Series Information -->
|
||||||
<div class="mb-8">
|
<div class="form-section">
|
||||||
<h2 class="text-lg font-semibold text-guild-100 mb-4">Series Information</h2>
|
<h2 class="section-heading">Series Information</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="field">
|
||||||
<div>
|
<label>
|
||||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
Series Title <span class="required">*</span>
|
||||||
Series Title <span class="text-ember-400">*</span>
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
v-model="seriesForm.title"
|
||||||
v-model="seriesForm.title"
|
type="text"
|
||||||
type="text"
|
placeholder="e.g., Cooperative Game Development Fundamentals"
|
||||||
placeholder="e.g., Cooperative Game Development Fundamentals"
|
required
|
||||||
required
|
:class="{ 'has-error': fieldErrors.title }"
|
||||||
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"
|
@input="generateSlugFromTitle"
|
||||||
:class="{ 'border-ember-700 focus:ring-ember-500': fieldErrors.title }"
|
/>
|
||||||
@input="generateSlugFromTitle"
|
<p v-if="fieldErrors.title" class="field-error">{{ fieldErrors.title }}</p>
|
||||||
/>
|
</div>
|
||||||
<p v-if="fieldErrors.title" class="mt-1 text-sm text-ember-400">{{ fieldErrors.title }}</p>
|
|
||||||
|
<div v-if="generatedSlug" class="field">
|
||||||
|
<label>Generated Series ID</label>
|
||||||
|
<div class="slug-display">
|
||||||
|
{{ generatedSlug }}
|
||||||
|
</div>
|
||||||
|
<p class="help-text">
|
||||||
|
This unique identifier will be automatically generated from your title
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label>
|
||||||
|
Series Description <span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="seriesForm.description"
|
||||||
|
placeholder="Describe what the series covers and its goals"
|
||||||
|
required
|
||||||
|
rows="4"
|
||||||
|
:class="{ 'has-error': fieldErrors.description }"
|
||||||
|
></textarea>
|
||||||
|
<p v-if="fieldErrors.description" class="field-error">{{ fieldErrors.description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>Series Type</label>
|
||||||
|
<select v-model="seriesForm.type">
|
||||||
|
<option value="workshop_series">Workshop Series</option>
|
||||||
|
<option value="recurring_meetup">Recurring Meetup</option>
|
||||||
|
<option value="multi_day">Multi-Day Event</option>
|
||||||
|
<option value="course">Course</option>
|
||||||
|
<option value="tournament">Tournament</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="generatedSlug">
|
<div class="field">
|
||||||
<label class="block text-sm font-medium text-guild-100 mb-2">Generated Series ID</label>
|
<label>Total Events Planned</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">
|
<input
|
||||||
{{ generatedSlug }}
|
v-model.number="seriesForm.totalEvents"
|
||||||
</div>
|
type="number"
|
||||||
<p class="mt-1 text-sm text-guild-500">
|
min="1"
|
||||||
This unique identifier will be automatically generated from your title
|
placeholder="e.g., 4"
|
||||||
</p>
|
/>
|
||||||
</div>
|
<p class="help-text">How many events will be in this series? (optional)</p>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-guild-100 mb-2">
|
|
||||||
Series Description <span class="text-ember-400">*</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 }"
|
|
||||||
></textarea>
|
|
||||||
<p v-if="fieldErrors.description" class="mt-1 text-sm text-ember-400">{{ 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>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<option value="workshop_series">Workshop Series</option>
|
|
||||||
<option value="recurring_meetup">Recurring Meetup</option>
|
|
||||||
<option value="multi_day">Multi-Day Event</option>
|
|
||||||
<option value="course">Course</option>
|
|
||||||
<option value="tournament">Tournament</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-guild-100 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"
|
|
||||||
/>
|
|
||||||
<p class="text-sm text-guild-500 mt-1">How many events will be in this series? (optional)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form Actions -->
|
<!-- Form Actions -->
|
||||||
<div class="flex justify-between items-center pt-6 border-t border-guild-700">
|
<div class="form-actions">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/admin/series-management"
|
to="/admin/series-management"
|
||||||
class="px-4 py-2 text-guild-400 hover:text-guild-100 font-medium"
|
class="btn"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="form-actions-right">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="createAndAddEvent"
|
@click="createAndAddEvent"
|
||||||
:disabled="creating"
|
: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="btn"
|
||||||
>
|
>
|
||||||
{{ creating ? 'Creating...' : 'Create & Add Event' }}
|
{{ creating ? 'Creating...' : 'Create & Add Event' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="creating"
|
: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="btn btn-primary"
|
||||||
>
|
>
|
||||||
{{ creating ? 'Creating...' : 'Create Series' }}
|
{{ creating ? 'Creating...' : 'Create Series' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -150,7 +130,8 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'admin'
|
layout: 'admin',
|
||||||
|
middleware: 'admin',
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -190,22 +171,22 @@ const generateSlugFromTitle = () => {
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
formErrors.value = []
|
formErrors.value = []
|
||||||
fieldErrors.value = {}
|
fieldErrors.value = {}
|
||||||
|
|
||||||
if (!seriesForm.title.trim()) {
|
if (!seriesForm.title.trim()) {
|
||||||
formErrors.value.push('Series title is required')
|
formErrors.value.push('Series title is required')
|
||||||
fieldErrors.value.title = 'Please enter a series title'
|
fieldErrors.value.title = 'Please enter a series title'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!seriesForm.description.trim()) {
|
if (!seriesForm.description.trim()) {
|
||||||
formErrors.value.push('Series description is required')
|
formErrors.value.push('Series description is required')
|
||||||
fieldErrors.value.description = 'Please provide a description for the series'
|
fieldErrors.value.description = 'Please provide a description for the series'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!generatedSlug.value) {
|
if (!generatedSlug.value) {
|
||||||
formErrors.value.push('Series title must generate a valid ID')
|
formErrors.value.push('Series title must generate a valid ID')
|
||||||
fieldErrors.value.title = 'Please enter a title that can generate a valid series ID'
|
fieldErrors.value.title = 'Please enter a title that can generate a valid series ID'
|
||||||
}
|
}
|
||||||
|
|
||||||
return formErrors.value.length === 0
|
return formErrors.value.length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,7 +195,7 @@ const createSeries = async (redirectAfter = true) => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
creating.value = true
|
creating.value = true
|
||||||
try {
|
try {
|
||||||
const response = await $fetch('/api/admin/series', {
|
const response = await $fetch('/api/admin/series', {
|
||||||
|
|
@ -224,16 +205,16 @@ const createSeries = async (redirectAfter = true) => {
|
||||||
id: generatedSlug.value
|
id: generatedSlug.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
showSuccessMessage.value = true
|
showSuccessMessage.value = true
|
||||||
setTimeout(() => { showSuccessMessage.value = false }, 5000)
|
setTimeout(() => { showSuccessMessage.value = false }, 5000)
|
||||||
|
|
||||||
if (redirectAfter) {
|
if (redirectAfter) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/admin/series-management')
|
router.push('/admin/series-management')
|
||||||
}, 1500)
|
}, 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create series:', error)
|
console.error('Failed to create series:', error)
|
||||||
|
|
@ -260,9 +241,127 @@ const createAndAddEvent = async () => {
|
||||||
totalEvents: seriesForm.totalEvents
|
totalEvents: seriesForm.totalEvents
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem('seriesEventData', JSON.stringify(seriesData))
|
sessionStorage.setItem('seriesEventData', JSON.stringify(seriesData))
|
||||||
router.push('/admin/events/create?series=true')
|
router.push('/admin/events/create?series=true')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
padding: 28px 28px 20px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-bright);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p { font-size: 12px; color: var(--text-dim); }
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--candle);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.back-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
.form-body { padding: 24px 28px; }
|
||||||
|
|
||||||
|
.form-section { margin-bottom: 32px; }
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-family: 'Brygada 1918', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-bright);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px dashed var(--ember);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ember);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box ul { margin-top: 6px; padding: 0; list-style: none; }
|
||||||
|
.error-box li::before { content: '\2022\00a0'; }
|
||||||
|
|
||||||
|
.success-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px dashed var(--candle);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required { color: var(--ember); }
|
||||||
|
|
||||||
|
.has-error { border-color: var(--ember) !important; }
|
||||||
|
|
||||||
|
.help-text { font-size: 11px; color: var(--text-faint); margin-top: 4px; }
|
||||||
|
.field-error { font-size: 11px; color: var(--ember); margin-top: 4px; }
|
||||||
|
|
||||||
|
.slug-display {
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-family: 'Commit Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-bright);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px dashed var(--border);
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-header { padding: 24px 20px 16px; }
|
||||||
|
.form-body { padding: 20px; }
|
||||||
|
.form-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -21,27 +21,29 @@
|
||||||
|
|
||||||
<div class="membership-card">
|
<div class="membership-card">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tbody>
|
||||||
<td>Status</td>
|
<tr>
|
||||||
<td>
|
<td>Status</td>
|
||||||
<span class="status-dot" :class="memberData.status || 'active'"></span>
|
<td>
|
||||||
{{ memberData.status || 'Active' }}
|
<span class="status-dot" :class="memberData.status || 'active'"></span>
|
||||||
</td>
|
{{ memberData.status || 'Active' }}
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Circle</td>
|
<tr>
|
||||||
<td :style="{ color: `var(--c-${memberData.circle || 'community'})` }">
|
<td>Circle</td>
|
||||||
{{ memberData.circle || 'Community' }}
|
<td :style="{ color: `var(--c-${memberData.circle || 'community'})` }">
|
||||||
</td>
|
{{ memberData.circle || 'Community' }}
|
||||||
</tr>
|
</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Contribution</td>
|
<tr>
|
||||||
<td>${{ memberData.contributionAmount || 0 }} / month</td>
|
<td>Contribution</td>
|
||||||
</tr>
|
<td>${{ memberData.contributionAmount || 0 }} / month</td>
|
||||||
<tr>
|
</tr>
|
||||||
<td>Member since</td>
|
<tr>
|
||||||
<td>{{ formatMemberSince(memberData.createdAt) }}</td>
|
<td>Member since</td>
|
||||||
</tr>
|
<td>{{ formatMemberSince(memberData.createdAt) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
|
<ClientOnly>
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
<div v-if="authPending" class="loading-state">
|
<div v-if="authPending" class="loading-state">
|
||||||
<div class="spinner" />
|
<div class="spinner" />
|
||||||
|
|
@ -149,6 +150,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="loading-state">
|
||||||
|
<div class="spinner" />
|
||||||
|
<p>Loading your dashboard...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="content-main">
|
<div class="content-main">
|
||||||
|
<ClientOnly>
|
||||||
|
|
||||||
<!-- Stats + New Update row -->
|
<!-- Stats + New Update row -->
|
||||||
<div v-if="isAuthenticated && !pending" class="stats-row">
|
<div v-if="isAuthenticated && !pending" class="stats-row">
|
||||||
|
|
@ -108,6 +109,14 @@
|
||||||
<NuxtLink to="/updates/new" class="btn btn-primary">+ Post Your First Update</NuxtLink>
|
<NuxtLink to="/updates/new" class="btn btn-primary">+ Post Your First Update</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template #fallback>
|
||||||
|
<div class="state-box">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p class="state-text">Loading your updates...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Events Mini Sidebar -->
|
<!-- Events Mini Sidebar -->
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ export default defineNuxtConfig({
|
||||||
compatibilityDate: "2025-07-15",
|
compatibilityDate: "2025-07-15",
|
||||||
devtools: { enabled: process.env.NODE_ENV !== "production" },
|
devtools: { enabled: process.env.NODE_ENV !== "production" },
|
||||||
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"],
|
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"],
|
||||||
|
colorMode: {
|
||||||
|
preference: "system",
|
||||||
|
fallback: "light",
|
||||||
|
classSuffix: "",
|
||||||
|
},
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
link: [
|
link: [
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export default defineEventHandler(async (event) => {
|
||||||
.replace(/\n/g, '<br>')
|
.replace(/\n/g, '<br>')
|
||||||
.replace(/\{loginLink\}/g, loginButton)
|
.replace(/\{loginLink\}/g, loginButton)
|
||||||
|
|
||||||
const { error: sendError } = await resend.emails.send({
|
const { error: emailError } = await resend.emails.send({
|
||||||
from: 'Ghost Guild <welcome@babyghosts.org>',
|
from: 'Ghost Guild <welcome@babyghosts.org>',
|
||||||
to: [member.email],
|
to: [member.email],
|
||||||
subject: 'You\'re invited to Ghost Guild',
|
subject: 'You\'re invited to Ghost Guild',
|
||||||
|
|
@ -64,8 +64,8 @@ export default defineEventHandler(async (event) => {
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
|
|
||||||
if (sendError) {
|
if (emailError) {
|
||||||
results.push({ memberId: member._id, email: member.email, success: false, error: sendError.message })
|
results.push({ memberId: member._id, email: member.email, success: false, error: emailError.message })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
42
server/api/dev/test-login.get.js
Normal file
42
server/api/dev/test-login.get.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import Member from '../../models/member.js'
|
||||||
|
import { connectDB } from '../../utils/mongoose.js'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Only allow in development
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await connectDB()
|
||||||
|
|
||||||
|
// Find or create a test admin user
|
||||||
|
let member = await Member.findOne({ email: 'test-admin@ghostguild.dev' })
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
member = await Member.create({
|
||||||
|
email: 'test-admin@ghostguild.dev',
|
||||||
|
name: 'Test Admin',
|
||||||
|
circle: 'founder',
|
||||||
|
contributionTier: '0',
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = useRuntimeConfig(event)
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ memberId: member._id, email: member.email },
|
||||||
|
config.jwtSecret,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
)
|
||||||
|
|
||||||
|
setCookie(event, 'auth-token', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
})
|
||||||
|
|
||||||
|
await sendRedirect(event, '/admin', 302)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue