diff --git a/.gitignore b/.gitignore index 15f2a76..a0df783 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ logs .fleet .idea /docs/ -*.md/ +/*.md # Local env files .env diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 3b1685d..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,99 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Ghost Guild is a membership community platform for game developers exploring cooperative business models. Built with Nuxt 4, Vue 3, MongoDB, and Nuxt UI 4. - -## Commands - -```bash -npm run dev # Start dev server at http://localhost:3000 -npm run build # Production build -npm run preview # Preview production build -npm run test:run # Vitest single run (pre-push hook) -npm run test:e2e # Playwright E2E (needs dev server + MongoDB) -npm run test:a11y # Accessibility scans -npm run test:all # Vitest + Playwright -``` - -**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. - -**Testing:** Vitest for unit/handler tests (`tests/`), Playwright for E2E (`e2e/`). Husky pre-push hook runs Vitest. See `TESTING.md` for details. - -## Architecture - -### Stack - -- **Framework:** Nuxt 4 (Vue 3 + Nitro server) -- **UI:** Nuxt UI 4 (`@nuxt/ui@^4`) with Tailwind CSS -- **Database:** MongoDB via Mongoose -- **Auth:** JWT magic link (email-only, no passwords) -- **Payments:** Helcim (recurring subscriptions + ticket sales) -- **Email:** Resend -- **Slack:** `@slack/web-api` for member invitations and notifications -- **Images:** Cloudinary -- **Analytics:** Plausible (`ghostguild.org`) - -### Key Directories - -- `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/middleware/` — Route guards: `auth.js` (member pages), `admin.js` (admin pages), `coming-soon.global.js` (launch gate) -- `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/`, `dev/` (dev-only helpers) -- `server/models/` — Mongoose schemas: `Member`, `Event`, `Series`, `Update` -- `server/utils/` — Service integrations: `mongoose.js`, `helcim.js`, `resend.js`, `slack.ts`, `tickets.js` - -### Domain Model - -Three membership **circles**: Community, Founder, Practitioner — each with different access and context. Five **contribution tiers**: $0, $5, $15, $30, $50/month via Helcim subscriptions. - -Member statuses: `pending_payment`, `active`, `suspended`, `cancelled`. - -Events support ticketing with circle-specific pricing overrides and can be grouped into Series with bundled passes. - -### Design System (Zine Direction) - -- **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` -- **Typography:** Brygada 1918 (serif, display/headings) + Commit Mono (monospace, body/UI/everything structural) — loaded via Google Fonts in `nuxt.config.ts` -- **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 -- **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` -- **Visual language:** Dashed borders (1px dashed), cream backgrounds, no rounded corners, text-forward density, minimal decoration -- **Color mode:** `@nuxtjs/color-mode` with preference `system`, fallback `light`. Dark mode via `.dark` class on `` -- **Layouts:** `default` (sidebar + main, member/public pages), `admin` (sidebar + main, admin pages), `landing` (horizontal nav, unused) - -### Environment - -Copy `.env.example` to `.env`. Required: `MONGODB_URI`, `JWT_SECRET`, `RESEND_API_KEY`, `HELCIM_API_TOKEN`, `SLACK_BOT_TOKEN`. Public vars are prefixed `NUXT_PUBLIC_`. The `NUXT_PUBLIC_COMING_SOON` flag gates access behind a launch page. - -## Conventions - -- All frontend code is plain JavaScript (not TypeScript), using Vue 3 Composition API -- Server utilities auto-imported by Nitro — no explicit imports needed in API routes -- Use `USwitch` (not `UToggle`) — this is the correct Nuxt UI 3+ component name -- No fallback/placeholder data — always use real data -- Follow Nuxt 4 file-based routing conventions for route naming -- Always check Nuxt UI 4 latest documentation on the web when implementing UI components -- Auth API responses (`/api/auth/status`, `/api/auth/member`) must include `status` in the returned member object — `useMemberStatus` defaults to `PENDING_PAYMENT` if missing -- Helcim payment testing requires ngrok: `npx nuxi dev --https` then `ngrok http https://localhost:3000` — Helcim blocks localhost origins -- The `/api/helcim/initialize-payment` endpoint skips auth for `event_ticket` type payments (public users can buy tickets) - -## Product Spec - -The sections below describe planned and in-progress features for reference. - -### Member Features -- Profiles with privacy controls (public/members-only/private per field) -- Member updates/mini blog with rich text and images -- Peer support system with Cal.com integration for 1:1 scheduling - -### Events System -- RSVP with capacity limits and waitlist management -- Calendar export (.ics), ticketing, series passes -- Member-proposed events with interest threshold - -### Resources (Planned) -- Learning paths by circle, templates and tools, case studies -- Tag by circle relevance, download tracking, version control diff --git a/app/assets/css/fonts.css b/app/assets/css/fonts.css index 9b5bae6..62767ff 100644 --- a/app/assets/css/fonts.css +++ b/app/assets/css/fonts.css @@ -1,8 +1,16 @@ /* - * Font declarations for Ghost Guild — Zine Direction + * Self-hosted font declarations for Ghost Guild — Zine Direction * - * Brygada 1918: Display/heading serif (Google Fonts, variable 400-700, italic) - * Commit Mono: Body/UI monospace (Google Fonts) + * Brygada 1918: Display/heading serif + * Commit Mono: Body/UI monospace * - * Loaded via Google Fonts link in nuxt.config.ts head. + * Fonts are bundled locally via Fontsource. */ + +@import "@fontsource-variable/brygada-1918/wght.css"; +@import "@fontsource-variable/brygada-1918/wght-italic.css"; + +@import "@fontsource/commit-mono/400.css"; +@import "@fontsource/commit-mono/500.css"; +@import "@fontsource/commit-mono/600.css"; +@import "@fontsource/commit-mono/700.css"; diff --git a/app/assets/css/main.css b/app/assets/css/main.css index b4f6ac2..de0d0a7 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -15,6 +15,7 @@ :root { --bg: #f4efe4; + --input-bg: #faf8f2; --surface: #e8dfc8; --surface-hover: #e0d6bc; --border: #b8a880; @@ -36,10 +37,12 @@ --c-practitioner: #2a4650; --green: #4a6a38; --green-bg: rgba(74, 106, 56, 0.08); + --ember-bg: rgba(138, 68, 32, 0.1); } .dark { --bg: #131210; + --input-bg: #1c1a17; --surface: #1a1815; --surface-hover: #252220; --border: #2a2520; @@ -59,16 +62,17 @@ --c-community: #a06850; --c-founder: #c06030; --c-practitioner: #4a7080; + --ember-bg: rgba(192, 96, 48, 0.14); } /* ---- TAILWIND @THEME MAPPING ---- */ @theme { - --font-sans: 'Commit Mono', monospace; - --font-body: 'Commit Mono', monospace; - --font-mono: 'Commit Mono', monospace; - --font-display: 'Brygada 1918', serif; - --font-serif: 'Brygada 1918', serif; + --font-sans: "Commit Mono", monospace; + --font-body: "Commit Mono", monospace; + --font-mono: "Commit Mono", monospace; + --font-display: "Brygada 1918", serif; + --font-serif: "Brygada 1918", serif; /* Map primary to candle for Nuxt UI components */ --color-primary-500: var(--candle); @@ -81,14 +85,30 @@ body { background: var(--bg); color: var(--text); - font-family: 'Commit Mono', monospace; + font-family: "Commit Mono", monospace; font-size: 13px; line-height: 1.6; -webkit-font-smoothing: antialiased; } -a { color: var(--candle); text-decoration: none; } -a:hover { text-decoration: underline; } +/* ---- NOISE TEXTURE OVERLAY ---- */ +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + background: url("~/assets/images/noise.webp") repeat; + opacity: 0.025; +} + +a { + color: var(--candle); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} /* ---- SECTION LABELS ---- */ .section-label { @@ -108,14 +128,26 @@ a:hover { text-decoration: underline; } padding: 2px 8px; border: 1px dashed; } -.badge.community { color: var(--c-community); border-color: rgba(122, 72, 56, 0.35); } -.badge.founder { color: var(--c-founder); border-color: rgba(138, 68, 32, 0.35); } -.badge.practitioner { color: var(--c-practitioner); border-color: rgba(42, 70, 80, 0.35); } -.badge.all { color: var(--text-dim); border-color: var(--border); } +.badge.community { + color: var(--c-community); + border-color: rgba(122, 72, 56, 0.35); +} +.badge.founder { + color: var(--c-founder); + border-color: rgba(138, 68, 32, 0.35); +} +.badge.practitioner { + color: var(--c-practitioner); + border-color: rgba(42, 70, 80, 0.35); +} +.badge.all { + color: var(--text-dim); + border-color: var(--border); +} /* ---- BUTTONS ---- */ .btn { - font-family: 'Commit Mono', monospace; + font-family: "Commit Mono", monospace; font-size: 12px; padding: 7px 18px; border: 1px dashed var(--border); @@ -125,14 +157,20 @@ a:hover { text-decoration: underline; } letter-spacing: 0.04em; transition: all 0.15s; } -.btn:hover { background: var(--surface-hover); border-color: var(--border-d); } +.btn:hover { + background: var(--surface-hover); + border-color: var(--border-d); +} .btn-primary { background: var(--candle); color: var(--bg); border-color: var(--candle); border-style: solid; } -.btn-primary:hover { background: var(--candle-dim); border-color: var(--candle-dim); } +.btn-primary:hover { + background: var(--candle-dim); + border-color: var(--candle-dim); +} .btn-danger { color: var(--ember); border-color: var(--ember); @@ -144,7 +182,9 @@ a:hover { text-decoration: underline; } } /* ---- FORM FIELDS ---- */ -.field { margin-bottom: 12px; } +.field { + margin-bottom: 12px; +} .field label { font-size: 10px; letter-spacing: 0.08em; @@ -153,17 +193,21 @@ a:hover { text-decoration: underline; } margin-bottom: 3px; display: block; } -.field input, .field select, .field textarea { +.field input, +.field select, +.field textarea { width: 100%; padding: 5px 8px; - font-family: 'Commit Mono', monospace; + font-family: "Commit Mono", monospace; font-size: 13px; color: var(--text-bright); - background: var(--bg); + background: var(--input-bg); border: 1px dashed var(--border); outline: none; } -.field input:focus, .field select:focus, .field textarea:focus { +.field input:focus, +.field select:focus, +.field textarea:focus { border-color: var(--candle); border-style: solid; } @@ -174,8 +218,25 @@ a:hover { text-decoration: underline; } padding: 20px 24px; transition: border-color 0.2s; } -.dashed-box:hover { border-color: var(--candle-faint); } -.dashed-box.no-hover:hover { border-color: var(--border); } +.dashed-box:hover { + border-color: var(--candle-faint); +} +.dashed-box.no-hover:hover { + border-color: var(--border); +} + +/* ---- SEGMENTED CONTROL (flush dashed-border groups) ---- */ +/* Negative-margin overlap: every item keeps all 4 borders, + siblings overlap by 1px, active item paints on top via z-index. */ +.segmented { + display: flex; +} +.segmented > * { + position: relative; +} +.segmented > * + * { + margin-left: -1px; +} /* ---- SECTION DIVIDERS ---- */ .section-divider { diff --git a/app/assets/images/noise.webp b/app/assets/images/noise.webp new file mode 100644 index 0000000..d9f0fa2 Binary files /dev/null and b/app/assets/images/noise.webp differ diff --git a/app/components/AppNavigation.vue b/app/components/AppNavigation.vue index 6a82cd0..8fef28e 100644 --- a/app/components/AppNavigation.vue +++ b/app/components/AppNavigation.vue @@ -17,7 +17,13 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} + >{{ item.label }} + +
  • + Sign out
  • @@ -28,18 +34,8 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} - - - - - @@ -53,7 +49,8 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} + >{{ item.label }} @@ -64,7 +61,8 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} + >{{ item.label }} @@ -78,7 +76,8 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} + >{{ item.label }} @@ -89,7 +88,8 @@ :to="item.path" :class="{ active: isActive(item.path) }" @click="handleNavigate" - >{{ item.label }} + >{{ item.label }} @@ -99,29 +99,18 @@ @@ -134,68 +123,59 @@ const props = defineProps({ type: Boolean, default: false, }, -}) +}); -const emit = defineEmits(['navigate']) +const emit = defineEmits(["navigate"]); -const route = useRoute() -const { isAuthenticated, logout, memberData } = useAuth() -const { openLoginModal } = useLoginModal() +const route = useRoute(); +const { isAuthenticated, logout } = useAuth(); +const isDev = import.meta.dev; const handleNavigate = () => { if (props.isMobile) { - emit('navigate') + emit("navigate"); } -} +}; const handleLogout = async () => { - await logout() - handleNavigate() -} - -const openLogin = () => { - openLoginModal() - handleNavigate() -} + await logout(); + handleNavigate(); + navigateTo("/"); +}; const isActive = (path) => { - if (path === '/') return route.path === '/' - return route.path.startsWith(path) -} + if (path === "/") return route.path === "/"; + return route.path.startsWith(path); +}; // Public nav items const publicItems = [ - { label: 'Home', path: '/' }, - { label: 'About', path: '/about' }, - { label: 'Events', path: '/events' }, - { label: 'Members', path: '/members' }, - { label: 'Wiki', path: '/wiki' }, -] + { label: "Home", path: "/" }, + { label: "About", path: "/about" }, + { label: "Events", path: "/events" }, + { label: "Members", path: "/members" }, + { label: "Wiki", path: "https://wiki.ghostguild.org" }, +]; const joinItems = [ - { label: 'Become a member', path: '/join' }, - { label: 'Propose an event', path: '/events' }, -] + { label: "Become a member", path: "/join" }, + { label: "Propose an event", path: "/events" }, +]; // Logged-in nav items const youItems = [ - { label: 'Dashboard', path: '/member/dashboard' }, - { label: 'Profile', path: '/member/profile' }, - { label: 'Account', path: '/member/account' }, - { label: 'My Updates', path: '/member/my-updates' }, -] + { label: "Dashboard", path: "/member/dashboard" }, + { label: "Profile", path: "/member/profile" }, + { label: "Account", path: "/member/account" }, + { label: "My Updates", path: "/member/my-updates" }, +]; const exploreItems = [ - { label: 'Events', path: '/events' }, - { label: 'Members', path: '/members' }, - { label: 'Wiki', path: '/wiki' }, - { label: 'About', path: '/about' }, -] - -const communityItems = [ - { label: 'Peer Support', path: '/members' }, - { label: 'Propose an Event', path: '/events' }, -] + { label: "Events", path: "/events" }, + { label: "Members", path: "/members" }, + { label: "Wiki", path: "/wiki" }, + { label: "About", path: "/about" }, +]; diff --git a/app/components/ColorModeToggle.vue b/app/components/ColorModeToggle.vue index fc7b28d..b8d0ab1 100644 --- a/app/components/ColorModeToggle.vue +++ b/app/components/ColorModeToggle.vue @@ -1,22 +1,24 @@ diff --git a/app/components/DevLoginPanel.vue b/app/components/DevLoginPanel.vue new file mode 100644 index 0000000..858c610 --- /dev/null +++ b/app/components/DevLoginPanel.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/app/components/EventsMiniSidebar.vue b/app/components/EventsMiniSidebar.vue index 84486de..1cf35ce 100644 --- a/app/components/EventsMiniSidebar.vue +++ b/app/components/EventsMiniSidebar.vue @@ -6,13 +6,16 @@
    - {{ formatDate(event.date) }} - {{ event.title }} + {{ formatDate(event.startDate) }} + {{ + event.title + }} {{ event.circle }} + >{{ event.circle }}
    @@ -30,13 +33,13 @@ diff --git a/app/components/PrivacyToggle.vue b/app/components/PrivacyToggle.vue index bafa0ce..2245e1a 100644 --- a/app/components/PrivacyToggle.vue +++ b/app/components/PrivacyToggle.vue @@ -1,26 +1,27 @@ diff --git a/app/components/TagInput.vue b/app/components/TagInput.vue index ca8140f..ab77fcb 100644 --- a/app/components/TagInput.vue +++ b/app/components/TagInput.vue @@ -17,37 +17,37 @@ diff --git a/app/pages/about.vue b/app/pages/about.vue index 4adbce6..93b028c 100644 --- a/app/pages/about.vue +++ b/app/pages/about.vue @@ -4,67 +4,115 @@

    About Ghost Guild

    -

    A membership community for game developers exploring cooperative business models.

    +

    + A membership community for game developers exploring cooperative + business models. +

    -

    Ghost Guild grew out of Baby Ghosts, a Canadian nonprofit that's been supporting indie game developers since 2018. We noticed a gap: game developers interested in cooperative models had nowhere to learn, practice, and connect with others doing the same work.

    -

    Ghost Guild is the response — a membership program where developers at every stage of cooperative practice can find resources, events, mentorship, and community.

    -

    We don't prescribe a single model. We're a place to explore the options, learn from people who've tried them, and build something that works for your team.

    +

    + Ghost Guild grew out of Baby Ghosts, a Canadian nonprofit that's been + supporting indie game developers since 2018. We noticed a gap: game + developers interested in cooperative models had nowhere to learn, + practice, and connect with others doing the same work. +

    +

    + Ghost Guild is the response — a membership program where + developers at every stage of cooperative practice can find resources, + events, mentorship, and community. +

    +

    + We don't prescribe a single model. We're a place to explore the + options, learn from people who've tried them, and build something that + works for your team. +

    -
    -

    Community

    +

    Community

    "The open hall"
    -

    For anyone exploring cooperative models. Wiki access, public events, Slack community, monthly meetings.

    +

    + For anyone exploring cooperative models. Wiki access, public + events, Slack community, monthly meetings. +

    -

    Founder

    +

    Founder

    "The workshop"
    -

    For people actively building cooperatives. Peer accelerator, mentorship, governance templates.

    +

    + For people actively building cooperatives. Peer accelerator, + mentorship, governance templates. +

    -

    Practitioner

    +

    Practitioner

    "The alcove"
    -

    For experienced practitioners. Mentoring, teaching, shaping the program direction.

    +

    + For experienced practitioners. Mentoring, teaching, shaping the + program direction. +

    - -
    - -

    Membership is $0–50/month, pay what you can. Nobody is excluded for lack of funds. Your contribution supports infrastructure, events, and community resources.

    -
      -
    • $0 I need support right now
    • -
    • $5 I can contribute
    • -
    • $15 I can sustain the community
    • -
    • $30 I can support others too
    • -
    • $50 I want to sponsor multiple members
    • -
    -
    - - -
    - -

    We gather in Slack, at monthly meetings, and through peer support sessions. The wiki is our shared knowledge base — growing as members contribute. Events range from workshops to social hangs to deep-dive series.

    - Join the Guild → + +
    +
    + +

    + Membership is $0–50/month, pay what you can. Nobody is + excluded for lack of funds. Your contribution supports + infrastructure, events, and community resources. +

    +
      +
    • $0 I need support right now
    • +
    • $5 I can contribute
    • +
    • + $15 I can sustain the community +
    • +
    • + $30 I can support others too +
    • +
    • + $50 I want to sponsor multiple + members +
    • +
    +
    +
    + +

    + We gather in Slack, at monthly meetings, and through peer support + sessions. The wiki is our shared knowledge base — growing as + members contribute. Events range from workshops to social hangs to + deep-dive series. +

    + Join the Guild → +
    -

    Ghost Guild is a program of Baby Ghosts, a Canadian nonprofit advancing cooperative models in game development. No tracking. No ads. No venture capital.

    -

    babyghosts.fund →

    +

    + Ghost Guild is a program of Baby Ghosts, a Canadian nonprofit + advancing cooperative models in game development. No tracking. No + ads. No venture capital. +

    +

    + babyghosts.fund → +

    @@ -75,10 +123,10 @@ diff --git a/app/pages/members.vue b/app/pages/members/index.vue similarity index 85% rename from app/pages/members.vue rename to app/pages/members/index.vue index 9c7e30f..a7d4658 100644 --- a/app/pages/members.vue +++ b/app/pages/members/index.vue @@ -14,13 +14,17 @@ class="filter-search" placeholder="Search members..." @input="debouncedSearch" - > + /> @@ -29,17 +33,25 @@ type="checkbox" :checked="peerSupportFilter === 'true'" @change="togglePeerSupport" - > + /> Offering support - Showing {{ totalCount }} member{{ totalCount === 1 ? '' : 's' }} + Showing {{ totalCount }} member{{ totalCount === 1 ? "" : "s" }}
    -
    +
    Skills:
    -
    +
    Topics:
    -
    +
    Active filters: - + {{ circleLabels[selectedCircle] }} @@ -101,19 +117,11 @@ Offering Peer Support - + {{ skill }} - + {{ topic }} @@ -134,11 +142,7 @@
    -
    +
    + /> {{ getInitials(member.name) }}
    - {{ member.name }} - {{ member.name }} - {{ member.pronouns }} + {{ + member.name + }} + {{ + member.pronouns + }}
    - {{ circleLabels[member.circle] }} + {{ + circleLabels[member.circle] + }}