Initial commit
This commit is contained in:
parent
6fc1013745
commit
826517a798
18 changed files with 16576 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
12
Dockerfile
Normal 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
5
app/app.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<NuxtPage />
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
38
app/pages/admin/members.vue
Normal file
38
app/pages/admin/members.vue
Normal 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
30
app/pages/index.vue
Normal 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
44
app/pages/join.vue
Normal 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>
|
||||||
40
app/pages/members/index.vue
Normal file
40
app/pages/members/index.vue
Normal 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
6
eslint.config.mjs
Normal 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
9
nuxt.config.ts
Normal 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
16212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
package.json
Normal file
26
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
32
server/api/auth/login.post.js
Normal file
32
server/api/auth/login.post.js
Normal 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 }
|
||||||
|
})
|
||||||
25
server/api/members/create.post.js
Normal file
25
server/api/members/create.post.js
Normal 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
29
server/emails/welcome.js
Normal 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
24
server/models/member.js
Normal 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
18
tsconfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue