ghostguild-org/app/pages/index.vue
2026-04-27 10:28:21 +01:00

356 lines
8.9 KiB
Vue

<template>
<div>
<!-- HERO -->
<div class="hero">
<h1>Ghost Guild is where game developers explore cooperative models.</h1>
<p>
Resources, events, and a community of people figuring it out. Three
circles, pay what you can.
</p>
<div class="hero-links">
<NuxtLink to="/join" class="hero-link primary"
>Become a member</NuxtLink
>
<a href="https://wiki.ghostguild.org" class="hero-link"
>Read the wiki</a
>
<NuxtLink to="/about" class="hero-link">About the Guild</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>
</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 v-if="wikiArticles?.length" class="wiki-list">
<div
v-for="article in wikiArticles"
:key="article._id"
class="wiki-item"
>
<div class="block-inset wiki-item-inner">
<a :href="article.url" target="_blank">{{ article.title }}</a>
</div>
</div>
</div>
<div v-else class="block-inset">
<p class="empty">
<a href="https://wiki.ghostguild.org">Browse the wiki &rarr;</a>
</p>
</div>
</div>
</div>
<!-- PARCHMENT INSET -->
<ParchmentInset>
<div
class="label"
style="color: var(--candle-faint); margin-bottom: 12px"
>
From the Wiki
</div>
<template v-if="hasCustomWikiFeature">
<h2>{{ wikiFeature.title || DEFAULT_WIKI_FEATURE_TITLE }}</h2>
<p v-for="(para, i) in customWikiParagraphs" :key="i">{{ para }}</p>
</template>
<template v-else>
<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="https://wiki.ghostguild.org">practicing together</a>.
</p>
</template>
<p>
<a href="https://wiki.ghostguild.org">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 { data: wikiArticles } = await useFetch("/api/wiki/recent", {
query: { limit: 4 },
default: () => [],
});
const DEFAULT_WIKI_FEATURE_TITLE = "What is a cooperative studio?";
const { data: wikiFeature } = await useFetch(
"/api/site-content/homepage.wiki_feature",
{ default: () => ({ title: "", body: "" }) },
);
const hasCustomWikiFeature = computed(() => !!wikiFeature.value?.body?.trim());
const customWikiParagraphs = computed(() => {
const body = wikiFeature.value?.body?.trim() || "";
return body
.split(/\n{2,}/)
.map((p) => p.trim())
.filter(Boolean);
});
const circleData = [
{
value: "community",
label: "Community",
metaphor: "The open hall",
blurb:
"For anyone exploring cooperative models in game development. Solo devs, researchers, students, people who just heard about this and want to know more.",
},
{
value: "founder",
label: "Founder",
metaphor: "The workshop",
blurb:
"For people actively building cooperative studios. You're working through governance, legal structure, revenue sharing, and all the hard parts.",
},
{
value: "practitioner",
label: "Practitioner",
metaphor: "The alcove",
blurb:
"Where experience is shared and knowledge given back. You're here to support newcomers, help shape the Cooperative Foundations program, and find peers.",
},
];
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;
}
/* ---- 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>