feat: reskin public pages to zine direction

This commit is contained in:
Jennie Robinson Faber 2026-04-02 21:29:52 +01:00
parent 8b3daadadd
commit 88caca94c7
8 changed files with 2663 additions and 3577 deletions

View file

@ -1,148 +1,219 @@
<template>
<div>
<!-- Page Header -->
<PageHeader
title="About Ghost Guild"
subtitle=""
size="large"
/>
<!-- ABOUT HERO (side by side) -->
<div class="about-hero">
<div class="about-hero-left">
<h1>About Ghost Guild</h1>
<p>A membership community for game developers exploring cooperative business models.</p>
</div>
<div class="about-hero-right">
<div class="section-label">Our Story</div>
<p>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.</p>
<p>Ghost Guild is the response &mdash; a membership program where developers at every stage of cooperative practice can find resources, events, mentorship, and community.</p>
<p>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.</p>
</div>
</div>
<!-- Main Content -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-3xl prose prose-lg dark:prose-invert">
<!-- directives:[] -->
<div id="content">
<p>
Ghost Guild is a community of learning and practice for anyone,
anywhere interested in a video game industry-wide shift to
worker-owned studio models.
</p>
<p>
We reject hierarchy wherever it shows up. Ghost Guild is
tier-less, but peer-full.
</p>
<h2 id="our-story">Our Story</h2>
<p>
Ghost Guild is the membership program of Baby Ghosts, a Canadian
nonprofit that provides resources and support for worker-owned
game studios.
</p>
<p>
For three years, Baby Ghosts has run the Peer Accelerator - an
intensive program pairing early-stage studios with mentorship,
peer learning, and grants. Twenty-five studios have graduated. The
model works! Peer learning builds stronger foundations than
top-down advice ever could.
</p>
<p>
But not everyone can commit to a six-month cohort. Some folks are
still exploring. Others are already running established
cooperatives and want to give back. Many are scattered across the
world and just need to know they're not alone in wanting to build
something different.
</p>
<p><em>Ghost Guild is how we open the doors wider.</em></p>
<p>
As we build our knowledge commons, more folks can benefit from
collectively compiled wisdom and find community - whether or not
they ever apply to the Peer Accelerator. The intensive program
continues. Ghost Guild expands access to everything around it.
</p>
<h2 id="the-circles">The Circles</h2>
<p>
We've loosely named some circles you can join. This is not to rank
you, but to connect you with folks at a similar stage and with
resources that fit where you are right now.
</p>
<p>
No circle is superior. There's no shame in sticking with one for a
while, or moving between them to find the best fit.
</p>
<p>
The Community Circle is for individuals exploring cooperative
principles. Whether you're working in the industry or in academia,
you'll get access to the knowledge commons, workshops, resources,
guides, community Slack and peer support, and social events and
networking.
</p>
<p>
The Founder Circle is for those actively building a worker-owned
studio. You'll have access to everything within the platform, just
like any other member, but you might be particularly interested in
peer matching with studios at similar stages and Peer Accelerator
alumni, and templates for governance, financial modelling, and
decision-making.
</p>
<p>
The Practitioner Circle is for Peer Accelerator alumni and
experienced cooperative studio leaders. You'll hopefully find
yourself providing paid support to other members, as well as
engaging in collaborative research opportunities with academic
partners, connecting to other coops for business development, and
helping build a platform for changing industry practices.
</p>
<h2 id="how-contribution-works">How Contribution Works</h2>
<p>
Choosing your financial contribution is also not about paying for
access. Everything is available to every member, no matter their
circle or contribution level.
</p>
<p>
Rather, it's about finding a dues level that's meaningful to you
without being a burden.
</p>
<p>
The knowledge commons is open to all Ghosties. Your contribution
sustains a community you believe in.
</p>
<p>
If dues are a barrier, that's okay. Members who are able to
contribute more can direct additional funds to the Solidarity
Fund, which covers dues for those who need support.
</p>
<h2 id="community">Community</h2>
<p>
When you join Ghost Guild, you join a community of Ghosties -
folks at every stage of the journey, learning from each other.
</p>
<p>
Our Slack community is built with care. New members are welcomed
thoughtfully, channels are structured to help you find your
people, and we grow at a pace that protects what makes this space
special.
</p>
<p>
This is a cascading mentorship structure where everyone is both
learning and teaching. Practitioners mentor Founders. Founders
mentor Community members. And Community members bring fresh
perspectives that keep everyone honest.
</p>
<p>Welcome, Ghostie! 👻</p>
</div>
</div>
</UContainer>
</section>
<!-- CONTENT AREA WITH EVENTS SIDEBAR -->
<div class="content-area">
<div class="content-main">
<!-- Link to Membership Circles -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-3xl">
<h2 class="text-display-sm font-bold text-[--ui-text] mb-4">
Membership Circles
</h2>
<p class="text-lg text-[--ui-text-muted] mb-6">
Learn about our three membership circles and find where you fit.
</p>
<UButton to="/about/circles" variant="outline" size="lg">
Explore Membership Circles
</UButton>
<!-- THE CIRCLES -->
<div class="about-section" id="circles">
<div class="section-label">The Circles</div>
<div class="circles-grid">
<div id="community" class="circle-cell">
<h3 style="color: var(--c-community);">Community</h3>
<div class="circle-subtitle">"The open hall"</div>
<p>For anyone exploring cooperative models. Wiki access, public events, Slack community, monthly meetings.</p>
</div>
<div id="founder" class="circle-cell">
<h3 style="color: var(--c-founder);">Founder</h3>
<div class="circle-subtitle">"The workshop"</div>
<p>For people actively building cooperatives. Peer accelerator, mentorship, governance templates.</p>
</div>
<div id="practitioner" class="circle-cell">
<h3 style="color: var(--c-practitioner);">Practitioner</h3>
<div class="circle-subtitle">"The alcove"</div>
<p>For experienced practitioners. Mentoring, teaching, shaping the program direction.</p>
</div>
</div>
</div>
<!-- HOW CONTRIBUTION WORKS -->
<div class="about-section">
<div class="section-label">How Contribution Works</div>
<p>Membership is $0&ndash;50/month, pay what you can. Nobody is excluded for lack of funds. Your contribution supports infrastructure, events, and community resources.</p>
<ul class="tier-list">
<li><span class="tier-amt">$0</span> I need support right now</li>
<li><span class="tier-amt">$5</span> I can contribute</li>
<li><span class="tier-amt">$15</span> I can sustain the community</li>
<li><span class="tier-amt">$30</span> I can support others too</li>
<li><span class="tier-amt">$50</span> I want to sponsor multiple members</li>
</ul>
</div>
<!-- COMMUNITY -->
<div class="about-section">
<div class="section-label">Community</div>
<p>We gather in Slack, at monthly meetings, and through peer support sessions. The wiki is our shared knowledge base &mdash; growing as members contribute. Events range from workshops to social hangs to deep-dive series.</p>
<NuxtLink to="/join" class="cta">Join the Guild &rarr;</NuxtLink>
</div>
<!-- ABOUT BABY GHOSTS -->
<div class="about-section">
<div class="section-label">About Baby Ghosts</div>
<p>Ghost Guild is a program of Baby Ghosts, a Canadian nonprofit advancing cooperative models in game development. No tracking. No ads. No venture capital.</p>
<p><a href="https://babyghosts.fund" target="_blank">babyghosts.fund &rarr;</a></p>
</div>
</div>
<!-- EVENTS MINI SIDEBAR -->
<EventsMiniSidebar :events="upcomingEvents" />
</div>
</UContainer>
</section>
</div>
</template>
<script setup>
// No specific logic needed for the about page at this time
const { data: upcomingEvents } = await useFetch('/api/events', {
query: { limit: 3, upcoming: true },
default: () => [],
})
</script>
<style scoped>
/* ---- ABOUT HERO ---- */
.about-hero {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border-bottom: 1px dashed var(--border);
}
.about-hero-left {
padding: 32px 32px 28px;
border-right: 1px dashed var(--border);
}
.about-hero-left h1 {
font-family: 'Brygada 1918', serif;
font-size: 28px;
font-weight: 600;
color: var(--text-bright);
line-height: 1.15;
letter-spacing: -0.01em;
margin-bottom: 12px;
}
.about-hero-left p {
color: var(--text-dim);
line-height: 1.7;
font-size: 13px;
}
.about-hero-right {
padding: 32px;
}
.about-hero-right p {
color: var(--text-dim);
font-size: 12px;
line-height: 1.7;
margin-bottom: 10px;
}
/* ---- CONTENT AREA ---- */
.content-area {
display: grid;
grid-template-columns: 1fr 200px;
}
.content-main {
padding: 0;
min-width: 0;
}
/* ---- SECTIONS ---- */
.about-section {
padding: 28px 32px;
border-bottom: 1px dashed var(--border);
}
.about-section > p {
font-size: 12px;
color: var(--text-dim);
line-height: 1.7;
margin-bottom: 8px;
max-width: 560px;
}
.cta {
display: inline-block;
margin-top: 8px;
font-size: 12px;
color: var(--candle);
}
/* ---- CIRCLES GRID ---- */
.circles-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0;
border: 1px dashed var(--border);
}
.circle-cell {
padding: 20px;
border-right: 1px dashed var(--border);
}
.circle-cell:last-child { border-right: none; }
.circle-cell h3 {
font-family: 'Brygada 1918', serif;
font-size: 18px;
font-weight: 500;
line-height: 1.2;
margin-bottom: 2px;
}
.circle-subtitle {
font-style: italic;
font-size: 12px;
color: var(--text-dim);
margin-bottom: 8px;
}
.circle-cell p {
font-size: 12px;
color: var(--text-dim);
line-height: 1.65;
}
/* ---- TIER LIST ---- */
.tier-list {
list-style: none;
padding: 0;
}
.tier-list li {
padding: 5px 0;
font-size: 12px;
color: var(--text-dim);
border-bottom: 1px dashed var(--border);
display: flex;
gap: 12px;
}
.tier-list li:last-child { border-bottom: none; }
.tier-amt {
color: var(--text-bright);
font-weight: 600;
min-width: 36px;
}
/* ---- RESPONSIVE ---- */
@media (max-width: 1024px) {
.content-area { grid-template-columns: 1fr; }
.circles-grid { grid-template-columns: 1fr; }
.circle-cell {
border-right: none;
border-bottom: 1px dashed var(--border);
}
.circle-cell:last-child { border-bottom: none; }
}
@media (max-width: 768px) {
.about-hero {
grid-template-columns: 1fr;
}
.about-hero-left {
border-right: none;
border-bottom: 1px dashed var(--border);
}
}
</style>

View file

@ -1,212 +1,4 @@
<template>
<div>
<!-- Page Header -->
<PageHeader
title="About Our Membership Circles"
subtitle="All members of Ghost Guild share the Baby Ghosts mission: Advancing cooperative and worker-centric labour models in the Canadian interactive digital arts sector."
size="large"
/>
<!-- How Ghost Guild Works -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-display font-bold text-[--ui-text] mb-6">
How membership works
</h2>
<div class="prose prose-lg dark:prose-invert max-w-none">
<p class="text-xl font-semibold text-[--ui-text] mb-6">
Everyone gets everything. Your circle reflects where you are in
your cooperative journey. Your financial contribution reflects
what you can afford. These are completely separate choices.
</p>
<ul
class="list-disc pl-6 text-lg leading-relaxed text-[--ui-text-muted] space-y-3 mb-12"
>
<li>
The entire knowledge commons, all events, and full community
participation on our private Slack
</li>
<li>One member, one vote in all decisions</li>
<li>Pay what you can ($0-50+/month)</li>
<li>Contribute your skills, time, and knowledge</li>
</ul>
</div>
</div>
</UContainer>
</section>
<!-- Find Your Circle -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-display font-bold text-[--ui-text] mb-4">
Find your circle
</h2>
<p class="text-lg text-[--ui-text-muted] mb-12">
Circles help us provide relevant guidance and connect you with
others at similar stages. Choose based on where you are now!
</p>
<div class="space-y-12">
<!-- Community Circle -->
<div class="circle-surface-community rounded-xl p-6">
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
Community Circle
</h3>
<div
class="prose prose-lg dark:prose-invert max-w-none text-[--ui-text-muted]"
>
<p>
Maybe you've heard rumours about cooperatives in game dev and
you're curious. Or you're frustrated with traditional studio
hierarchies and wondering if there's another way. This circle
is for anyone exploring whether cooperative principles might
fit their work.
</p>
<p>
This space is for you if you're: an individual game worker
dreaming of different possibilities a researcher digging
into the rise of alternative studio models an industry ally
who wants to support cooperative work <em>anyone</em> who's
co-op-curious!
</p>
<p>
Our resources and community space will help you understand
cooperative basics, connect with others asking the same
questions, and give you a look at real examples from game
studios. You don't need to have a studio or project of your
own - just join and see what strikes your fancy!
</p>
</div>
</div>
<!-- Founder Circle -->
<div class="circle-surface-founder rounded-xl p-6">
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
Founder Circle
</h3>
<div
class="prose prose-lg dark:prose-invert max-w-none text-[--ui-text-muted]"
>
<p>
You're way past wondering about "what if" and into "how do we
actually do this?" Perhaps you're forming a new cooperative
studio from scratch, or converting an existing team to a co-op
structure, or working through the messy reality of turning
values into sustainable practice.
</p>
<p>
This is the space for the practical stuff: governance
documents you can read and adapt, financial models for
cooperative studios, connections with other founders
navigating similar challenges.
</p>
<p>
We have two paths through this circle that we will be
launching soon:
</p>
<ul>
<li>
Peer Accelerator Prep Track <em>(coming soon)</em>
Structured preparation if you're planning to apply for the
PA program
</li>
<li>
Indie Track <em>(coming soon)</em> Flexible, self-paced
support for teams building at their own pace
</li>
</ul>
<p>
Join us to figure out how you can balance your values with
keeping the lights on - whether you're a full founding team, a
solo founder exploring structures, or an existing studio in
transition.
</p>
</div>
</div>
<!-- Practitioner Circle -->
<div class="circle-surface-practitioner rounded-xl p-6">
<h3 class="text-display-sm font-bold text-[--ui-text] mb-2">
Practitioner Circle
</h3>
<div
class="prose prose-lg dark:prose-invert max-w-none text-[--ui-text-muted]"
>
<p>
You've done it. You're actually running a
cooperative/worker-centric studio or you've been through our
Peer Accelerator. Now you're figuring out how to sustain it,
improve it, and maybe help others learn from what
<em>you've</em> learned.
</p>
<p>
This circle is for: Peer Accelerator alumni members of
established co-ops mentors who want to support other
cooperatives researchers studying cooperative models in
practice
</p>
<p>
Here, we create space for practitioners to share what's
actually working (and what isn't), support emerging
cooperatives, collaborate across studios, and contribute to
building a knowledge commons.
</p>
</div>
</div>
</div>
</div>
</UContainer>
</section>
<!-- Important Notes -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-display font-bold text-[--ui-text] mb-8">
Important Notes
</h2>
<div class="space-y-6 text-lg text-[--ui-text-muted]">
<p>
<strong>Movement between circles is fluid.</strong> As you move
along in your journey, you can shift circles anytime. Just let us
know.
</p>
<p>
<strong>Your contribution is separate from your circle.</strong>
Whether you contribute $0 or $50+/month, you get full access to
everything. Choose based on your financial capacity, not your
circle.
</p>
<p>
<strong>Not sure which circle?</strong> Start with Community - you
can always move. Or email us and we'll chat about what makes sense
for you.
</p>
</div>
</div>
</UContainer>
</section>
</div>
</template>
<script setup>
// No specific logic needed for the circles page at this time
// Circles content has moved to the About page
navigateTo('/about#circles', { redirectCode: 301 })
</script>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,186 +1,270 @@
<template>
<div class="max-w-6xl mx-auto px-6 md:px-8">
<!-- Hero Section -->
<section class="py-16 md:py-24 ink-grain">
<div class="max-w-2xl">
<h1
class="text-display-xl font-light text-guild-100 leading-tight mb-2"
>
Build your co-op studio
</h1>
<p
class="text-display-xl font-light text-guild-500 leading-tight mb-8"
>
with people who get it.
</p>
<p class="text-lg text-guild-400 leading-relaxed mb-8 max-w-xl">
Ghost Guild is a peer community for game developers exploring
cooperative models. Find support, share knowledge, grow together.
</p>
<!-- Signup Form -->
<form @submit.prevent="handleJoinSubmit" class="mb-4">
<div class="flex flex-col sm:flex-row gap-3">
<UInput
v-model="joinEmail"
type="email"
placeholder="your.email@example.com"
size="lg"
class="flex-1"
:disabled="isSubmitting"
/>
<UButton
type="submit"
size="lg"
:loading="isSubmitting"
:disabled="!isEmailValid"
>
Join Us
</UButton>
</div>
</form>
<p class="text-sm text-guild-600">Free to join. Pay what you can.</p>
<!-- Success/Error Messages -->
<div
v-if="submitSuccess"
class="mt-4 p-3 bg-primary-500/10 border border-primary-500/30 rounded-lg"
>
<p class="text-primary-400 text-sm">
Check your email to complete signup!
</p>
</div>
<div
v-if="submitError"
class="mt-4 p-3 bg-ember-900/20 border border-ember-500/30 rounded-lg"
>
<p class="text-ember-400 text-sm">{{ submitError }}</p>
</div>
</div>
</section>
<GuildDivider variant="woodcut" />
<!-- Value Props Section -->
<section class="py-16">
<div class="grid md:grid-cols-3 gap-8 md:gap-12">
<div>
<p class="text-ui-label text-candlelight-400 mb-3">Peer Support</p>
<p class="text-guild-400 leading-relaxed">
Connect with founders at your stage and practitioners who've been
there. Real conversations, real help.
</p>
</div>
<div>
<p class="text-ui-label text-candlelight-400 mb-3">
Shared Knowledge
</p>
<p class="text-guild-400 leading-relaxed">
Templates, governance docs, financial modelstools built by co-ops,
for co-ops. All members get full access.
</p>
</div>
<div>
<p class="text-ui-label text-candlelight-400 mb-3">
Solidarity Economics
</p>
<p class="text-guild-400 leading-relaxed">
Those who can, support those who can't. No tiers, no gatekeeping.
Everyone gets everything.
</p>
<!-- HERO -->
<div class="hero">
<h1>Ghost Guild is where game developers practice cooperative business models.</h1>
<p>Resources, events, and a community of people figuring it out. Three circles, no hierarchy. $050/mo, pay what you can.</p>
<div class="hero-links">
<NuxtLink to="/join" class="hero-link primary">Become a member</NuxtLink>
<NuxtLink to="/wiki" class="hero-link">Read the wiki</NuxtLink>
<NuxtLink to="/about" class="hero-link">What is this?</NuxtLink>
</div>
</div>
</section>
<GuildDivider variant="woodcut" />
<!-- THREE CIRCLES -->
<div class="content-row">
<div v-for="circle in circleData" :key="circle.value" class="content-block">
<div class="label" :style="{ color: `var(--c-${circle.value})` }">{{ circle.label }}</div>
<h2>{{ circle.metaphor }}</h2>
<p>{{ circle.blurb }}</p>
<details>
<summary>What's included?</summary>
<p>{{ circle.included }}</p>
</details>
</div>
</div>
<!-- Circles Section -->
<section class="py-16">
<p class="text-ui-label text-guild-600 mb-8">Find your people</p>
<div class="space-y-4 mb-8">
<NuxtLink
v-for="circle in circles"
:key="circle.value"
to="/about/circles"
class="flex items-baseline gap-8 group py-2"
>
<span
class="text-guild-300 group-hover:text-guild-100 transition-colors w-32 md:w-40"
>
{{ circle.label }}
<!-- UPCOMING EVENTS + WIKI -->
<div class="content-row two-col">
<div class="content-block">
<div class="label">Upcoming Events</div>
<div v-if="events?.length" class="event-list">
<div v-for="event in events" :key="event._id" class="event-item">
<span class="event-date">{{ formatDate(event.date) }}</span>
<span class="event-title">
<NuxtLink :to="`/events/${event._id}`">{{ event.title }}</NuxtLink>
</span>
<span class="text-guild-600">
{{ circle.shortDescription }}
</span>
</NuxtLink>
<CircleBadge v-if="event.circle" :circle="event.circle" />
</div>
</div>
<p v-else class="empty">No upcoming events</p>
</div>
<div class="content-block">
<div class="label">Recently in the Wiki</div>
<div class="wiki-list">
<div class="wiki-item">
<a href="/wiki">Revenue sharing models</a>
</div>
<div class="wiki-item">
<a href="/wiki">What is a cooperative studio?</a>
</div>
<div class="wiki-item">
<a href="/wiki">Governance structures</a>
</div>
<div class="wiki-item">
<a href="/wiki">Legal incorporation guide</a>
</div>
</div>
</div>
</div>
<p class="text-sm text-guild-600 italic">
These reflect your journey, not your status. Move between them as you
grow.
</p>
</section>
<GuildDivider variant="woodcut" />
<!-- Bottom CTA Section -->
<section class="py-24 text-center">
<p class="text-ui-label text-guild-600 mb-4">Part of the Baby Ghosts family</p>
<h2 class="text-display font-light text-guild-200 mb-8">
Ready to find your people?
</h2>
<UButton
to="/join"
variant="outline"
size="lg"
class="hover:bg-primary-500/10"
>
Become a Ghostie
</UButton>
</section>
<!-- PARCHMENT INSET -->
<ParchmentInset>
<div class="label" style="color: var(--candle-faint); opacity: 0.6; margin-bottom: 12px;">From the Wiki</div>
<h2>What is a cooperative studio?</h2>
<p>A cooperative studio is a game development company owned and governed by the people who work there. Decisions are made collectively. Profits are shared according to contribution, not ownership stake.</p>
<p>The games industry is full of stories about crunch, layoffs, and studios that extract value from workers. Cooperatives are one alternative not the only one, but one worth <a href="/wiki">practicing together</a>.</p>
<p><a href="/wiki">Read more in the wiki &rarr;</a></p>
</ParchmentInset>
</div>
</template>
<script setup>
import { getCircleOptions } from "~/config/circles";
definePageMeta({
layout: "default",
});
})
const circles = getCircleOptions();
const { data: events } = await useFetch('/api/events', {
query: { limit: 4, upcoming: true },
default: () => [],
})
// Join form state
const joinEmail = ref("");
const isSubmitting = ref(false);
const submitSuccess = ref(false);
const submitError = ref("");
const circleData = [
{
value: 'community',
label: 'Community',
metaphor: 'The open hall',
blurb: 'Arrival, curiosity, orientation. For anyone exploring cooperative models in game development. Access the wiki, public events, and Slack.',
included: 'Wiki access, public events, Slack community, monthly guild meetings. Free or pay-what-you-can.',
},
{
value: 'founder',
label: 'Founder',
metaphor: 'The workshop',
blurb: 'For people actively building cooperatives. Structured practice, peer support, templates, and hands-on resources.',
included: 'Everything in Community plus the peer accelerator, 1:1 mentorship matching, and Founder-only workshops.',
},
{
value: 'practitioner',
label: 'Practitioner',
metaphor: 'The alcove',
blurb: 'Where experience is shared and knowledge given back. Teaching, advising, shaping the program itself.',
included: 'Everything in Founder plus the ability to mentor, propose events, contribute to the wiki, and help govern the Guild.',
},
]
const isEmailValid = computed(() => {
return joinEmail.value && joinEmail.value.includes("@");
});
const handleJoinSubmit = async () => {
if (!isEmailValid.value || isSubmitting.value) return;
isSubmitting.value = true;
submitSuccess.value = false;
submitError.value = "";
try {
// Redirect to join page with email pre-filled
await navigateTo({
path: "/join",
query: { email: joinEmail.value },
});
} catch (err) {
console.error("Join error:", err);
submitError.value = "Something went wrong. Please try again.";
} finally {
isSubmitting.value = false;
const formatDate = (dateStr) => {
if (!dateStr) return ''
const d = new Date(dateStr)
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
};
</script>
<style scoped>
/* ---- HERO ---- */
.hero {
padding: 48px 32px;
border-bottom: 1px dashed var(--border);
}
.hero h1 {
font-family: 'Brygada 1918', serif;
font-size: 36px;
font-weight: 600;
color: var(--text-bright);
line-height: 1.15;
letter-spacing: -0.01em;
margin-bottom: 16px;
max-width: 540px;
}
.hero p {
color: var(--text-dim);
max-width: 460px;
line-height: 1.7;
margin-bottom: 20px;
}
.hero-links {
display: flex;
gap: 16px;
font-size: 13px;
}
.hero-link {
color: var(--candle);
padding: 6px 16px;
border: 1px dashed var(--candle-faint);
transition: all 0.2s;
text-decoration: none;
}
.hero-link:hover {
border-color: var(--candle);
border-style: solid;
text-decoration: none;
}
.hero-link.primary {
background: var(--candle);
color: var(--bg);
border-color: var(--candle);
border-style: solid;
}
.hero-link.primary:hover {
background: var(--candle-dim);
border-color: var(--candle-dim);
}
/* ---- CONTENT GRID ---- */
.content-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-bottom: 1px dashed var(--border);
}
.content-row.two-col {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.content-block {
padding: 24px 28px;
border-right: 1px dashed var(--border);
min-width: 0;
overflow-wrap: break-word;
}
.content-block:last-child { border-right: none; }
.content-block h2 {
font-family: 'Brygada 1918', serif;
font-size: 18px;
font-weight: 500;
color: var(--text-bright);
margin-bottom: 8px;
}
.content-block p {
color: var(--text-dim);
font-size: 12px;
line-height: 1.65;
}
.content-block .label {
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-faint);
margin-bottom: 8px;
}
/* ---- DETAILS ---- */
details {
margin-top: 12px;
}
details summary {
font-size: 12px;
color: var(--candle-dim);
cursor: pointer;
list-style: none;
}
details summary::before {
content: '+ ';
}
details[open] summary::before {
content: ' ';
}
details p {
margin-top: 8px;
}
/* ---- EVENT LIST ---- */
.event-item {
display: grid;
grid-template-columns: 80px 1fr auto;
gap: 16px;
align-items: baseline;
padding: 10px 0;
border-bottom: 1px dashed var(--border);
transition: padding-left 0.2s;
}
.event-item:last-child { border-bottom: none; }
.event-item:hover { padding-left: 4px; }
.event-date { color: var(--text-faint); font-size: 12px; }
.event-title { color: var(--text); font-size: 13px; }
.event-title a { color: var(--text); text-decoration: none; }
.event-title a:hover { color: var(--candle); }
/* ---- WIKI LIST ---- */
.wiki-item {
padding: 8px 0;
border-bottom: 1px dashed var(--border);
font-size: 13px;
}
.wiki-item:last-child { border-bottom: none; }
.wiki-item a { color: var(--text); text-decoration: none; }
.wiki-item a:hover { color: var(--candle); }
.empty {
color: var(--text-faint);
font-size: 12px;
}
/* ---- RESPONSIVE ---- */
@media (max-width: 768px) {
.content-row,
.content-row.two-col {
grid-template-columns: 1fr;
}
.content-block {
border-right: none;
border-bottom: 1px dashed var(--border);
}
.content-block:last-child { border-bottom: none; }
.hero-links {
flex-direction: column;
gap: 8px;
}
.hero-link {
text-align: center;
}
}
</style>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,503 +1,164 @@
<template>
<div>
<div v-if="pending" class="min-h-screen flex items-center justify-center">
<div class="text-center">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"
></div>
<p class="text-[--ui-text-muted]">Loading series details...</p>
</div>
</div>
<div v-if="pending" class="loading">Loading series details...</div>
<div
v-else-if="error"
class="min-h-screen flex items-center justify-center"
>
<div class="text-center">
<h2 class="text-2xl font-bold text-[--ui-text] mb-2">
Series Not Found
</h2>
<p class="text-[--ui-text-muted] mb-6">
The event series you're looking for doesn't exist.
</p>
<NuxtLink to="/series" class="text-primary hover:underline">
Back to Event Series
</NuxtLink>
</div>
<div v-else-if="error" class="loading">
<h2>Series Not Found</h2>
<p>The event series you're looking for doesn't exist.</p>
<NuxtLink to="/events">&larr; Back to Events</NuxtLink>
</div>
<div v-else>
<!-- Page Header -->
<PageHeader :title="series.title" size="large" />
<!-- Series Meta -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<!-- Series Description -->
<div v-if="series.description" class="mb-8">
<p class="text-lg text-[--ui-text-muted] leading-relaxed">
{{ series.description }}
</p>
<!-- BACK LINK -->
<div class="back-link">
<NuxtLink to="/events">&larr; Back to Events</NuxtLink>
</div>
<div class="flex items-center gap-4 mb-8 flex-wrap">
<span
:class="[
'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
getSeriesTypeBadgeClass(series.type),
]"
>
{{ formatSeriesType(series.type) }}
</span>
<span
:class="[
'inline-flex items-center px-3 py-1 rounded text-sm font-medium',
getSeriesStatusClass(),
]"
>
{{ getSeriesStatusText() }}
<!-- SERIES HEADER -->
<div class="series-header">
<h1>{{ series.title }}</h1>
<div class="series-meta-row">
<span v-if="series.type" class="badge all">{{ formatSeriesType(series.type) }}</span>
<span class="meta-text">{{ series.events?.length || 0 }} sessions</span>
<span v-if="series.startDate" class="meta-text">
{{ formatDate(series.startDate) }} &ndash; {{ formatDate(series.endDate) }}
</span>
</div>
<!-- Series Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12">
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Total Events</div>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.completedEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Completed</div>
<!-- DESCRIPTION -->
<div v-if="series.description" class="section">
<p>{{ series.description }}</p>
</div>
<div>
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.upcomingEvents }}
</div>
<div class="text-sm text-[--ui-text-muted]">Upcoming</div>
</div>
<div v-if="series.statistics.totalRegistrations">
<div class="text-3xl font-bold text-[--ui-text] mb-1">
{{ series.statistics.totalRegistrations }}
</div>
<div class="text-sm text-[--ui-text-muted]">Registrations</div>
</div>
</div>
<!-- Series Date Range -->
<div
v-if="series.startDate && series.endDate"
class="flex items-center gap-2 text-[--ui-text-muted] mb-8"
>
<Icon name="heroicons:calendar-days" class="w-5 h-5" />
<span>
Series runs from
{{ formatDateRange(series.startDate, series.endDate) }}
</span>
</div>
<!-- Status Message -->
<div
v-if="series?.statistics?.isOngoing"
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded mb-8"
>
<p class="text-candlelight-500 dark:text-candlelight-400 font-semibold mb-1">
This series is currently ongoing!
</p>
<p class="text-sm text-[--ui-text-muted]">
Register for upcoming events to join the learning journey.
</p>
</div>
<div
v-else-if="series?.statistics?.isUpcoming"
class="p-4 bg-guild-500/10 border border-guild-500/30 rounded mb-8"
>
<p class="text-guild-300 dark:text-guild-300 font-semibold mb-1">
This series is starting soon!
</p>
<p class="text-sm text-[--ui-text-muted]">
Mark your calendar and register for the events.
</p>
</div>
<div
v-else-if="series?.statistics?.isCompleted"
class="p-4 bg-guild-500/10 border border-guild-500/30 rounded mb-8"
>
<p class="text-[--ui-text] font-semibold mb-1">
This series has concluded.
</p>
<p class="text-sm text-[--ui-text-muted]">
Check out our other event series for more opportunities to learn
and connect.
</p>
</div>
</div>
</UContainer>
</section>
<!-- Series Pass Purchase (if tickets enabled) -->
<section v-if="series?.tickets?.enabled" class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-display-sm font-bold text-[--ui-text] mb-8">
Get Your Series Pass
</h2>
<SeriesPassPurchase
:series-id="series.id || series._id"
:series-info="{
id: series.id,
title: series.title,
totalEvents: series?.statistics?.totalEvents || 0,
type: series.type,
}"
:series-events="series.events || []"
:user-email="user?.email"
:user-name="user?.name"
@purchase-success="handlePurchaseSuccess"
/>
</div>
</UContainer>
</section>
<!-- Events Timeline -->
<section class="py-20 bg-[--ui-bg-elevated]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h2 class="text-display-sm font-bold text-[--ui-text] mb-8">
Event Schedule
</h2>
<div class="space-y-4">
<div
v-for="(event, index) in series?.events || []"
:key="event.id"
class="group"
>
<div class="flex items-start gap-4">
<!-- Position indicator -->
<div class="flex flex-col items-center flex-shrink-0">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold border',
getEventTimelineColor(event),
]"
>
{{ event.series?.position || index + 1 }}
</div>
<div
v-if="index < (series?.events?.length || 0) - 1"
class="w-0.5 h-12 bg-[--ui-border]"
></div>
</div>
<!-- Event Card -->
<NuxtLink
:to="`/events/${event.slug || event.id}`"
class="flex-1 border border-[--ui-border] rounded p-4 hover:border-primary transition-colors"
>
<div
class="flex flex-col md:flex-row md:items-start md:justify-between gap-3"
>
<!-- Event Info -->
<div class="flex-1 min-w-0">
<div class="flex items-start gap-2 mb-2 flex-wrap">
<h3
class="text-lg font-semibold text-[--ui-text] group-hover:text-primary transition-colors"
>
<!-- EVENT LIST -->
<div class="section">
<div class="section-label">Sessions</div>
<div v-if="series.events?.length">
<div v-for="(event, index) in series.events" :key="event._id || index" class="event-row">
<span class="event-num">{{ String(index + 1).padStart(2, '0') }}</span>
<span class="event-date">{{ formatDate(event.startDate) }}</span>
<div class="event-info">
<NuxtLink :to="`/events/${event.slug || event._id || event.id}`" class="event-title-link">
{{ event.title }}
</h3>
<span
:class="[
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium flex-shrink-0',
getEventStatusClass(event),
]"
>
{{ getEventStatus(event) }}
</span>
</div>
<p
v-if="event.description"
class="text-[--ui-text-muted] mb-3 line-clamp-2"
>
{{ event.description }}
</p>
<div
class="flex flex-wrap items-center gap-3 text-sm text-[--ui-text-muted]"
>
<div class="flex items-center gap-1">
<Icon
name="heroicons:calendar-days"
class="w-4 h-4"
/>
{{ formatEventDate(event.startDate) }}
</div>
<div class="flex items-center gap-1">
<Icon name="heroicons:clock" class="w-4 h-4" />
{{ formatEventTime(event.startDate) }}
</div>
<div
v-if="event.location"
class="flex items-center gap-1"
>
<Icon name="heroicons:map-pin" class="w-4 h-4" />
{{ event.location }}
</div>
<div
v-if="event.registeredCount"
class="flex items-center gap-1"
>
<Icon name="heroicons:users" class="w-4 h-4" />
{{ event.registeredCount }} registered
</div>
</div>
</div>
<!-- Arrow -->
<div class="flex items-center md:pt-1">
<Icon
name="heroicons:arrow-right"
class="w-5 h-5 text-[--ui-text-muted] group-hover:text-primary group-hover:translate-x-1 transition-all"
/>
</div>
</div>
</NuxtLink>
<span class="event-status">{{ getEventStatus(event) }}</span>
</div>
</div>
</div>
<p v-else class="empty">No sessions scheduled yet.</p>
</div>
</UContainer>
</section>
<!-- Questions -->
<section class="py-20 bg-[--ui-bg]">
<UContainer>
<div class="max-w-4xl mx-auto">
<h3 class="text-xl font-bold text-[--ui-text] mb-3">
Questions About This Series?
</h3>
<p class="text-[--ui-text-muted] mb-4">
If you have any questions about this event series, please reach
out to us.
</p>
<a
href="mailto:events@ghostguild.org"
class="text-primary hover:underline"
>
events@ghostguild.org
</a>
<!-- PASS PURCHASE -->
<div v-if="series.passPrice" class="section">
<DashedBox>
<div class="section-label">Series Pass</div>
<p>Get access to all sessions in this series with a single pass.</p>
<div class="pass-price">${{ series.passPrice }}</div>
</DashedBox>
</div>
<div class="mt-8">
<NuxtLink to="/series" class="text-primary hover:underline">
Back to all event series
</NuxtLink>
<!-- QUESTIONS -->
<div class="section">
<div class="section-label">Questions?</div>
<p>If you have questions about this series, reach out to us.</p>
<a href="mailto:events@ghostguild.org">events@ghostguild.org</a>
</div>
</div>
</UContainer>
</section>
</div>
</div>
</template>
<script setup>
const route = useRoute();
const { data: session } = useAuth();
const toast = useToast();
const route = useRoute()
// Get user info
const user = computed(() => session?.value?.user || null);
const { data: series, pending, error } = await useFetch(`/api/series/${route.params.id}`)
// Fetch series data from API
const {
data: series,
pending,
error,
refresh: refreshSeries,
} = await useFetch(`/api/series/${route.params.id}`);
// Handle series not found
if (error.value?.statusCode === 404) {
throw createError({
statusCode: 404,
statusMessage: "Event series not found",
});
throw createError({ statusCode: 404, statusMessage: 'Event series not found' })
}
// Handle successful series pass purchase
const handlePurchaseSuccess = async (response) => {
// Refresh series data to show updated registration status
await refreshSeries();
// Scroll to top to show success message
window.scrollTo({ top: 0, behavior: "smooth" });
};
// Helper functions
const formatSeriesType = (type) => {
const types = {
workshop_series: "Workshop Series",
recurring_meetup: "Recurring Meetup",
multi_day: "Multi-Day Event",
course: "Course",
tournament: "Tournament",
};
return types[type] || type;
};
const types = { workshop_series: 'Workshop Series', recurring_meetup: 'Recurring Meetup', multi_day: 'Multi-Day Event', course: 'Course', tournament: 'Tournament' }
return types[type] || type
}
const getSeriesTypeBadgeClass = (type) => {
const classes = {
workshop_series:
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
recurring_meetup:
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
multi_day:
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
course:
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
tournament:
"bg-ember-900/20 text-ember-500 dark:text-ember-400 border border-ember-700/30",
};
return (
classes[type] ||
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border border-earth-700/30"
);
};
const getSeriesStatusText = () => {
if (!series.value?.statistics) return "Active";
if (series.value.statistics.isOngoing) return "Ongoing";
if (series.value.statistics.isUpcoming) return "Starting Soon";
if (series.value.statistics.isCompleted) return "Completed";
return "Active";
};
const getSeriesStatusClass = () => {
if (!series.value?.statistics)
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
if (series.value.statistics.isOngoing)
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
if (series.value.statistics.isUpcoming)
return "bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30";
if (series.value.statistics.isCompleted)
return "bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30";
return "bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30";
};
const formatEventDate = (date) => {
return new Date(date).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
};
const formatEventTime = (date) => {
return new Date(date).toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const formatDateRange = (startDate, endDate) => {
const start = new Date(startDate);
const end = new Date(endDate);
const formatter = new Intl.DateTimeFormat("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
return `${formatter.format(start)} to ${formatter.format(end)}`;
};
const formatDate = (dateStr) => {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
const getEventStatus = (event) => {
const now = new Date();
const startDate = new Date(event.startDate);
const endDate = new Date(event.endDate);
if (now < startDate) return "Upcoming";
if (now >= startDate && now <= endDate) return "Ongoing";
return "Completed";
};
const getEventStatusClass = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border border-guild-500/30",
Ongoing:
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border border-candlelight-700/30",
Completed:
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30",
};
return (
classes[status] ||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border border-guild-500/30"
);
};
const getEventTimelineColor = (event) => {
const status = getEventStatus(event);
const classes = {
Upcoming:
"bg-guild-500/10 text-guild-300 dark:text-guild-300 border-guild-500/30",
Ongoing:
"bg-candlelight-900/20 text-candlelight-500 dark:text-candlelight-400 border-candlelight-700/30",
Completed:
"bg-earth-900/20 text-earth-400 dark:text-earth-400 border-earth-700/30",
};
return (
classes[status] ||
"bg-guild-500/10 text-guild-400 dark:text-guild-400 border-guild-500/30"
);
};
// SEO Meta
useHead(() => {
if (!series || !series.value) {
return {
title: "Event Series - Ghost Guild",
meta: [
{
name: "description",
content:
"Explore our multi-event series designed for learning and growth",
},
],
};
const now = new Date()
const start = new Date(event.startDate)
const end = new Date(event.endDate)
if (now < start) return 'Upcoming'
if (now >= start && now <= end) return 'Ongoing'
return 'Completed'
}
return {
title: `${series.value.title} - Event Series - Ghost Guild`,
meta: [
{
name: "description",
content:
series.value.description ||
"Explore our multi-event series designed for learning and growth",
},
],
};
});
useHead(() => ({
title: series.value ? `${series.value.title} - Event Series - Ghost Guild` : 'Event Series - Ghost Guild',
meta: [{ name: 'description', content: series.value?.description || 'Multi-event series' }],
}))
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
.loading { padding: 48px 32px; color: var(--text-dim); }
.loading h2 { font-family: 'Brygada 1918', serif; font-size: 22px; color: var(--text-bright); margin-bottom: 8px; }
.back-link { padding: 12px 32px; border-bottom: 1px dashed var(--border); font-size: 12px; }
.back-link a { color: var(--candle); text-decoration: none; }
.series-header {
padding: 28px 32px;
border-bottom: 1px dashed var(--border);
}
.series-header h1 {
font-family: 'Brygada 1918', serif;
font-size: 28px;
font-weight: 600;
color: var(--text-bright);
line-height: 1.15;
margin-bottom: 12px;
}
.series-meta-row {
display: flex;
gap: 12px;
align-items: center;
font-size: 12px;
}
.meta-text { color: var(--text-faint); }
.section {
padding: 24px 32px;
border-bottom: 1px dashed var(--border);
}
.section p { font-size: 12px; color: var(--text-dim); line-height: 1.7; max-width: 560px; margin-bottom: 8px; }
.section a { font-size: 12px; color: var(--candle); }
.event-row {
display: grid;
grid-template-columns: 32px 80px 1fr;
gap: 12px;
align-items: baseline;
padding: 10px 0;
border-bottom: 1px dashed var(--border);
font-size: 12px;
}
.event-row:last-child { border-bottom: none; }
.event-num { color: var(--text-faint); font-size: 11px; }
.event-date { color: var(--text-faint); }
.event-title-link { color: var(--text); text-decoration: none; font-size: 13px; }
.event-title-link:hover { color: var(--candle); }
.event-status { font-size: 10px; color: var(--text-faint); margin-left: 8px; }
.pass-price {
font-family: 'Brygada 1918', serif;
font-size: 22px;
font-weight: 600;
color: var(--candle);
margin-top: 8px;
}
.empty { font-size: 12px; color: var(--text-faint); }
</style>