Initial commit

This commit is contained in:
Jennie Robinson Faber 2025-08-26 14:17:16 +01:00
parent 6fc1013745
commit 826517a798
18 changed files with 16576 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

5
app/app.vue Normal file
View file

@ -0,0 +1,5 @@
<template>
<UApp>
<NuxtPage />
</UApp>
</template>

View file

@ -0,0 +1,38 @@
<!-- pages/admin/members.vue -->
<template>
<UContainer>
<UTable :columns="columns" :rows="members" :loading="pending">
<template #actions-data="{ row }">
<UDropdown :items="actions(row)">
<UButton variant="ghost" icon="i-heroicons-ellipsis-horizontal" />
</UDropdown>
</template>
</UTable>
</UContainer>
</template>
<script setup>
const { data: members, pending } = await useFetch("/api/admin/members");
const columns = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "circle", label: "Circle" },
{ key: "contributionTier", label: "Contribution" },
{ key: "slackInvited", label: "Slack" },
{ key: "actions" },
];
const actions = (row) => [
[
{
label: "Send Slack Invite",
click: () => sendSlackInvite(row),
},
{
label: "View Details",
click: () => navigateTo(`/admin/members/${row._id}`),
},
],
];
</script>

30
app/pages/index.vue Normal file
View file

@ -0,0 +1,30 @@
<!-- pages/index.vue -->
<template>
<div>
<UContainer>
<UHero>
<template #title>
Pay what you can, take what you need, build what we dream
</template>
<template #description>
Ghost Guild: A solidarity-based community for game developers
exploring cooperative models
</template>
<template #actions>
<UButton to="/join" size="lg" color="purple">
Join Ghost Guild
</UButton>
</template>
</UHero>
<UGrid :cols="3" class="mt-16 gap-8">
<UCard v-for="circle in circles" :key="circle.id">
<template #header>
<h3>{{ circle.name }}</h3>
</template>
{{ circle.description }}
</UCard>
</UGrid>
</UContainer>
</div>
</template>

44
app/pages/join.vue Normal file
View file

@ -0,0 +1,44 @@
<!-- pages/join.vue -->
<template>
<UContainer class="py-12">
<UForm :state="form" @submit="handleSubmit">
<!-- Step 1: Basic Info -->
<UFormGroup label="Email" name="email">
<UInput v-model="form.email" type="email" />
</UFormGroup>
<!-- Step 2: Choose Circle -->
<URadioGroup
v-model="form.circle"
:options="circleOptions"
name="circle" />
<!-- Step 3: Choose Contribution -->
<URadioGroup
v-model="form.contribution"
:options="contributionOptions"
name="contribution" />
<!-- Step 4: Helcim Checkout -->
<div id="helcim-payment"></div>
<UButton type="submit" block> Complete Membership </UButton>
</UForm>
</UContainer>
</template>
<script setup>
const form = reactive({
email: "",
name: "",
circle: "community",
contribution: "15",
});
// Load Helcim.js
onMounted(() => {
const script = document.createElement("script");
script.src = "https://secure.helcim.app/helcim-pay.js";
document.head.appendChild(script);
});
</script>

View file

@ -0,0 +1,40 @@
<!-- pages/members/index.vue -->
<template>
<UDashboard>
<UDashboardPanel>
<UDashboardHeader>
<template #title> Welcome back, {{ member?.name }}! </template>
</UDashboardHeader>
<UGrid :cols="2" class="gap-4">
<UCard>
<template #header>Your Circle</template>
<p class="text-xl font-semibold">{{ member?.circle }}</p>
<UButton variant="soft" size="sm" class="mt-2">
Request Circle Change
</UButton>
</UCard>
<UCard>
<template #header>Your Contribution</template>
<p class="text-xl font-semibold">
${{ member?.contributionTier }}/month
</p>
<p class="text-sm text-gray-600">Supporting 2 solidarity spots</p>
<UButton variant="soft" size="sm" class="mt-2">
Adjust Contribution
</UButton>
</UCard>
</UGrid>
<UCard class="mt-6">
<template #header>Quick Links</template>
<UList>
<li><NuxtLink to="/members/resources">Resource Library</NuxtLink></li>
<li><a href="https://gamma-space.slack.com">Slack Community</a></li>
<li><NuxtLink to="/members/events">Upcoming Events</NuxtLink></li>
</UList>
</UCard>
</UDashboardPanel>
</UDashboard>
</template>

6
eslint.config.mjs Normal file
View file

@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

9
nuxt.config.ts Normal file
View file

@ -0,0 +1,9 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",
devtools: { enabled: true },
modules: ["@nuxt/eslint", "@nuxt/ui", "@nuxtjs/plausible"],
plausible: {
domain: "ghostguild.org",
},
});

16212
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "nuxt-app",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/eslint": "^1.9.0",
"@nuxt/ui": "^3.3.2",
"bcryptjs": "^3.0.2",
"eslint": "^9.34.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.18.0",
"nitro-cors": "^0.7.1",
"nuxt": "^4.0.3",
"resend": "^6.0.1",
"typescript": "^5.9.2",
"vue": "^3.5.20",
"vue-router": "^4.5.1"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

2
public/robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View file

@ -0,0 +1,32 @@
// server/api/auth/login.post.js
import jwt from 'jsonwebtoken'
import Member from '~/server/models/member'
export default defineEventHandler(async (event) => {
const { email } = await readBody(event)
const member = await Member.findOne({ email })
if (!member) {
throw createError({ statusCode: 404 })
}
// Send magic link via Resend
const token = jwt.sign(
{ memberId: member._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
)
await resend.emails.send({
from: 'Ghost Guild <noreply@ghostguild.org>',
to: email,
subject: 'Your Ghost Guild login link',
html: `
<a href="https://ghostguild.org/auth/verify?token=${token}">
Click here to log in
</a>
`
})
return { success: true }
})

View file

@ -0,0 +1,25 @@
// server/models/member.js
import mongoose from 'mongoose'
const memberSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
circle: {
type: String,
enum: ['community', 'founder', 'practitioner'],
required: true
},
contributionTier: {
type: String,
enum: ['0', '5', '15', '30', '50'],
required: true
},
helcimCustomerId: String,
helcimSubscriptionId: String,
slackInvited: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
lastLogin: Date
})
export default mongoose.model('Member', memberSchema)

29
server/emails/welcome.js Normal file
View file

@ -0,0 +1,29 @@
// server/emails/welcome.js
export const welcomeEmail = (member) => ({
from: 'Ghost Guild <welcome@ghostguild.org>',
to: member.email,
subject: 'Welcome to Ghost Guild! 👻',
html: `
<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Welcome to the community, ${member.name}!</h1>
<p>You've joined the <strong>${member.circle} circle</strong>
with a ${member.contributionTier}/month contribution.</p>
<h2>Your next steps:</h2>
<ol>
<li>Watch for your Slack invite (within 24 hours)</li>
<li>Explore the <a href="https://ghostguild.org/members/resources">resource library</a></li>
<li>Introduce yourself in #introductions</li>
</ol>
<p>Thank you for being part of our solidarity economy!</p>
<hr style="margin: 30px 0; border: 1px solid #eee;">
<p style="color: #666; font-size: 14px;">
Questions? Reply to this email or reach out in Slack.
</p>
</div>
`
})

24
server/models/member.js Normal file
View file

@ -0,0 +1,24 @@
// server/models/member.js
import mongoose from 'mongoose'
const memberSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
name: { type: String, required: true },
circle: {
type: String,
enum: ['community', 'founder', 'practitioner'],
required: true
},
contributionTier: {
type: String,
enum: ['0', '5', '15', '30', '50'],
required: true
},
helcimCustomerId: String,
helcimSubscriptionId: String,
slackInvited: { type: Boolean, default: false },
createdAt: { type: Date, default: Date.now },
lastLogin: Date
})
export default mongoose.model('Member', memberSchema)

18
tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}