feat(signup): community guidelines agreement and policies routes
Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
This commit is contained in:
parent
e0d11e47f4
commit
c5e901ed24
13 changed files with 1292 additions and 54 deletions
|
|
@ -32,7 +32,7 @@
|
||||||
class="form-input"
|
class="form-input"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="accept-email">Email</label>
|
<label class="form-label" for="accept-email">Email</label>
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
class="form-input"
|
class="form-input"
|
||||||
type="email"
|
type="email"
|
||||||
disabled
|
disabled
|
||||||
/>
|
>
|
||||||
<p class="field-note">Email cannot be changed. Contact us if you need to use a different email.</p>
|
<p class="field-note">Email cannot be changed. Contact us if you need to use a different email.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -53,7 +53,7 @@
|
||||||
class="form-input"
|
class="form-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. they/them, she/her"
|
placeholder="e.g. they/them, she/her"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="accept-location">City / Region</label>
|
<label class="form-label" for="accept-location">City / Region</label>
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
class="form-input"
|
class="form-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g. Vancouver, BC"
|
placeholder="e.g. Vancouver, BC"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="circle"
|
name="circle"
|
||||||
value="community"
|
value="community"
|
||||||
/>
|
>
|
||||||
<label for="circle-community">
|
<label for="circle-community">
|
||||||
<span class="circle-label-name" style="color: var(--c-community);">Community</span>
|
<span class="circle-label-name" style="color: var(--c-community);">Community</span>
|
||||||
<span class="circle-label-desc">Learning about co-ops</span>
|
<span class="circle-label-desc">Learning about co-ops</span>
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="circle"
|
name="circle"
|
||||||
value="founder"
|
value="founder"
|
||||||
/>
|
>
|
||||||
<label for="circle-founder">
|
<label for="circle-founder">
|
||||||
<span class="circle-label-name" style="color: var(--c-founder);">Founder</span>
|
<span class="circle-label-name" style="color: var(--c-founder);">Founder</span>
|
||||||
<span class="circle-label-desc">Building your studio</span>
|
<span class="circle-label-desc">Building your studio</span>
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
name="circle"
|
name="circle"
|
||||||
value="practitioner"
|
value="practitioner"
|
||||||
/>
|
>
|
||||||
<label for="circle-practitioner">
|
<label for="circle-practitioner">
|
||||||
<span class="circle-label-name" style="color: var(--c-practitioner);">Practitioner</span>
|
<span class="circle-label-name" style="color: var(--c-practitioner);">Practitioner</span>
|
||||||
<span class="circle-label-desc">Leading and mentoring</span>
|
<span class="circle-label-desc">Leading and mentoring</span>
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
class="form-input"
|
class="form-input"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="2-3 sentences about what you're looking for"
|
placeholder="2-3 sentences about what you're looking for"
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
|
|
@ -142,14 +142,12 @@
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input
|
<input
|
||||||
|
v-model="form.agreedToGuidelines"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="form.agreedToTerms"
|
>
|
||||||
/>
|
|
||||||
<span>
|
<span>
|
||||||
I've read and agree to the
|
I agree to the Ghost Guild
|
||||||
<NuxtLink to="/agreement" target="_blank">Member Agreement</NuxtLink>
|
<NuxtLink to="/community-guidelines" target="_blank">Community Guidelines</NuxtLink>.
|
||||||
and
|
|
||||||
<NuxtLink to="/guidelines" target="_blank">Code of Conduct</NuxtLink>
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -221,11 +219,11 @@ const form = reactive({
|
||||||
circle: "community",
|
circle: "community",
|
||||||
motivation: "",
|
motivation: "",
|
||||||
contributionTier: "15",
|
contributionTier: "15",
|
||||||
agreedToTerms: false,
|
agreedToGuidelines: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return form.name && form.circle && form.contributionTier && form.agreedToTerms;
|
return form.name && form.circle && form.contributionTier && form.agreedToGuidelines;
|
||||||
});
|
});
|
||||||
|
|
||||||
const needsPayment = computed(() => {
|
const needsPayment = computed(() => {
|
||||||
|
|
@ -283,7 +281,7 @@ const handleAccept = async () => {
|
||||||
circle: form.circle,
|
circle: form.circle,
|
||||||
motivation: form.motivation || undefined,
|
motivation: form.motivation || undefined,
|
||||||
contributionTier: form.contributionTier,
|
contributionTier: form.contributionTier,
|
||||||
agreedToTerms: form.agreedToTerms,
|
agreedToGuidelines: form.agreedToGuidelines,
|
||||||
token: token.value,
|
token: token.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
377
app/pages/community-guidelines.vue
Normal file
377
app/pages/community-guidelines.vue
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
<template>
|
||||||
|
<PageShell title="Community Guidelines" subtitle="What you're agreeing to when you join Ghost Guild">
|
||||||
|
<div class="guidelines-prose">
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Welcome</h2>
|
||||||
|
<p>
|
||||||
|
Ghost Guild is a community for game workers exploring cooperative and
|
||||||
|
worker-centric models. By joining, you're becoming part of a growing
|
||||||
|
community of practice built on mutual support, shared learning, and
|
||||||
|
solidarity.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This page covers everything you're agreeing to as a member. Related
|
||||||
|
policies are linked throughout and are part of this agreement.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>What Membership Means</h2>
|
||||||
|
<p>
|
||||||
|
Ghost Guild membership is about community and participation, not
|
||||||
|
access to hidden content. Every member gets the same access to
|
||||||
|
resources, events, and community spaces regardless of what they
|
||||||
|
contribute financially.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When you join Ghost Guild, you become a Class B member of Baby
|
||||||
|
Ghosts, our parent charity. Class A membership is held by a small
|
||||||
|
group involved in governance, mainly our directors. Class A and
|
||||||
|
Class B have equal access to resources, community, events, and the
|
||||||
|
Solidarity Fund. Voting at the Annual General Meeting is limited
|
||||||
|
to Class A members, as set out in our
|
||||||
|
<NuxtLink to="/policies/by-laws">by-laws</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>The three circles</h3>
|
||||||
|
<p>
|
||||||
|
Our three membership circles describe where you are in your journey
|
||||||
|
with cooperative models. They're not a hierarchy.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Community Circle:</strong> for folks learning about
|
||||||
|
cooperative principles
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Founder Circle:</strong> for those actively building a
|
||||||
|
cooperative studio
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Practitioner Circle:</strong> for experienced cooperative
|
||||||
|
studio leaders
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
You can move between circles as your work and interests evolve. Just
|
||||||
|
reach out to the Membership Committee when you're ready.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Solidarity economics</h3>
|
||||||
|
<p>
|
||||||
|
We operate on a pay-what-you-can model. Your contribution is fully
|
||||||
|
decoupled from your circle. Members with more financial capacity help
|
||||||
|
make space for members with less.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If money is tight, choose the $0 option. If you have more capacity,
|
||||||
|
contributing at a higher tier supports others. You can adjust your
|
||||||
|
contribution anytime as your situation changes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The Solidarity Fund is administered by the Membership Committee, and
|
||||||
|
its status is reported to the community each year.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Your Rights as a Member</h2>
|
||||||
|
<p>As a Ghost Guild member, you have:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Equal access to resources, events, community spaces, and the
|
||||||
|
Solidarity Fund, regardless of circle or contribution level
|
||||||
|
</li>
|
||||||
|
<li>Support from the Solidarity Fund if you face financial barriers</li>
|
||||||
|
<li>The ability to move between circles as your journey evolves</li>
|
||||||
|
<li>
|
||||||
|
Privacy protection in line with our
|
||||||
|
<NuxtLink to="/policies/privacy">Privacy Policy</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Your Responsibilities as a Member</h2>
|
||||||
|
<p>As a Ghost Guild member, you commit to:</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Upholding Baby Ghosts' and Gamma Space's shared values, including
|
||||||
|
cooperation, mutual support, and equity
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Treating fellow members with care and following our
|
||||||
|
<NuxtLink to="/policies/code-of-conduct">Code of Conduct</NuxtLink>
|
||||||
|
at all times
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Participating within your capacity. This is a community of
|
||||||
|
practice. Show up in whatever way works for you.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Contributing dues in line with your ability, or working with the
|
||||||
|
Membership Committee to access the Solidarity Fund
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Approaching disagreements with openness and using our
|
||||||
|
<NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink>
|
||||||
|
when conflicts arise
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Community privacy</h3>
|
||||||
|
<p>
|
||||||
|
Our community spaces, including our shared Slack workspace, operate
|
||||||
|
with an assumption of privacy. This means:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Don't share screenshots, message content, or other community
|
||||||
|
content externally without the explicit consent of everyone
|
||||||
|
involved
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Don't contribute community conversations, messages, or member
|
||||||
|
content to generative AI tools like ChatGPT or Claude. This
|
||||||
|
protects everyone's privacy and contributions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Violations of these privacy norms can result in removal from the
|
||||||
|
community
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Contributing to the Commons</h2>
|
||||||
|
<p>
|
||||||
|
The Ghost Guild wiki at
|
||||||
|
<a href="https://wiki.ghostguild.org">wiki.ghostguild.org</a> is a
|
||||||
|
knowledge commons. Anything you contribute to it is automatically and
|
||||||
|
irrevocably licensed under the
|
||||||
|
<a href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>
|
||||||
|
(CC-BY-SA 4.0) at the moment you post it.
|
||||||
|
</p>
|
||||||
|
<p>In plain terms:</p>
|
||||||
|
<ul>
|
||||||
|
<li>You still hold the copyright to what you wrote</li>
|
||||||
|
<li>
|
||||||
|
Anyone (members, the public, other cooperatives, organizations
|
||||||
|
adapting the material) can use, share, adapt, and build on your
|
||||||
|
contribution, including for commercial purposes, as long as they
|
||||||
|
credit you and release their derivatives under the same license
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You can't withdraw your contribution from the commons later, even
|
||||||
|
if you leave Ghost Guild
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If wiki material gets republished elsewhere (like on
|
||||||
|
<a href="https://coop.love">coop.love</a>), it stays under
|
||||||
|
CC-BY-SA 4.0 and you stay credited
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This is how a knowledge commons works, and it's central to what Ghost
|
||||||
|
Guild is doing. If you have something you'd rather keep private or
|
||||||
|
under a more restrictive license, don't put it in the wiki.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Profile information, bulletin board posts, comments in member-only
|
||||||
|
spaces, and direct messages aren't part of the commons and stay under
|
||||||
|
your control. See our
|
||||||
|
<NuxtLink to="/policies/terms">Terms of Service</NuxtLink> for the
|
||||||
|
details.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Our Privacy Commitments</h2>
|
||||||
|
<p>
|
||||||
|
Your personal information is used to administer your membership and
|
||||||
|
to communicate with you about Ghost Guild.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We use a small number of third-party services to run the platform
|
||||||
|
(payment processing, email, hosting, analytics). Our
|
||||||
|
<NuxtLink to="/policies/privacy">Privacy Policy</NuxtLink> lists who
|
||||||
|
they are and what they see.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We don't sell your data, share it for marketing, or feed any community
|
||||||
|
content into generative AI tools.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Membership Terms</h2>
|
||||||
|
<p>
|
||||||
|
Membership is valid for one year from joining or renewal. Dues can be
|
||||||
|
paid monthly or annually, and renewal happens by continuing dues
|
||||||
|
payments or arranging support through the Solidarity Fund.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can adjust your contribution to any amount, including $0, at any
|
||||||
|
time. There's no minimum contribution to maintain membership in good
|
||||||
|
standing. A failed monthly payment doesn't end your membership. If a
|
||||||
|
payment doesn't go through, we'll reach out to work it out.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can end your membership at any time by contacting the Membership
|
||||||
|
Committee. In rare cases, membership may be ended for serious
|
||||||
|
violations of these guidelines, following the process in our
|
||||||
|
<NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink>.
|
||||||
|
Dues are not refunded.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you leave, your wiki contributions remain in the commons under
|
||||||
|
their CC-BY-SA 4.0 license. Your other personal information is handled
|
||||||
|
according to the retention rules in our
|
||||||
|
<NuxtLink to="/policies/privacy">Privacy Policy</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Related Policies</h2>
|
||||||
|
<p>These policies are part of what you agree to by joining:</p>
|
||||||
|
<ul>
|
||||||
|
<li><NuxtLink to="/policies/code-of-conduct">Code of Conduct</NuxtLink></li>
|
||||||
|
<li><NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink></li>
|
||||||
|
<li><NuxtLink to="/policies/privacy">Privacy Policy</NuxtLink></li>
|
||||||
|
<li><NuxtLink to="/policies/terms">Terms of Service</NuxtLink></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="guidelines-section">
|
||||||
|
<h2>Agreement</h2>
|
||||||
|
<p>
|
||||||
|
By joining Ghost Guild, you're confirming that you've read,
|
||||||
|
understood, and agree to these community guidelines and the policies
|
||||||
|
linked above.
|
||||||
|
</p>
|
||||||
|
<p class="welcome-line">Welcome to the community, Ghostie!</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Community Guidelines · Ghost Guild',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.guidelines-prose {
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section {
|
||||||
|
padding: 28px 0;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.guidelines-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section h2 {
|
||||||
|
font-family: "Brygada 1918", serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-bright);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section h3 {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-bright);
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
.guidelines-section ul li {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.guidelines-section ul li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section ol {
|
||||||
|
list-style: none;
|
||||||
|
counter-reset: guideline-item;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
.guidelines-section ol li {
|
||||||
|
counter-increment: guideline-item;
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 28px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.guidelines-section ol li::before {
|
||||||
|
content: counter(guideline-item) ".";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
width: 22px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section a {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guidelines-section strong {
|
||||||
|
color: var(--text-bright);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-line {
|
||||||
|
font-family: "Brygada 1918", serif;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-bright);
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.guidelines-prose {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>Full access to the knowledge commons, Slack, and peer support</li>
|
<li>Full access to the knowledge commons, Slack, and peer support</li>
|
||||||
<li>Free access to all Ghost Guild events</li>
|
<li>Free access to all Ghost Guild events</li>
|
||||||
<li>One member, one vote</li>
|
<li>Equal access for every member, regardless of contribution</li>
|
||||||
<li>Your circle reflects where you are, not rank</li>
|
<li>Your circle reflects where you are, not rank</li>
|
||||||
<li>Pay what you can ($0–$50+/month, separate from circle)</li>
|
<li>Pay what you can ($0–$50+/month, separate from circle)</li>
|
||||||
<li>Higher contributions create solidarity spots for others</li>
|
<li>Higher contributions create solidarity spots for others</li>
|
||||||
|
|
@ -251,6 +251,20 @@
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input
|
||||||
|
v-model="form.agreedToGuidelines"
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
I agree to the Ghost Guild
|
||||||
|
<NuxtLink to="/community-guidelines" target="_blank"
|
||||||
|
>Community Guidelines</NuxtLink
|
||||||
|
>.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button
|
<button
|
||||||
class="form-submit"
|
class="form-submit"
|
||||||
|
|
@ -264,9 +278,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="form-note">
|
<p class="form-note">
|
||||||
By joining you agree to our
|
You can change your circle or contribution at any time from your
|
||||||
<NuxtLink to="/guidelines">community guidelines</NuxtLink>. You
|
|
||||||
can change your circle or contribution at any time from your
|
|
||||||
dashboard. Payment is handled securely through
|
dashboard. Payment is handled securely through
|
||||||
<a href="https://www.helcim.com" target="_blank" rel="noopener"
|
<a href="https://www.helcim.com" target="_blank" rel="noopener"
|
||||||
>Helcim</a
|
>Helcim</a
|
||||||
|
|
@ -397,6 +409,7 @@ const form = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
circle: "community",
|
circle: "community",
|
||||||
contributionTier: "15",
|
contributionTier: "15",
|
||||||
|
agreedToGuidelines: false,
|
||||||
billingAddress: {
|
billingAddress: {
|
||||||
street: "",
|
street: "",
|
||||||
city: "",
|
city: "",
|
||||||
|
|
@ -442,7 +455,13 @@ const {
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return form.name && form.email && form.circle && form.contributionTier;
|
return (
|
||||||
|
form.name &&
|
||||||
|
form.email &&
|
||||||
|
form.circle &&
|
||||||
|
form.contributionTier &&
|
||||||
|
form.agreedToGuidelines
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if payment is required
|
// Check if payment is required
|
||||||
|
|
@ -471,6 +490,7 @@ const handleSubmit = async () => {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
circle: form.circle,
|
circle: form.circle,
|
||||||
contributionTier: form.contributionTier,
|
contributionTier: form.contributionTier,
|
||||||
|
agreedToGuidelines: form.agreedToGuidelines,
|
||||||
billingAddress: form.billingAddress,
|
billingAddress: form.billingAddress,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -961,6 +981,25 @@ onUnmounted(() => {
|
||||||
color: var(--candle-dim);
|
color: var(--candle-dim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- CHECKBOX ---- */
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.checkbox-label input {
|
||||||
|
margin-top: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.checkbox-label a,
|
||||||
|
.checkbox-label :deep(a) {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- ERROR & SUCCESS BOXES ---- */
|
/* ---- ERROR & SUCCESS BOXES ---- */
|
||||||
.error-box {
|
.error-box {
|
||||||
border: 1px dashed var(--ember);
|
border: 1px dashed var(--ember);
|
||||||
|
|
|
||||||
79
app/pages/policies/[slug].vue
Normal file
79
app/pages/policies/[slug].vue
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<PageShell :title="policy.title" subtitle="Ghost Guild Policy">
|
||||||
|
<div class="policy-prose">
|
||||||
|
<p class="policy-status">This policy is being finalized.</p>
|
||||||
|
<p>
|
||||||
|
{{ policy.description }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The full text will be published here ahead of launch. In the meantime,
|
||||||
|
the expectations that apply are summarized in our
|
||||||
|
<NuxtLink to="/community-guidelines">Community Guidelines</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Questions? Contact the Membership Committee at
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// Note: /policies/code-of-conduct and /policies/conflict-resolution are
|
||||||
|
// handled by routeRules in nuxt.config.ts and redirect to external Obsidian
|
||||||
|
// pages. /policies/privacy and /policies/terms have dedicated pages
|
||||||
|
// (privacy.vue, terms.vue) that take precedence over this dynamic route.
|
||||||
|
const POLICIES = {
|
||||||
|
'by-laws': {
|
||||||
|
title: 'By-Laws',
|
||||||
|
description:
|
||||||
|
"Ghost Guild's governing by-laws, including membership classes, voting rights, and the structure of the Membership Committee.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const slug = String(route.params.slug || '')
|
||||||
|
const policy = POLICIES[slug]
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
throw createError({ statusCode: 404, statusMessage: 'Policy not found', fatal: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: `${policy.title} · Ghost Guild`,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.policy-prose {
|
||||||
|
max-width: 640px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-status {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--candle);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-prose p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-prose a {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.policy-prose {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
328
app/pages/policies/privacy.vue
Normal file
328
app/pages/policies/privacy.vue
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
<template>
|
||||||
|
<PageShell title="Privacy Policy" subtitle="How Ghost Guild handles your data">
|
||||||
|
<div class="policy-prose">
|
||||||
|
<p class="policy-updated">Last updated: April 18, 2026</p>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<p>
|
||||||
|
Ghost Guild is a program of Baby Ghosts, a Canadian non-profit. This
|
||||||
|
policy explains what information we collect when you use
|
||||||
|
ghostguild.org and wiki.ghostguild.org, what we do with it, and what
|
||||||
|
choices you have.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We treat your data as something you've trusted us with, not something
|
||||||
|
we own. If anything here is unclear or feels off, email us at
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>What we collect</h2>
|
||||||
|
<p>
|
||||||
|
<strong>When you apply for membership:</strong> your name, pronouns,
|
||||||
|
location, email, the answers you give in your application, and which
|
||||||
|
membership circle you're applying for.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>When you build a profile:</strong> anything you choose to add
|
||||||
|
(bio, studio affiliation, skills, social links, availability for peer
|
||||||
|
support, Slack handle). Your profile is visible to other Ghost Guild
|
||||||
|
members. It isn't made public or visible to search engines. If you
|
||||||
|
don't want something seen by other members, don't put it in your
|
||||||
|
profile.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>When you contribute to the wiki:</strong> your edits,
|
||||||
|
comments, and authorship are recorded so members can see who wrote
|
||||||
|
what and so we can roll back if needed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>When you post on the bulletin board:</strong> your posts
|
||||||
|
(needs you have, offers you can make), your name as the poster, and
|
||||||
|
the timestamps. The actual connecting between members happens in
|
||||||
|
Slack, not on our platform.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>When you pay dues:</strong> payment is handled by Helcim. We
|
||||||
|
see the amount, date, and that the payment came from you. We don't
|
||||||
|
see or store card numbers or banking details.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>When you get emails from us:</strong> Resend delivers them.
|
||||||
|
They process your email address and basic delivery metadata.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>For site analytics:</strong> we use Plausible. Plausible
|
||||||
|
doesn't use tracking cookies, doesn't store IP addresses, and doesn't
|
||||||
|
follow you across sites. It tells us aggregate things like which
|
||||||
|
pages get visited and roughly how many people use the site. It
|
||||||
|
doesn't tell us who you are.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We don't use Google Analytics, advertising pixels, or any other
|
||||||
|
tracking. The site uses cookies needed to keep you logged in. That's
|
||||||
|
it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Why we collect it</h2>
|
||||||
|
<p>
|
||||||
|
<strong>To run the membership program:</strong> review applications,
|
||||||
|
give you access, support your participation, send you things you've
|
||||||
|
signed up for.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>To help members find each other:</strong> making your profile
|
||||||
|
and bulletin board posts visible to the rest of the community.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>To report on impact:</strong> anonymized, aggregated numbers
|
||||||
|
for funders and the community (how many members, what topics come up
|
||||||
|
on the bulletin board, where members are based).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>To meet our obligations</strong> as a non-profit corporation
|
||||||
|
under Canadian law.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Who else sees it</h2>
|
||||||
|
<p>The services that store or process your data on our behalf:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Helcim, payment processing, Canada</li>
|
||||||
|
<li>Resend, transactional email, US</li>
|
||||||
|
<li>Hetzner, server hosting, Germany</li>
|
||||||
|
<li>Outline, wiki software running on our Hetzner server</li>
|
||||||
|
<li>Plausible, anonymous site analytics, EU</li>
|
||||||
|
<li>
|
||||||
|
Slack, if you accept an invitation to the shared Gamma Space
|
||||||
|
workspace
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<strong>Other Ghost Guild members:</strong> they can see your profile
|
||||||
|
and any bulletin board posts you make. None of this is public or
|
||||||
|
visible to search engines.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Wiki contributions:</strong> anything you post on
|
||||||
|
wiki.ghostguild.org is part of a public knowledge commons (see the
|
||||||
|
<NuxtLink to="/policies/terms">Terms of Service</NuxtLink> for the
|
||||||
|
specifics) and may be visible to anyone on the internet.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Baby Ghosts staff and the Membership Committee:</strong> for
|
||||||
|
purposes related to running the program.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>What we don't do</h2>
|
||||||
|
<p>We don't sell your data.</p>
|
||||||
|
<p>We don't share it with third parties for marketing.</p>
|
||||||
|
<p>
|
||||||
|
We don't feed your messages, profile, applications, bulletin board
|
||||||
|
posts, or other community content into generative AI tools, and we
|
||||||
|
don't allow members to do that with each other's content either. This
|
||||||
|
is a community commitment, not just a policy line.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If we ever want to do something different than what's in this policy,
|
||||||
|
we'll ask first.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>How long we keep it</h2>
|
||||||
|
<p>
|
||||||
|
<strong>While you're a member:</strong> as long as you have an
|
||||||
|
account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>After you leave:</strong> we keep basic account records for
|
||||||
|
one year for administrative reasons (renewals, returning members,
|
||||||
|
financial records we're required to keep). Your wiki contributions
|
||||||
|
remain in the commons under their license; that's how a commons works
|
||||||
|
(see <NuxtLink to="/policies/terms">Terms of Service</NuxtLink>).
|
||||||
|
Your bulletin board posts are removed when you close your account.
|
||||||
|
Aggregate impact data has no expiry and doesn't identify you.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Financial records:</strong> we keep what we have to keep
|
||||||
|
under tax law (currently six years).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can ask us to delete your account and personal data at any time.
|
||||||
|
We'll do it unless we're legally required to retain something
|
||||||
|
specific, in which case we'll tell you what and why.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Your rights</h2>
|
||||||
|
<p>You can:</p>
|
||||||
|
<ul>
|
||||||
|
<li>See what we have about you</li>
|
||||||
|
<li>Correct anything that's wrong</li>
|
||||||
|
<li>Download your data</li>
|
||||||
|
<li>Delete your account</li>
|
||||||
|
<li>Withdraw consent for anything you previously agreed to</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
To do any of these, email
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>. We'll
|
||||||
|
respond within 30 days.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you live in a province with its own privacy law (Quebec, British
|
||||||
|
Columbia, Alberta), you may have additional rights under that law.
|
||||||
|
PIPEDA may also apply to some of what we do. We'll respect those
|
||||||
|
rights regardless of which framework technically covers your
|
||||||
|
situation.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Security</h2>
|
||||||
|
<p>
|
||||||
|
We host on a private server, encrypt data in transit, and limit
|
||||||
|
access to staff who need it. Perfect security doesn't exist. If
|
||||||
|
something happens that affects your data, we'll tell you within a
|
||||||
|
reasonable time and explain what we're doing about it.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Children</h2>
|
||||||
|
<p>
|
||||||
|
Ghost Guild is for adults. We don't knowingly collect data from
|
||||||
|
anyone under 18.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Changes</h2>
|
||||||
|
<p>
|
||||||
|
If we change this policy in a way that affects how we handle your
|
||||||
|
data, we'll email members and post the change with a date stamp at
|
||||||
|
least 30 days before it takes effect. Continued use after that means
|
||||||
|
you accept the changes. If you don't, close your account first.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
Questions, requests, complaints:
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>
|
||||||
|
</p>
|
||||||
|
<p class="policy-address">
|
||||||
|
Baby Ghosts<br>
|
||||||
|
3230 Yonge Street #4052<br>
|
||||||
|
Toronto ON M4N 3P6<br>
|
||||||
|
Canada
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Privacy Policy · Ghost Guild',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.policy-prose {
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-updated {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--candle);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section {
|
||||||
|
padding: 28px 0;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.policy-section:first-of-type {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.policy-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section h2 {
|
||||||
|
font-family: "Brygada 1918", serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-bright);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
.policy-section ul li {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.policy-section ul li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section a {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section strong {
|
||||||
|
color: var(--text-bright);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-address {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.policy-prose {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
357
app/pages/policies/terms.vue
Normal file
357
app/pages/policies/terms.vue
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
<template>
|
||||||
|
<PageShell title="Terms of Service" subtitle="Using ghostguild.org and wiki.ghostguild.org">
|
||||||
|
<div class="policy-prose">
|
||||||
|
<p class="policy-updated">Last updated: April 18, 2026</p>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<p>
|
||||||
|
These terms apply to your use of ghostguild.org and
|
||||||
|
wiki.ghostguild.org, run by Baby Ghosts (a Canadian non-profit). The
|
||||||
|
<NuxtLink to="/community-guidelines">Member Agreement</NuxtLink> and
|
||||||
|
<NuxtLink to="/policies/code-of-conduct">Code of Conduct</NuxtLink>
|
||||||
|
also apply if you're a member; they sit alongside these terms, and
|
||||||
|
the more specific document wins if there's ever a conflict.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Who can use Ghost Guild</h2>
|
||||||
|
<p>You can browse public pages without an account.</p>
|
||||||
|
<p>
|
||||||
|
To become a member, you have to be 18 or older, complete an
|
||||||
|
application, and be accepted by the Membership Committee. Membership
|
||||||
|
is reviewed against our values and processes; we can decline
|
||||||
|
applications, and we don't always provide detailed reasons.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Your account</h2>
|
||||||
|
<p>
|
||||||
|
You're responsible for what happens under your account. Don't share
|
||||||
|
login credentials. If you suspect unauthorized access, tell us at
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We may suspend or close accounts that violate these terms, the
|
||||||
|
<NuxtLink to="/policies/code-of-conduct">Code of Conduct</NuxtLink>,
|
||||||
|
or the
|
||||||
|
<NuxtLink to="/community-guidelines">Member Agreement</NuxtLink>. The
|
||||||
|
process for that lives in our
|
||||||
|
<NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We don't suspend or close accounts because a payment didn't go
|
||||||
|
through. If there's a problem with your contribution, we'll reach
|
||||||
|
out.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Membership and dues</h2>
|
||||||
|
<p>
|
||||||
|
Membership runs annually and renews unless you cancel. You can pay
|
||||||
|
your annual dues all at once or in monthly installments; both result
|
||||||
|
in the same annual membership.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Dues are pay-what-you-can: you choose your contribution, and you can
|
||||||
|
change it any time as your situation changes. The full pricing
|
||||||
|
structure is on the membership page.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We don't refund dues. If paying ever becomes a problem, the
|
||||||
|
Solidarity Fund is there for that reason; reach out and we'll work it
|
||||||
|
out.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Acceptable use</h2>
|
||||||
|
<p>
|
||||||
|
The
|
||||||
|
<NuxtLink to="/policies/code-of-conduct">Code of Conduct</NuxtLink>
|
||||||
|
governs how members behave in Ghost Guild spaces. Some basics that
|
||||||
|
also apply here:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Don't harass, harm, or discriminate against other members</li>
|
||||||
|
<li>
|
||||||
|
Don't share other members' content (Slack messages, profile
|
||||||
|
information, bulletin board posts, application materials, private
|
||||||
|
comments) outside the community without their consent
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Don't feed community content, including other members'
|
||||||
|
contributions, into generative AI tools
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Don't use Ghost Guild for illegal activity, spam, or attempts to
|
||||||
|
compromise the platform
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Don't impersonate others or misrepresent your identity or
|
||||||
|
affiliations
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Don't scrape the site or use automated tools to extract member data
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you violate these, we follow the
|
||||||
|
<NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink>.
|
||||||
|
Outcomes range from a conversation through to removal from the
|
||||||
|
community.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Your content</h2>
|
||||||
|
<p>
|
||||||
|
There are two different things happening with content on Ghost Guild,
|
||||||
|
and they work differently.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Profiles, bulletin board posts, comments, and other member-only content</h3>
|
||||||
|
<p>
|
||||||
|
You keep ownership of profile information, bulletin board posts,
|
||||||
|
comments, messages, and anything else you post in member-only spaces.
|
||||||
|
By posting these, you give Ghost Guild a non-exclusive license to
|
||||||
|
display them within the platform to other members.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you delete your account, we remove this content from member-facing
|
||||||
|
areas. Backups may persist for a reasonable period before being
|
||||||
|
purged.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Wiki contributions</h3>
|
||||||
|
<p>
|
||||||
|
The wiki at
|
||||||
|
<a href="https://wiki.ghostguild.org">wiki.ghostguild.org</a> is a
|
||||||
|
knowledge commons. When you contribute to it (creating pages, editing
|
||||||
|
existing ones, adding examples, leaving comments on wiki pages), your
|
||||||
|
contribution is automatically and irrevocably licensed under the
|
||||||
|
<a href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>
|
||||||
|
(CC-BY-SA 4.0).
|
||||||
|
</p>
|
||||||
|
<p>In plain terms:</p>
|
||||||
|
<ul>
|
||||||
|
<li>You still hold the copyright to what you wrote</li>
|
||||||
|
<li>
|
||||||
|
Anyone (members, the public, other cooperatives, organizations
|
||||||
|
adapting the material) can use, share, adapt, and build on your
|
||||||
|
contribution, including for commercial purposes, as long as they
|
||||||
|
credit you and license their derivatives under the same terms
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
You can't pull your contribution back out of the commons later,
|
||||||
|
even if you leave Ghost Guild
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If we publish wiki material in other places (like
|
||||||
|
<a href="https://coop.love">coop.love</a>), it stays under
|
||||||
|
CC-BY-SA 4.0 and you stay credited
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
This is how a knowledge commons works, and it's central to what Ghost
|
||||||
|
Guild is doing. We're explicit about it because we want you to
|
||||||
|
contribute knowing exactly what's happening to your work. If you have
|
||||||
|
something you'd rather keep private or under a more restrictive
|
||||||
|
license, don't put it in the wiki.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Our content</h2>
|
||||||
|
<p>
|
||||||
|
Educational materials, templates, and resources we publish on
|
||||||
|
ghostguild.org are for member use within the spirit of the
|
||||||
|
cooperative movement. Where a specific license is attached (like
|
||||||
|
Creative Commons), follow that license. We ask for attribution to
|
||||||
|
Ghost Guild or Baby Ghosts.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Third-party services</h2>
|
||||||
|
<p>
|
||||||
|
Ghost Guild relies on Helcim (payments), Resend (email), Hetzner
|
||||||
|
(hosting), Outline (wiki), Plausible (analytics), and Slack (via the
|
||||||
|
shared Gamma Space workspace). Their terms apply when you interact
|
||||||
|
with their services. We've picked them carefully but we can't
|
||||||
|
guarantee they'll never have outages, security issues, or policy
|
||||||
|
changes.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Disclaimers</h2>
|
||||||
|
<p>
|
||||||
|
Ghost Guild is provided as-is. We work to keep things running and
|
||||||
|
resources accurate, but we don't guarantee uptime, completeness, or
|
||||||
|
that information on the site is right for your specific situation.
|
||||||
|
Treat our templates and educational materials as starting points, not
|
||||||
|
legal, financial, or business advice. Talk to qualified professionals
|
||||||
|
before you make decisions that need them.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Limitation of liability</h2>
|
||||||
|
<p>
|
||||||
|
To the extent the law allows, Baby Ghosts isn't liable for indirect,
|
||||||
|
incidental, or consequential damages arising from your use of the
|
||||||
|
site. Our total liability is capped at the amount of dues you've paid
|
||||||
|
in the past 12 months (which may well be zero, given how our pricing
|
||||||
|
works).
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Disputes</h2>
|
||||||
|
<p>
|
||||||
|
For disputes between members, we use the
|
||||||
|
<NuxtLink to="/policies/conflict-resolution">Conflict Resolution Policy</NuxtLink>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For disputes with Baby Ghosts about these terms or your account,
|
||||||
|
please reach out first. Most things we can sort out by talking. If we
|
||||||
|
can't, the dispute will be governed by the laws of Ontario, Canada.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Changes</h2>
|
||||||
|
<p>
|
||||||
|
We'll update these terms from time to time. For changes that
|
||||||
|
materially affect your rights or our obligations, we'll email members
|
||||||
|
at least 30 days before the new terms take effect. Continued use
|
||||||
|
after that means you accept the changes. If you don't, close your
|
||||||
|
account before they kick in.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="policy-section">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
<p>
|
||||||
|
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>
|
||||||
|
</p>
|
||||||
|
<p class="policy-address">
|
||||||
|
Baby Ghosts<br>
|
||||||
|
3230 Yonge Street #4052<br>
|
||||||
|
Toronto ON M4N 3P6<br>
|
||||||
|
Canada
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</PageShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useHead({
|
||||||
|
title: 'Terms of Service · Ghost Guild',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.policy-prose {
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-updated {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--candle);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section {
|
||||||
|
padding: 28px 0;
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
.policy-section:first-of-type {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
.policy-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section h2 {
|
||||||
|
font-family: "Brygada 1918", serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-bright);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section h3 {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-bright);
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
.policy-section ul li {
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 0 2px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.policy-section ul li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 2px;
|
||||||
|
color: var(--candle-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section a {
|
||||||
|
color: var(--candle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-section strong {
|
||||||
|
color: var(--text-bright);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.policy-address {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.policy-prose {
|
||||||
|
padding: 20px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -36,6 +36,20 @@ export default defineNuxtConfig({
|
||||||
build: {
|
build: {
|
||||||
transpile: ["vue-cal"],
|
transpile: ["vue-cal"],
|
||||||
},
|
},
|
||||||
|
routeRules: {
|
||||||
|
"/policies/code-of-conduct": {
|
||||||
|
redirect: {
|
||||||
|
to: "https://publish.obsidian.md/baby-ghosts-corp-docs/Public/Policies/Code+of+Conduct",
|
||||||
|
statusCode: 302,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"/policies/conflict-resolution": {
|
||||||
|
redirect: {
|
||||||
|
to: "https://publish.obsidian.md/baby-ghosts-corp-docs/Public/Policies/Conflict+Resolution+Policy",
|
||||||
|
statusCode: 302,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
plausible: {
|
plausible: {
|
||||||
domain: "ghostguild.org",
|
domain: "ghostguild.org",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// Create a Helcim customer
|
// Create a Helcim customer
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
import Member from '../../models/member.js'
|
import Member from '../../models/member.js'
|
||||||
import { connectDB } from '../../utils/mongoose.js'
|
import { connectDB } from '../../utils/mongoose.js'
|
||||||
import { createHelcimCustomer } from '../../utils/helcim.js'
|
import { createHelcimCustomer } from '../../utils/helcim.js'
|
||||||
|
|
@ -7,7 +6,6 @@ import { createHelcimCustomer } from '../../utils/helcim.js'
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
await connectDB()
|
await connectDB()
|
||||||
const config = useRuntimeConfig(event)
|
|
||||||
const body = await validateBody(event, helcimCustomerSchema)
|
const body = await validateBody(event, helcimCustomerSchema)
|
||||||
|
|
||||||
// Check if member already exists
|
// Check if member already exists
|
||||||
|
|
@ -33,34 +31,16 @@ export default defineEventHandler(async (event) => {
|
||||||
circle: body.circle,
|
circle: body.circle,
|
||||||
contributionTier: body.contributionTier,
|
contributionTier: body.contributionTier,
|
||||||
helcimCustomerId: customerData.id,
|
helcimCustomerId: customerData.id,
|
||||||
status: 'pending_payment'
|
status: 'pending_payment',
|
||||||
|
agreement: { acceptedAt: new Date() }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Generate JWT token for the session
|
setAuthCookie(event, member)
|
||||||
const token = jwt.sign(
|
|
||||||
{
|
|
||||||
memberId: member._id,
|
|
||||||
email: body.email,
|
|
||||||
helcimCustomerId: customerData.id
|
|
||||||
},
|
|
||||||
config.jwtSecret,
|
|
||||||
{ expiresIn: '7d' }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set the session cookie server-side
|
|
||||||
setCookie(event, 'auth-token', token, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
maxAge: 60 * 60 * 24 * 7, // 7 days (matches verify.get.js and refresh.post.js)
|
|
||||||
path: '/',
|
|
||||||
domain: undefined // Let browser set domain automatically
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
customerId: customerData.id,
|
customerId: customerData.id,
|
||||||
customerCode: customerData.customerCode,
|
customerCode: customerData.customerCode,
|
||||||
token,
|
|
||||||
member: {
|
member: {
|
||||||
id: member._id,
|
id: member._id,
|
||||||
email: member.email,
|
email: member.email,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export default defineEventHandler(async (event) => {
|
||||||
contributionTier: body.contributionTier,
|
contributionTier: body.contributionTier,
|
||||||
bio: body.motivation || undefined,
|
bio: body.motivation || undefined,
|
||||||
status: body.contributionTier === '0' ? 'active' : 'pending_payment',
|
status: body.contributionTier === '0' ? 'active' : 'pending_payment',
|
||||||
|
agreement: { acceptedAt: new Date() },
|
||||||
})
|
})
|
||||||
|
|
||||||
await assignMemberNumber(member._id)
|
await assignMemberNumber(member._id)
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ const memberSchema = new mongoose.Schema({
|
||||||
inviteEmailSent: { type: Boolean, default: false },
|
inviteEmailSent: { type: Boolean, default: false },
|
||||||
inviteEmailSentAt: Date,
|
inviteEmailSentAt: Date,
|
||||||
|
|
||||||
|
agreement: {
|
||||||
|
acceptedAt: Date,
|
||||||
|
},
|
||||||
|
|
||||||
// Magic link single-use enforcement
|
// Magic link single-use enforcement
|
||||||
magicLinkJti: String,
|
magicLinkJti: String,
|
||||||
magicLinkJtiUsed: { type: Boolean, default: false },
|
magicLinkJtiUsed: { type: Boolean, default: false },
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,8 @@ export const helcimCustomerSchema = z.object({
|
||||||
name: z.string().min(1).max(200),
|
name: z.string().min(1).max(200),
|
||||||
email: z.string().trim().toLowerCase().email(),
|
email: z.string().trim().toLowerCase().email(),
|
||||||
circle: z.enum(['community', 'founder', 'practitioner']).optional(),
|
circle: z.enum(['community', 'founder', 'practitioner']).optional(),
|
||||||
contributionTier: z.enum(['0', '5', '15', '30', '50']).optional()
|
contributionTier: z.enum(['0', '5', '15', '30', '50']).optional(),
|
||||||
|
agreedToGuidelines: z.literal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const helcimInitializePaymentSchema = z.object({
|
export const helcimInitializePaymentSchema = z.object({
|
||||||
|
|
@ -147,7 +148,7 @@ export const seriesTicketPurchaseSchema = z.object({
|
||||||
name: z.string().min(1).max(200),
|
name: z.string().min(1).max(200),
|
||||||
email: z.string().trim().toLowerCase().email(),
|
email: z.string().trim().toLowerCase().email(),
|
||||||
paymentId: z.string().max(500).optional(),
|
paymentId: z.string().max(500).optional(),
|
||||||
ticketType: z.enum(['member', 'public', 'guest']).optional(),
|
ticketType: z.enum(['member', 'public', 'guest']),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const seriesTicketEligibilitySchema = z.object({
|
export const seriesTicketEligibilitySchema = z.object({
|
||||||
|
|
@ -345,7 +346,7 @@ export const inviteAcceptSchema = z.object({
|
||||||
circle: z.enum(['community', 'founder', 'practitioner']),
|
circle: z.enum(['community', 'founder', 'practitioner']),
|
||||||
motivation: z.string().max(5000).optional(),
|
motivation: z.string().max(5000).optional(),
|
||||||
contributionTier: z.enum(['0', '5', '15', '30', '50']),
|
contributionTier: z.enum(['0', '5', '15', '30', '50']),
|
||||||
agreedToTerms: z.literal(true),
|
agreedToGuidelines: z.literal(true),
|
||||||
token: z.string().min(1)
|
token: z.string().min(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,8 @@ describe('helcimCustomerSchema', () => {
|
||||||
it('accepts valid customer data', () => {
|
it('accepts valid customer data', () => {
|
||||||
const result = helcimCustomerSchema.safeParse({
|
const result = helcimCustomerSchema.safeParse({
|
||||||
name: 'Jane Doe',
|
name: 'Jane Doe',
|
||||||
email: 'jane@example.com'
|
email: 'jane@example.com',
|
||||||
|
agreedToGuidelines: true
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
@ -88,7 +89,8 @@ describe('helcimCustomerSchema', () => {
|
||||||
it('lowercases email', () => {
|
it('lowercases email', () => {
|
||||||
const result = helcimCustomerSchema.safeParse({
|
const result = helcimCustomerSchema.safeParse({
|
||||||
name: 'Jane',
|
name: 'Jane',
|
||||||
email: 'JANE@Example.COM'
|
email: 'JANE@Example.COM',
|
||||||
|
agreedToGuidelines: true
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(result.data.email).toBe('jane@example.com')
|
expect(result.data.email).toBe('jane@example.com')
|
||||||
|
|
@ -97,7 +99,8 @@ describe('helcimCustomerSchema', () => {
|
||||||
it('rejects invalid email', () => {
|
it('rejects invalid email', () => {
|
||||||
const result = helcimCustomerSchema.safeParse({
|
const result = helcimCustomerSchema.safeParse({
|
||||||
name: 'Jane',
|
name: 'Jane',
|
||||||
email: 'not-an-email'
|
email: 'not-an-email',
|
||||||
|
agreedToGuidelines: true
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
@ -106,11 +109,29 @@ describe('helcimCustomerSchema', () => {
|
||||||
const result = helcimCustomerSchema.safeParse({
|
const result = helcimCustomerSchema.safeParse({
|
||||||
name: 'Jane',
|
name: 'Jane',
|
||||||
email: 'jane@example.com',
|
email: 'jane@example.com',
|
||||||
|
agreedToGuidelines: true,
|
||||||
role: 'admin'
|
role: 'admin'
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(result.data).not.toHaveProperty('role')
|
expect(result.data).not.toHaveProperty('role')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('rejects missing agreedToGuidelines', () => {
|
||||||
|
const result = helcimCustomerSchema.safeParse({
|
||||||
|
name: 'Jane',
|
||||||
|
email: 'jane@example.com'
|
||||||
|
})
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects agreedToGuidelines:false', () => {
|
||||||
|
const result = helcimCustomerSchema.safeParse({
|
||||||
|
name: 'Jane',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
agreedToGuidelines: false
|
||||||
|
})
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('helcimSubscriptionSchema', () => {
|
describe('helcimSubscriptionSchema', () => {
|
||||||
|
|
@ -297,14 +318,16 @@ describe('seriesTicketPurchaseSchema', () => {
|
||||||
it('accepts valid series ticket purchase', () => {
|
it('accepts valid series ticket purchase', () => {
|
||||||
const result = seriesTicketPurchaseSchema.safeParse({
|
const result = seriesTicketPurchaseSchema.safeParse({
|
||||||
name: 'Buyer',
|
name: 'Buyer',
|
||||||
email: 'buyer@example.com'
|
email: 'buyer@example.com',
|
||||||
|
ticketType: 'member'
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rejects missing name', () => {
|
it('rejects missing name', () => {
|
||||||
const result = seriesTicketPurchaseSchema.safeParse({
|
const result = seriesTicketPurchaseSchema.safeParse({
|
||||||
email: 'buyer@example.com'
|
email: 'buyer@example.com',
|
||||||
|
ticketType: 'member'
|
||||||
})
|
})
|
||||||
expect(result.success).toBe(false)
|
expect(result.success).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import {
|
||||||
memberProfileUpdateSchema,
|
memberProfileUpdateSchema,
|
||||||
eventRegistrationSchema,
|
eventRegistrationSchema,
|
||||||
paymentVerifySchema,
|
paymentVerifySchema,
|
||||||
adminEventCreateSchema
|
adminEventCreateSchema,
|
||||||
|
seriesTicketPurchaseSchema
|
||||||
} from '../../../server/utils/schemas.js'
|
} from '../../../server/utils/schemas.js'
|
||||||
import { validateBody } from '../../../server/utils/validateBody.js'
|
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||||
|
|
||||||
|
|
@ -183,6 +184,42 @@ describe('memberProfileUpdateSchema', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('seriesTicketPurchaseSchema', () => {
|
||||||
|
const validBody = {
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
ticketType: 'member'
|
||||||
|
}
|
||||||
|
|
||||||
|
it('accepts member, public, and guest ticket types', () => {
|
||||||
|
for (const ticketType of ['member', 'public', 'guest']) {
|
||||||
|
const result = seriesTicketPurchaseSchema.safeParse({ ...validBody, ticketType })
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a body missing ticketType (closes pricing-mismatch gap)', () => {
|
||||||
|
const { ticketType, ...rest } = validBody
|
||||||
|
const result = seriesTicketPurchaseSchema.safeParse(rest)
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects null ticketType', () => {
|
||||||
|
const result = seriesTicketPurchaseSchema.safeParse({ ...validBody, ticketType: null })
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects an unknown ticketType value', () => {
|
||||||
|
const result = seriesTicketPurchaseSchema.safeParse({ ...validBody, ticketType: 'vip' })
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects empty-string ticketType', () => {
|
||||||
|
const result = seriesTicketPurchaseSchema.safeParse({ ...validBody, ticketType: '' })
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// --- validateBody integration tests ---
|
// --- validateBody integration tests ---
|
||||||
|
|
||||||
describe('validateBody', () => {
|
describe('validateBody', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue