ghostguild-org/app/pages/index.vue
Jennie Robinson Faber c40f2c7c63 fix: accessibility improvements and test infrastructure hardening
Add aria-labels to form controls (selects, checkboxes, switches), set
html lang attribute and page title, fix color contrast for --candle-dim
and --text-faint tokens, underline inline links, remove opacity hack.
Harden dev login endpoints with atomic findOneAndUpdate and tokenVersion
in JWT. Update Playwright timeouts and E2E test helpers.
2026-04-05 21:59:02 +01:00

307 lines
8.5 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.date) }}</span>
<span class="event-title">
<NuxtLink :to="`/events/${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: 80px 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>