ghostguild-org/app/pages/index.vue
Jennie Robinson Faber bab53cec9e
Some checks failed
Test / vitest (push) Successful in 12m45s
Test / playwright (push) Failing after 10m5s
Test / visual (push) Failing after 9m16s
merge: worktree-a11y-fixes into main
Accessibility fixes (aria-labels, color contrast, html lang, inline link
underlines), atomic dev login endpoints, and E2E test hardening.
2026-04-05 22:05:00 +01:00

362 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 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>
<!-- 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>
<!-- UPCOMING EVENTS + WIKI (full-bleed row dividers: border on full-width row, padding on inset only) -->
<div class="content-row two-col">
<div class="content-block">
<div class="block-inset">
<div class="label">Upcoming Events</div>
</div>
<div v-if="events?.length" class="event-list">
<div v-for="event in events" :key="event._id" class="event-item">
<div class="block-inset event-item-inner">
<span class="event-date">{{ formatDate(event.startDate) }}</span>
<span class="event-title">
<NuxtLink :to="`/events/${event.slug || event._id}`">{{
event.title
}}</NuxtLink>
</span>
<CircleBadge v-if="event.circle" :circle="event.circle" />
</div>
</div>
</div>
<div v-else class="block-inset">
<p class="empty">No upcoming events</p>
</div>
</div>
<div class="content-block">
<div class="block-inset">
<div class="label">Recently in the Wiki</div>
</div>
<div class="wiki-list">
<div class="wiki-item">
<div class="block-inset wiki-item-inner">
<a href="/wiki">Revenue sharing models</a>
</div>
</div>
<div class="wiki-item">
<div class="block-inset wiki-item-inner">
<a href="/wiki">What is a cooperative studio?</a>
</div>
</div>
<div class="wiki-item">
<div class="block-inset wiki-item-inner">
<a href="/wiki">Governance structures</a>
</div>
</div>
<div class="wiki-item">
<div class="block-inset wiki-item-inner">
<a href="/wiki">Legal incorporation guide</a>
</div>
</div>
</div>
</div>
</div>
<!-- PARCHMENT INSET -->
<ParchmentInset>
<div
class="label"
style="color: var(--candle-faint); 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>
definePageMeta({
layout: "default",
});
const { data: events } = await useFetch("/api/events", {
query: { limit: 4, upcoming: true },
default: () => [],
});
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 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));
align-items: stretch;
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;
align-self: stretch;
}
.content-row.two-col .content-block {
padding: 24px 0;
}
.content-row.two-col .block-inset {
padding-left: 28px;
padding-right: 28px;
}
.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 {
border-bottom: 1px dashed var(--border);
}
.event-list .event-item:last-child {
border-bottom: none;
}
.event-item-inner {
display: grid;
grid-template-columns: 60px 1fr auto;
gap: 16px;
align-items: baseline;
padding-top: 10px;
padding-bottom: 10px;
transition: padding-left 0.2s;
}
.content-row.two-col .event-item:hover .event-item-inner {
padding-left: calc(28px + 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 {
border-bottom: 1px dashed var(--border);
font-size: 13px;
}
.wiki-list .wiki-item:last-child {
border-bottom: none;
}
.wiki-item-inner {
padding-top: 8px;
padding-bottom: 8px;
}
.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>