Compare commits
27 commits
d15458b30a
...
9c7d6fa446
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c7d6fa446 | |||
| 5a69d6ab75 | |||
| d6cdf45838 | |||
| cb93f14160 | |||
| d93c16fbf7 | |||
| cad57b0083 | |||
| 1c2d1537a8 | |||
| 26791cc0e3 | |||
| 6527bbbe4e | |||
| 90acc35792 | |||
| dbd46cc157 | |||
| a9acc4c2dc | |||
| dadec1a273 | |||
| f85f284ea5 | |||
| 55c57d263d | |||
| 1da76b11cb | |||
| 350d6c219c | |||
| 05c47c4499 | |||
| 59d2be2df8 | |||
| 23154ff232 | |||
| a69c9d9b49 | |||
| dc2becf63e | |||
| e19b16a5cc | |||
| e756170884 | |||
| 7e44809a83 | |||
| f66455eda5 | |||
| 955217a941 |
40 changed files with 369 additions and 220 deletions
|
|
@ -158,7 +158,7 @@ const slackLinks = computed(() => {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.board-post {
|
.board-post {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
padding: 18px 22px;
|
padding: 20px 24px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
-webkit-column-break-inside: avoid;
|
-webkit-column-break-inside: avoid;
|
||||||
|
|
@ -219,7 +219,7 @@ const slackLinks = computed(() => {
|
||||||
|
|
||||||
.post-title {
|
.post-title {
|
||||||
font-family: "Brygada 1918", serif;
|
font-family: "Brygada 1918", serif;
|
||||||
font-size: 19px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ function handleSubmit() {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.post-form {
|
.post-form {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
padding: 14px 16px;
|
padding: 16px 16px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,7 +147,7 @@ function handleSubmit() {
|
||||||
}
|
}
|
||||||
.form-title {
|
.form-title {
|
||||||
font-family: "Brygada 1918", serif;
|
font-family: "Brygada 1918", serif;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
}
|
}
|
||||||
|
|
@ -183,7 +183,7 @@ function handleSubmit() {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
.circle-option {
|
.circle-option {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
padding: 14px 12px;
|
padding: 12px 12px;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
|
|
@ -83,7 +83,7 @@ defineEmits(['update:modelValue'])
|
||||||
}
|
}
|
||||||
|
|
||||||
.circle-tag {
|
.circle-tag {
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="series-badge p-4 bg-guild-800/50 dark:bg-guild-700/30 rounded-xl border border-guild-600 dark:border-guild-600"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-6">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="flex flex-wrap items-center gap-2 mb-2">
|
|
||||||
<span
|
|
||||||
class="series-badge__label text-sm font-semibold text-guild-300 dark:text-guild-300"
|
|
||||||
>
|
|
||||||
Part of a Series
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="totalEvents"
|
|
||||||
class="series-badge__count inline-flex items-center px-2 py-0.5 rounded-md bg-guild-700/50 dark:bg-guild-600/50 text-sm font-medium text-guild-200 dark:text-guild-200"
|
|
||||||
>
|
|
||||||
<template v-if="position">
|
|
||||||
Event {{ position }} of {{ totalEvents }}
|
|
||||||
</template>
|
|
||||||
<template v-else> {{ totalEvents }} events in series </template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h3
|
|
||||||
class="series-badge__title text-lg font-semibold text-guild-100 dark:text-guild-100 mb-2"
|
|
||||||
>
|
|
||||||
{{ title }}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
v-if="description"
|
|
||||||
class="series-badge__description text-sm text-guild-300 dark:text-guild-300"
|
|
||||||
>
|
|
||||||
{{ description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="seriesId" class="flex-shrink-0 self-start">
|
|
||||||
<UButton
|
|
||||||
:to="`/series/${seriesId}`"
|
|
||||||
color="primary"
|
|
||||||
size="md"
|
|
||||||
label="View Series"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
totalEvents: {
|
|
||||||
type: Number,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
seriesId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -160,12 +160,16 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Already Registered -->
|
<!-- Already Registered -->
|
||||||
<div v-else-if="alreadyRegistered" class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg">
|
<div
|
||||||
|
v-else-if="alreadyRegistered"
|
||||||
|
class="p-4"
|
||||||
|
style="background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<Icon name="heroicons:check-badge" class="w-6 h-6 text-candlelight-400 flex-shrink-0" />
|
<Icon name="heroicons:check-badge" class="w-6 h-6 flex-shrink-0" style="color: var(--candle)" />
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold text-candlelight-300 mb-1">You're Registered!</div>
|
<div class="font-semibold mb-1" style="color: var(--candle)">You're Registered!</div>
|
||||||
<div class="text-sm text-candlelight-400">
|
<div class="text-sm" style="color: var(--candle)">
|
||||||
You have a series pass and are registered for all {{ totalEvents }} events.
|
You have a series pass and are registered for all {{ totalEvents }} events.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -154,17 +154,19 @@
|
||||||
securely
|
securely
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label class="consent-field">
|
<div class="consent-block">
|
||||||
<input
|
<label class="consent-field">
|
||||||
v-model="form.createAccount"
|
<input
|
||||||
type="checkbox"
|
v-model="form.createAccount"
|
||||||
:disabled="processing"
|
type="checkbox"
|
||||||
>
|
:disabled="processing"
|
||||||
<span>Create a free guest account so I can manage my registration</span>
|
>
|
||||||
</label>
|
<span>Create a free guest account so I can manage my registration</span>
|
||||||
<p class="field-hint consent-hint">
|
</label>
|
||||||
Guest accounts let you view your tickets and register faster next time. We won't add you to member communications.
|
<p class="field-hint consent-hint">
|
||||||
</p>
|
Guest accounts let you view your tickets and register faster next time. We won't add you to member communications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -450,22 +452,26 @@ const formatEventDate = (date) => {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.consent-field {
|
.consent-block {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
column-gap: 8px;
|
||||||
|
row-gap: 4px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.consent-field {
|
||||||
|
display: contents;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin-bottom: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.consent-field input[type="checkbox"] {
|
.consent-field input[type="checkbox"] {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
flex-shrink: 0;
|
|
||||||
accent-color: var(--candle);
|
accent-color: var(--candle);
|
||||||
}
|
}
|
||||||
.consent-hint {
|
.consent-hint {
|
||||||
margin-bottom: 14px;
|
grid-column: 2;
|
||||||
padding-left: 24px;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ const formatDate = (dateStr) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.em-circle {
|
.em-circle {
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
padding: 14px 32px;
|
padding: 16px 28px;
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,16 @@
|
||||||
<img
|
<img
|
||||||
:src="transformedImageUrl"
|
:src="transformedImageUrl"
|
||||||
:alt="modelValue.alt || 'Event image'"
|
:alt="modelValue.alt || 'Event image'"
|
||||||
class="w-full h-48 object-cover rounded-lg border border-guild-700"
|
class="w-full h-48 object-cover"
|
||||||
|
style="border: 1px solid var(--border)"
|
||||||
@error="console.log('Image failed to load:', transformedImageUrl)"
|
@error="console.log('Image failed to load:', transformedImageUrl)"
|
||||||
@load="console.log('Image loaded successfully:', transformedImageUrl)"
|
@load="console.log('Image loaded successfully:', transformedImageUrl)"
|
||||||
/>
|
>
|
||||||
<button
|
<button
|
||||||
@click="removeImage"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute top-2 right-2 p-1 bg-ember-500 text-white rounded-full hover:bg-ember-600 transition-colors"
|
class="absolute top-2 right-2 p-1 rounded-full transition-colors"
|
||||||
|
style="background: var(--ember); color: var(--parch-text)"
|
||||||
|
@click="removeImage"
|
||||||
>
|
>
|
||||||
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -21,67 +23,89 @@
|
||||||
<!-- Upload Area -->
|
<!-- Upload Area -->
|
||||||
<div
|
<div
|
||||||
v-if="!modelValue?.url"
|
v-if="!modelValue?.url"
|
||||||
class="border-2 border-dashed border-guild-700 rounded-lg p-6 text-center hover:border-guild-600 transition-colors"
|
class="border-2 border-dashed p-6 text-center transition-colors"
|
||||||
|
:style="
|
||||||
|
isDragging
|
||||||
|
? 'border-color: var(--candle); background: color-mix(in srgb, var(--candle) 15%, transparent)'
|
||||||
|
: 'border-color: var(--border)'
|
||||||
|
"
|
||||||
@dragover.prevent="isDragging = true"
|
@dragover.prevent="isDragging = true"
|
||||||
@dragleave.prevent="isDragging = false"
|
@dragleave.prevent="isDragging = false"
|
||||||
@drop.prevent="handleDrop"
|
@drop.prevent="handleDrop"
|
||||||
:class="{ 'border-candlelight-400 bg-candlelight-900/20': isDragging }"
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="handleFileSelect"
|
|
||||||
class="hidden"
|
class="hidden"
|
||||||
/>
|
@change="handleFileSelect"
|
||||||
|
>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<Icon name="heroicons:photo" class="w-12 h-12 text-guild-400 mx-auto" />
|
<Icon
|
||||||
|
name="heroicons:photo"
|
||||||
|
class="w-12 h-12 mx-auto"
|
||||||
|
style="color: var(--text-dim)"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-guild-400">
|
<p style="color: var(--text-dim)">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="font-medium"
|
||||||
|
style="color: var(--candle)"
|
||||||
@click="$refs.fileInput.click()"
|
@click="$refs.fileInput.click()"
|
||||||
class="text-candlelight-400 hover:text-candlelight-300 font-medium"
|
|
||||||
>
|
>
|
||||||
Click to upload
|
Click to upload
|
||||||
</button>
|
</button>
|
||||||
or drag and drop
|
or drag and drop
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-guild-500">PNG, JPG, GIF up to 10MB</p>
|
<p class="text-sm" style="color: var(--text-faint)">
|
||||||
|
PNG, JPG, GIF up to 10MB
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alt Text Input -->
|
<!-- Alt Text Input -->
|
||||||
<div v-if="modelValue?.url">
|
<div v-if="modelValue?.url">
|
||||||
<label class="block text-sm font-medium text-guild-100 mb-1">
|
<label
|
||||||
|
class="block text-sm font-medium mb-1"
|
||||||
|
style="color: var(--text-bright)"
|
||||||
|
>
|
||||||
Alt Text (for accessibility)
|
Alt Text (for accessibility)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
:value="modelValue.alt || ''"
|
:value="modelValue.alt || ''"
|
||||||
@input="updateAltText($event.target.value)"
|
|
||||||
placeholder="Describe this image..."
|
placeholder="Describe this image..."
|
||||||
class="w-full bg-guild-800 border border-guild-700 rounded-lg px-3 py-2 text-guild-100 placeholder-guild-500 focus:ring-2 focus:ring-candlelight-500 focus:border-transparent"
|
class="w-full px-3 py-2"
|
||||||
/>
|
style="
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
"
|
||||||
|
@input="updateAltText($event.target.value)"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Progress -->
|
<!-- Upload Progress -->
|
||||||
<div v-if="isUploading" class="space-y-2">
|
<div v-if="isUploading" class="space-y-2">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-guild-400">Uploading...</span>
|
<span style="color: var(--text-dim)">Uploading...</span>
|
||||||
<span class="text-guild-400">{{ uploadProgress }}%</span>
|
<span style="color: var(--text-dim)">{{ uploadProgress }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-guild-800 rounded-full h-2">
|
<div
|
||||||
|
class="w-full rounded-full h-2"
|
||||||
|
style="background: var(--surface)"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-candlelight-600 h-2 rounded-full transition-all duration-300"
|
class="h-2 rounded-full transition-all duration-300"
|
||||||
:style="`width: ${uploadProgress}%`"
|
:style="`width: ${uploadProgress}%; background: var(--candle)`"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
<div v-if="errorMessage" class="text-sm text-ember-400">
|
<div v-if="errorMessage" class="text-sm" style="color: var(--ember)">
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="your.email@example.com"
|
placeholder="your.email@example.com"
|
||||||
required
|
required
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
|
|
@ -182,7 +182,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown))
|
||||||
|
|
||||||
.modal-overline {
|
.modal-overline {
|
||||||
font-family: 'Brygada 1918', serif;
|
font-family: 'Brygada 1918', serif;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
@ -218,7 +218,7 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown))
|
||||||
.info-box {
|
.info-box {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
padding: 10px 14px;
|
padding: 12px 16px;
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@
|
||||||
<Icon
|
<Icon
|
||||||
v-if="isValidParse && naturalInput.trim()"
|
v-if="isValidParse && naturalInput.trim()"
|
||||||
name="heroicons:check-circle"
|
name="heroicons:check-circle"
|
||||||
class="w-5 h-5 text-candlelight-500"
|
class="w-5 h-5"
|
||||||
|
style="color: var(--candle)"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
v-else-if="hasError && naturalInput.trim()"
|
v-else-if="hasError && naturalInput.trim()"
|
||||||
name="heroicons:exclamation-circle"
|
name="heroicons:exclamation-circle"
|
||||||
class="w-5 h-5 text-ember-500"
|
class="w-5 h-5"
|
||||||
|
style="color: var(--ember)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
|
|
@ -31,7 +33,8 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="parsedDate && isValidParse"
|
v-if="parsedDate && isValidParse"
|
||||||
class="text-sm text-candlelight-400 bg-candlelight-900/20 px-3 py-2 rounded-lg border border-candlelight-800"
|
class="text-sm px-3 py-2"
|
||||||
|
style="color: var(--candle); background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon name="heroicons:calendar" class="w-4 h-4" />
|
<Icon name="heroicons:calendar" class="w-4 h-4" />
|
||||||
|
|
@ -41,7 +44,8 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="hasError && naturalInput.trim()"
|
v-if="hasError && naturalInput.trim()"
|
||||||
class="text-sm text-ember-400 bg-ember-900/20 px-3 py-2 rounded-lg border border-ember-800"
|
class="text-sm px-3 py-2"
|
||||||
|
style="color: var(--ember); background: color-mix(in srgb, var(--ember) 15%, transparent); border: 1px solid var(--ember)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
|
<Icon name="heroicons:exclamation-triangle" class="w-4 h-4" />
|
||||||
|
|
@ -51,7 +55,7 @@
|
||||||
|
|
||||||
<!-- Fallback datetime-local input -->
|
<!-- Fallback datetime-local input -->
|
||||||
<details class="text-sm">
|
<details class="text-sm">
|
||||||
<summary class="cursor-pointer text-guild-400 hover:text-guild-100">
|
<summary class="cursor-pointer" style="color: var(--text-dim)">
|
||||||
Use traditional date picker
|
Use traditional date picker
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ const barEmpty = computed(() => '-'.repeat((4 - completedCount.value) * 2) + ']'
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
border: 1px dashed rgba(237, 228, 208, 0.25);
|
border: 1px dashed color-mix(in srgb, var(--parch-text) 25%, transparent);
|
||||||
color: var(--parch-accent);
|
color: var(--parch-accent);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
@ -134,7 +134,7 @@ const barEmpty = computed(() => '-'.repeat((4 - completedCount.value) * 2) + ']'
|
||||||
.ow-progress {
|
.ow-progress {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px dashed rgba(237, 228, 208, 0.12);
|
border-top: 1px dashed color-mix(in srgb, var(--parch-text) 12%, transparent);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--parch-text-dim);
|
color: var(--parch-text-dim);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -153,7 +153,7 @@ const barEmpty = computed(() => '-'.repeat((4 - completedCount.value) * 2) + ']'
|
||||||
}
|
}
|
||||||
|
|
||||||
.ow-bar-empty {
|
.ow-bar-empty {
|
||||||
color: rgba(237, 228, 208, 0.2);
|
color: color-mix(in srgb, var(--parch-text) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ow-skip {
|
.ow-skip {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error State -->
|
<!-- Error State -->
|
||||||
<div
|
<div v-else-if="error" class="error-state p-6">
|
||||||
v-else-if="error"
|
<h3 class="error-state__heading text-lg font-semibold mb-2">
|
||||||
class="p-6 bg-ember-900/20 rounded-xl border border-ember-800"
|
|
||||||
>
|
|
||||||
<h3 class="text-lg font-semibold text-ember-300 mb-2">
|
|
||||||
Unable to Load Series Pass
|
Unable to Load Series Pass
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-ember-400">{{ error }}</p>
|
<p class="error-state__body">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
|
|
@ -48,7 +45,7 @@
|
||||||
<!-- Registration Form -->
|
<!-- Registration Form -->
|
||||||
<div
|
<div
|
||||||
v-if="passInfo.available && !passInfo.alreadyRegistered"
|
v-if="passInfo.available && !passInfo.alreadyRegistered"
|
||||||
class="bg-guild-800/50 dark:bg-guild-700/30 rounded-xl border border-guild-600 dark:border-guild-600 p-6"
|
class="registration-form p-6"
|
||||||
>
|
>
|
||||||
<h3 class="text-xl font-bold text-[--ui-text] mb-6">
|
<h3 class="text-xl font-bold text-[--ui-text] mb-6">
|
||||||
{{
|
{{
|
||||||
|
|
@ -103,18 +100,20 @@
|
||||||
<!-- Member Benefits Notice -->
|
<!-- Member Benefits Notice -->
|
||||||
<div
|
<div
|
||||||
v-if="passInfo.ticket.isFree && passInfo.memberInfo?.isMember"
|
v-if="passInfo.ticket.isFree && passInfo.memberInfo?.isMember"
|
||||||
class="p-4 bg-candlelight-900/20 border border-candlelight-700/30 rounded-lg"
|
class="p-4"
|
||||||
|
style="background: color-mix(in srgb, var(--candle) 15%, transparent); border: 1px solid var(--candle)"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<Icon
|
<Icon
|
||||||
name="heroicons:sparkles"
|
name="heroicons:sparkles"
|
||||||
class="w-5 h-5 text-candlelight-400 flex-shrink-0 mt-0.5"
|
class="w-5 h-5 flex-shrink-0 mt-0.5"
|
||||||
|
style="color: var(--candle)"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-semibold text-candlelight-300 mb-1">
|
<div class="font-semibold mb-1" style="color: var(--candle)">
|
||||||
Member Benefit
|
Member Benefit
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-candlelight-400">
|
<div class="text-sm" style="color: var(--candle)">
|
||||||
This series pass is free for Ghost Guild members!
|
This series pass is free for Ghost Guild members!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -355,3 +354,18 @@ const formatPrice = (price, currency = "CAD") => {
|
||||||
}).format(price);
|
}).format(price);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-state {
|
||||||
|
background: color-mix(in srgb, var(--ember) 8%, transparent);
|
||||||
|
border: 1px dashed var(--ember);
|
||||||
|
}
|
||||||
|
.error-state__heading,
|
||||||
|
.error-state__body {
|
||||||
|
color: var(--ember);
|
||||||
|
}
|
||||||
|
.registration-form {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ const stepLabel = computed(() => {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background: rgba(42, 32, 21, 0.72);
|
background: color-mix(in srgb, var(--parch) 72%, transparent);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,16 @@
|
||||||
<div class="section-label">The Circles</div>
|
<div class="section-label">The Circles</div>
|
||||||
<div class="circles-grid">
|
<div class="circles-grid">
|
||||||
<div id="community" class="circle-cell">
|
<div id="community" class="circle-cell">
|
||||||
<h3 style="color: var(--c-community)">Community</h3>
|
<h2 style="color: var(--c-community)">Community</h2>
|
||||||
|
|
||||||
<p>For anyone exploring cooperative models.</p>
|
<p>For anyone exploring cooperative models.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="founder" class="circle-cell">
|
<div id="founder" class="circle-cell">
|
||||||
<h3 style="color: var(--c-founder)">Founder</h3>
|
<h2 style="color: var(--c-founder)">Founder</h2>
|
||||||
<p>For people actively building cooperatives.</p>
|
<p>For people actively building cooperatives.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="practitioner" class="circle-cell">
|
<div id="practitioner" class="circle-cell">
|
||||||
<h3 style="color: var(--c-practitioner)">Practitioner</h3>
|
<h2 style="color: var(--c-practitioner)">Practitioner</h2>
|
||||||
<p>For experienced practitioners sharing what they know.</p>
|
<p>For experienced practitioners sharing what they know.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -570,7 +570,7 @@ tbody td {
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--c-founder);
|
color: var(--c-founder);
|
||||||
border: 1px dashed rgba(138, 68, 32, 0.3);
|
border: 1px dashed color-mix(in srgb, var(--ember) 30%, transparent);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,7 +583,7 @@ tbody td {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--c-founder);
|
color: var(--c-founder);
|
||||||
border: 1px dashed rgba(138, 68, 32, 0.4);
|
border: 1px dashed color-mix(in srgb, var(--ember) 40%, transparent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -632,12 +632,12 @@ tbody td {
|
||||||
|
|
||||||
.status-upcoming {
|
.status-upcoming {
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
border-color: rgba(122, 90, 16, 0.3);
|
border-color: color-mix(in srgb, var(--candle) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-ongoing {
|
.status-ongoing {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
border-color: rgba(74, 106, 56, 0.3);
|
border-color: color-mix(in srgb, var(--green) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-past {
|
.status-past {
|
||||||
|
|
@ -647,7 +647,7 @@ tbody td {
|
||||||
|
|
||||||
.status-cancelled {
|
.status-cancelled {
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
border-color: rgba(138, 68, 32, 0.3);
|
border-color: color-mix(in srgb, var(--ember) 30%, transparent);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<span class="item-sub">{{ member.email }}</span>
|
<span class="item-sub">{{ member.email }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-meta">
|
<div class="item-meta">
|
||||||
<span class="badge" :class="member.circle">{{ member.circle }}</span>
|
<CircleBadge :circle="member.circle" />
|
||||||
<span class="item-date">{{ formatDate(member.createdAt) }}</span>
|
<span class="item-date">{{ formatDate(member.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<p v-if="member" class="member-email">{{ member.email }}</p>
|
<p v-if="member" class="member-email">{{ member.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="member" class="header-badges">
|
<div v-if="member" class="header-badges">
|
||||||
<span class="badge" :class="member.circle">{{ member.circle }}</span>
|
<CircleBadge :circle="member.circle" />
|
||||||
<span :class="statusClass(member.status)" class="status-badge">{{ member.status }}</span>
|
<span :class="statusClass(member.status)" class="status-badge">{{ member.status }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,6 +56,9 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Contribution ($/mo)</label>
|
<label>Contribution ($/mo)</label>
|
||||||
<input v-model.number="form.contributionAmount" type="number" min="0" step="1">
|
<input v-model.number="form.contributionAmount" type="number" min="0" step="1">
|
||||||
|
<p class="field-hint field-hint--warn">
|
||||||
|
Writes to our database only. If the member is on a paid plan, also update <code>recurringAmount</code> in the Helcim dashboard — this form does not sync.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Status</label>
|
<label>Status</label>
|
||||||
|
|
@ -534,6 +537,24 @@ onMounted(loadActivity)
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-faint);
|
||||||
|
margin: 6px 0 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint--warn {
|
||||||
|
color: var(--ember);
|
||||||
|
border-left: 2px solid var(--ember);
|
||||||
|
padding: 4px 0 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint code {
|
||||||
|
font-family: "Commit Mono", monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<select v-model="statusFilter" aria-label="Filter by status">
|
<select v-model="statusFilter" aria-label="Filter by status">
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="active">Active</option>
|
<option value="active">Active</option>
|
||||||
<option value="pending_payment">Pending Payment</option>
|
<option value="pending_payment">Payment setup incomplete</option>
|
||||||
<option value="suspended">Suspended</option>
|
<option value="suspended">Suspended</option>
|
||||||
<option value="cancelled">Cancelled</option>
|
<option value="cancelled">Cancelled</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -108,9 +108,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="col-email">{{ member.email }}</td>
|
<td class="col-email">{{ member.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="member.circle">{{
|
<CircleBadge :circle="member.circle" />
|
||||||
member.circle
|
|
||||||
}}</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="col-mono">${{ member.contributionAmount ?? 0 }}/mo</td>
|
<td class="col-mono">${{ member.contributionAmount ?? 0 }}/mo</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -269,7 +267,7 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Circle</th>
|
<th>Circle</th>
|
||||||
<th>Tier</th>
|
<th>Contribution</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -373,7 +371,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Status</label>
|
<label>Status</label>
|
||||||
<select v-model="editingMember.status">
|
<select v-model="editingMember.status">
|
||||||
<option value="pending_payment">Pending Payment</option>
|
<option value="pending_payment">Payment setup incomplete</option>
|
||||||
<option value="active">Active</option>
|
<option value="active">Active</option>
|
||||||
<option value="suspended">Suspended</option>
|
<option value="suspended">Suspended</option>
|
||||||
<option value="cancelled">Cancelled</option>
|
<option value="cancelled">Cancelled</option>
|
||||||
|
|
@ -490,7 +488,7 @@ const sortDir = ref("desc");
|
||||||
|
|
||||||
const STATUS_LABELS = {
|
const STATUS_LABELS = {
|
||||||
active: "Active",
|
active: "Active",
|
||||||
pending_payment: "Pending",
|
pending_payment: "Payment setup incomplete",
|
||||||
suspended: "Suspended",
|
suspended: "Suspended",
|
||||||
cancelled: "Cancelled",
|
cancelled: "Cancelled",
|
||||||
};
|
};
|
||||||
|
|
@ -1149,7 +1147,7 @@ th.sortable:hover {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.badge.status-active {
|
.badge.status-active {
|
||||||
color: var(--green, #3a6b3a);
|
color: var(--green);
|
||||||
border-color: rgba(58, 107, 58, 0.45);
|
border-color: rgba(58, 107, 58, 0.45);
|
||||||
}
|
}
|
||||||
.badge.status-pending_payment {
|
.badge.status-pending_payment {
|
||||||
|
|
@ -1158,7 +1156,7 @@ th.sortable:hover {
|
||||||
}
|
}
|
||||||
.badge.status-suspended {
|
.badge.status-suspended {
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
border-color: rgba(138, 68, 32, 0.45);
|
border-color: color-mix(in srgb, var(--ember) 45%, transparent);
|
||||||
}
|
}
|
||||||
.badge.status-cancelled {
|
.badge.status-cancelled {
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
|
|
@ -1306,7 +1304,7 @@ th.sortable:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-error {
|
.row-error {
|
||||||
background: rgba(138, 68, 32, 0.04);
|
background: color-mix(in srgb, var(--ember) 4%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- PREVIEW BOX ---- */
|
/* ---- PREVIEW BOX ---- */
|
||||||
|
|
|
||||||
|
|
@ -643,8 +643,8 @@ tbody td {
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-accepted {
|
.status-accepted {
|
||||||
color: var(--green, #4a7);
|
color: var(--green);
|
||||||
border-color: var(--green, #4a7);
|
border-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-expired {
|
.status-expired {
|
||||||
|
|
@ -671,7 +671,7 @@ tbody td {
|
||||||
|
|
||||||
/* ---- STATUS INDICATORS ---- */
|
/* ---- STATUS INDICATORS ---- */
|
||||||
.status-ok {
|
.status-ok {
|
||||||
color: var(--green, #4a7);
|
color: var(--green);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -850,7 +850,7 @@ const exportSeriesData = () => {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--c-founder);
|
color: var(--c-founder);
|
||||||
border: 1px dashed rgba(138, 68, 32, 0.4);
|
border: 1px dashed color-mix(in srgb, var(--ember) 40%, transparent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -931,12 +931,12 @@ const exportSeriesData = () => {
|
||||||
|
|
||||||
.status-active {
|
.status-active {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
border-color: rgba(74, 106, 56, 0.3);
|
border-color: color-mix(in srgb, var(--green) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-upcoming {
|
.status-upcoming {
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
border-color: rgba(122, 90, 16, 0.3);
|
border-color: color-mix(in srgb, var(--candle) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-completed {
|
.status-completed {
|
||||||
|
|
@ -946,7 +946,7 @@ const exportSeriesData = () => {
|
||||||
|
|
||||||
.status-ongoing {
|
.status-ongoing {
|
||||||
color: var(--green);
|
color: var(--green);
|
||||||
border-color: rgba(74, 106, 56, 0.3);
|
border-color: color-mix(in srgb, var(--green) 30%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- LINK BUTTONS ---- */
|
/* ---- LINK BUTTONS ---- */
|
||||||
|
|
|
||||||
|
|
@ -954,8 +954,8 @@ const applyBatchVisibility = async (hidden) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-created {
|
.sync-created {
|
||||||
color: var(--green, #4a7);
|
color: var(--green);
|
||||||
border-color: var(--green, #4a7);
|
border-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-updated {
|
.sync-updated {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ if (import.meta.server && !xsrf.value) {
|
||||||
.auth-title {
|
.auth-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ useHead({ title: "Signed Out — Ghost Guild" });
|
||||||
.auth-title {
|
.auth-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const hasDetail = computed(
|
||||||
.auth-title {
|
.auth-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
|
|
@ -97,7 +97,7 @@ const hasDetail = computed(
|
||||||
|
|
||||||
.auth-detail-code {
|
.auth-detail-code {
|
||||||
color: var(--ember);
|
color: var(--ember);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ function resetForm() {
|
||||||
|
|
||||||
.wiki-login-title {
|
.wiki-login-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 32px;
|
font-size: 36px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--candle);
|
color: var(--candle);
|
||||||
|
|
@ -240,7 +240,7 @@ function resetForm() {
|
||||||
.wiki-login-sent-heading {
|
.wiki-login-sent-heading {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -357,13 +357,13 @@ onMounted(async () => {
|
||||||
|
|
||||||
/* ---- LOADING / EMPTY ---- */
|
/* ---- LOADING / EMPTY ---- */
|
||||||
.loading-state {
|
.loading-state {
|
||||||
padding: 60px 24px;
|
padding: 64px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 60px 24px;
|
padding: 64px 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.empty-title {
|
.empty-title {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ const handleLogout = async () => {
|
||||||
.coming-soon-title {
|
.coming-soon-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ useHead({
|
||||||
}
|
}
|
||||||
.guidelines-section ul li {
|
.guidelines-section ul li {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 2px 0 2px 18px;
|
padding: 2px 0 2px 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
|
|
@ -365,7 +365,7 @@ useHead({
|
||||||
font-family: "Brygada 1918", serif;
|
font-family: "Brygada 1918", serif;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,10 @@ const isAlmostFull = (event) => {
|
||||||
border-color: var(--candle-faint);
|
border-color: var(--candle-faint);
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
|
.past-toggle:focus-visible {
|
||||||
|
outline: 2px dashed var(--candle);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
.past-toggle.active {
|
.past-toggle.active {
|
||||||
border-color: var(--candle);
|
border-color: var(--candle);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
|
|
|
||||||
|
|
@ -747,7 +747,7 @@ onUnmounted(() => {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.tier-list li {
|
.tier-list li {
|
||||||
padding: 5px 0;
|
padding: 4px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@
|
||||||
form.contributionAmount === Number(memberData.contributionAmount || 0) ||
|
form.contributionAmount === Number(memberData.contributionAmount || 0) ||
|
||||||
isUpdating
|
isUpdating
|
||||||
"
|
"
|
||||||
@click="handleUpdateTier"
|
@click="handleUpdateContribution"
|
||||||
>
|
>
|
||||||
{{ isUpdating ? "Updating…" : "Update Contribution" }}
|
{{ isUpdating ? "Updating…" : "Update Contribution" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -482,7 +482,7 @@ const refreshNextBillingIfStale = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateTier = async () => {
|
const handleUpdateContribution = async () => {
|
||||||
isUpdating.value = true;
|
isUpdating.value = true;
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/members/update-contribution", {
|
await $fetch("/api/members/update-contribution", {
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@
|
||||||
<span class="profile-pronouns">{{ member.pronouns }}</span>
|
<span class="profile-pronouns">{{ member.pronouns }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-meta">
|
<div class="profile-meta">
|
||||||
<span v-if="member.circle" class="badge" :class="member.circle">
|
<CircleBadge v-if="member.circle" :circle="member.circle" :label="circleLabels[member.circle]" />
|
||||||
{{ circleLabels[member.circle] }}
|
|
||||||
</span>
|
|
||||||
<template v-if="member.studio">
|
<template v-if="member.studio">
|
||||||
<span class="meta-sep">·</span>
|
<span class="meta-sep">·</span>
|
||||||
<span class="profile-studio">{{ member.studio }}</span>
|
<span class="profile-studio">{{ member.studio }}</span>
|
||||||
|
|
@ -372,7 +370,7 @@ useHead({
|
||||||
}
|
}
|
||||||
.profile-name {
|
.profile-name {
|
||||||
font-family: "Brygada 1918", serif;
|
font-family: "Brygada 1918", serif;
|
||||||
font-size: 42px;
|
font-size: 36px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ const getEventStatus = (event) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 28px;
|
padding: 12px 28px;
|
||||||
border-bottom: 1px dashed var(--border);
|
border-bottom: 1px dashed var(--border);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,13 +132,16 @@ Not blocking launch — the amendment hasn't passed yet, and the user-visible co
|
||||||
|
|
||||||
See `docs/TODO.md` for:
|
See `docs/TODO.md` for:
|
||||||
- Button minimum target size (WCAG AAA 2.5.5).
|
- Button minimum target size (WCAG AAA 2.5.5).
|
||||||
- `/oidc/interaction/[uid]` routing quirk.
|
- ~~`/oidc/interaction/[uid]` routing quirk~~ — fixed 2026-04-29 (commit `23154ff`); root cause was `oidc-provider`'s `devInteractions` overriding our custom `interactions.url`.
|
||||||
- Admin layout migration from `guild-*` tokens to zine spec.
|
- ~~Admin layout migration from `guild-*` tokens to zine spec~~ — verified clean 2026-04-29; grep for `guild-[0-9]|candlelight-[0-9]|ember-[0-9]` across `app/layouts/`, `app/pages/admin/`, `app/components/admin/` returns zero matches. All tokens already converted.
|
||||||
- Admin dashboard quick-action button contrast.
|
- ~~Admin dashboard quick-action button contrast~~ — verified stale 2026-04-29.
|
||||||
- Members table NAME column clipping.
|
- ~~Members table NAME column clipping~~ — verified stale 2026-04-29.
|
||||||
- OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption).
|
- OWASP ASVS L1 Phase 4 (file-upload validation pipeline, granular RBAC, credential encryption).
|
||||||
- `tickets/available.get.js:115` `memberSavings` block reports `$0 saved` for inactive members — cosmetic; suppress comparison block when `!hasMemberAccess(member)` if it ever surfaces in UI.
|
- ~~`tickets/available.get.js:115` `memberSavings` block reports `$0 saved` for inactive members~~ — fixed 2026-04-29 (commit `f66455e`); `memberSavings` now gated on `hasMemberAccess(member)`.
|
||||||
- Simplify-pass follow-ups (2026-04-25): source-grep test bloat, login/verify rate-limit gap, stringly-typed `metadata.type`, reconcile-payments sequential loop, stale `new Date()` in events list, `loadPublicSeries` helper extraction.
|
- Simplify-pass follow-ups (2026-04-25): SHIPPED 2026-04-27 on branch `chore/simplify-pass-follow-ups` (pending merge). See `~/.claude/projects/-Users-jennie-Sites-ghostguild-org/memory/project_simplify_pass_2026_04_25.md`.
|
||||||
|
- ~~Reconcile `customerCode` bug~~ — fixed on `main` in commit `3c38333` ("pass customerCode (not helcimCustomerId) to Helcim transactions API"). Verified in `server/api/internal/reconcile-payments.post.js:97`.
|
||||||
|
- ~~Drive-by from 2026-04-29 phantom-Tailwind sweep: `app/components/EventSeriesBadge.vue` has zero usages~~ — deleted 2026-04-29 (commit `f85f284`); 81 lines removed.
|
||||||
|
- Simplify-pass follow-ups (2026-04-29): smallest wins shipped in commit `26791cc`; deferred items (rename `setPaymentBridgeCookie`, dedup admin `STATUS_LABELS`, extract `.tint-candle`/`.tint-ember` utilities, audit `member &&` truthy checks in sibling routes, restore `ImageUpload` alt-text input focus styling) tracked in `docs/TODO.md` § _Simplify-pass follow-ups — 2026-04-29_.
|
||||||
|
|
||||||
### Known gotchas worth addressing post-launch
|
### Known gotchas worth addressing post-launch
|
||||||
|
|
||||||
|
|
@ -150,14 +153,13 @@ See `docs/TODO.md` for:
|
||||||
|
|
||||||
Context: Phase 4 audit against `docs/specs/events-visual-audit-findings.md` fixed all critical phantom-palette, rounded-corner, CTA-mismatch, and input-styling issues across `EventTicketCard`, `EventTicketPurchase`, `EventSeriesTicketCard`, `SeriesPassPurchase`. Items below were explicitly deferred or out of reach.
|
Context: Phase 4 audit against `docs/specs/events-visual-audit-findings.md` fixed all critical phantom-palette, rounded-corner, CTA-mismatch, and input-styling issues across `EventTicketCard`, `EventTicketPurchase`, `EventSeriesTicketCard`, `SeriesPassPurchase`. Items below were explicitly deferred or out of reach.
|
||||||
|
|
||||||
- **Success-state color convention (4 instances).** "You're Registered!" blocks use `--candle` (gold) instead of `--green`. Touches `EventSeriesTicketCard.vue:186-196` (still uses phantom `candlelight-*` classes — preserved byte-for-byte pending decision) and registered-state wrappers in `SeriesPassPurchase.vue`. Needs a UX call on whether success should render gold (zine-consistent) or green (semantic). Once decided, finish the phantom-palette removal on those 4 lines.
|
- ~~**Success-state color convention (4 instances).**~~ Resolved 2026-04-29: gold (`--candle`) chosen as zine-consistent. Phantom-Tailwind cleanup shipped in `dc2becf` (`EventSeriesTicketCard.vue` + `SeriesPassPurchase.vue` member-benefit notice).
|
||||||
- **Sidebar breakpoint unverified.** `app/layouts/default.vue:89` hides the sidebar at ≤1024px per spec. Browser `resize_window` tool refused viewport changes during the audit, so the actual crossover and any layout shift at 1023–1025px was never visually confirmed. Do a manual responsive check before declaring the sidebar pattern shipped.
|
- ~~**Sidebar breakpoint unverified.**~~ Verified clean 2026-04-29 — `.events-mini` hides at ≤1024px cleanly across 1023/1024/1025/1100. Actual rule lives in `EventsMiniSidebar.vue:129` + `ColumnsLayout.vue:83` (audit doc cited the wrong line).
|
||||||
- **`EventTicketPurchase.vue:469` magic padding.** `.consent-hint { padding-left: 24px; }` is a hardcoded offset to align the hint under the checkbox text. Cosmetic; swap for a gap/grid approach when touching the consent block next.
|
- ~~**`EventTicketPurchase.vue:469` magic padding.**~~ Fixed 2026-04-29 (commit `7e44809`); consent block now uses a grid approach.
|
||||||
- **`.section-label` extraction candidate.** Several audited files repeat the same uppercase/letter-spaced small label pattern inline. Low-priority refactor into a utility class in `main.css`.
|
- ~~**`.section-label` extraction candidate.**~~ Verified 2026-04-29 — utility already exists at `main.css:128` and is used in 30+ places. Two scoped overrides intentionally diverge.
|
||||||
- **Past-events toggle component.** Existing, untouched this pass; noted in findings doc as a future consistency check.
|
- ~~**Past-events toggle component.**~~ Audited 2026-04-29 — consistent with the design system (dashed-border button, gold active state, valid `aria-pressed` toggle). Added missing `:focus-visible` outline in commit `dadec1a`; no other changes warranted.
|
||||||
|
|
||||||
### Contribution-amount redesign — cosmetic cleanup (naming only, not behavior)
|
### Contribution-amount redesign — cosmetic cleanup (naming only, not behavior)
|
||||||
- Rename admin members column header "Tier" → "Contribution" (`app/pages/admin/members/index.vue:265`).
|
|
||||||
- Update error log message referencing "tier" in `server/api/members/update-contribution.post.js:221`.
|
SHIPPED 2026-04-29 in commit `955217a` (admin column header, dropdown labels, handler rename, log message).
|
||||||
- Rename `handleUpdateTier` handler in `app/pages/member/account.vue`.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
checkTicketAvailability,
|
checkTicketAvailability,
|
||||||
checkUserSeriesPass,
|
checkUserSeriesPass,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
|
hasMemberAccess,
|
||||||
} from "../../../../utils/tickets.js";
|
} from "../../../../utils/tickets.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,7 +112,7 @@ export default defineEventHandler(async (event) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member && eventData.tickets?.public?.available) {
|
if (hasMemberAccess(member) && eventData.tickets?.public?.available) {
|
||||||
response.publicTicket = {
|
response.publicTicket = {
|
||||||
price: eventData.tickets.public.price,
|
price: eventData.tickets.public.price,
|
||||||
formattedPrice: formatPrice(
|
formattedPrice: formatPrice(
|
||||||
|
|
|
||||||
|
|
@ -88,12 +88,11 @@ export default defineEventHandler(async (event) => {
|
||||||
member
|
member
|
||||||
})
|
})
|
||||||
|
|
||||||
// Paid-tier signups need to complete Helcim checkout in the same tab
|
// Signup completes (paid checkout or free activation) before the magic
|
||||||
// before the magic link can be clicked. Issue a short-lived, payment-only
|
// link is clicked, so issue a short-lived, payment-only bridge cookie
|
||||||
// bridge cookie so /api/helcim/initialize-payment accepts the request.
|
// that lets /api/helcim/initialize-payment and /api/helcim/subscription
|
||||||
if (body.contributionAmount > 0) {
|
// identify the member without a verified auth session.
|
||||||
setPaymentBridgeCookie(event, member)
|
setPaymentBridgeCookie(event, member)
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,10 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberCadence = member.billingCadence || 'monthly';
|
const memberCadence = member.billingCadence || 'monthly';
|
||||||
|
// TODO: Cadence-switch UI on /member/account. Plain Helcim subscription
|
||||||
|
// updates can't change billing period — would need a sub-replacement flow
|
||||||
|
// (cancel current, create new at desired cadence). See
|
||||||
|
// docs/LAUNCH_READINESS.md "Known gotchas" → "Cadence switch rejected".
|
||||||
if (body.cadence && body.cadence !== memberCadence) {
|
if (body.cadence && body.cadence !== memberCadence) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
|
|
@ -250,7 +254,7 @@ export default defineEventHandler(async (event) => {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.statusCode) throw error;
|
if (error.statusCode) throw error;
|
||||||
console.error("Error updating contribution tier:", error);
|
console.error("Error updating contribution amount:", error);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: "An unexpected error occurred",
|
statusMessage: "An unexpected error occurred",
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,7 @@ export async function getOidcProvider() {
|
||||||
},
|
},
|
||||||
|
|
||||||
features: {
|
features: {
|
||||||
devInteractions: {
|
devInteractions: { enabled: false },
|
||||||
enabled: process.env.NODE_ENV !== "production",
|
|
||||||
},
|
|
||||||
revocation: { enabled: true },
|
revocation: { enabled: true },
|
||||||
rpInitiatedLogout: {
|
rpInitiatedLogout: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
||||||
142
tests/server/api/free-signup-flow.test.js
Normal file
142
tests/server/api/free-signup-flow.test.js
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
import Member from '../../../server/models/member.js'
|
||||||
|
import { createHelcimCustomer } from '../../../server/utils/helcim.js'
|
||||||
|
import customerHandler from '../../../server/api/helcim/customer.post.js'
|
||||||
|
import subscriptionHandler from '../../../server/api/helcim/subscription.post.js'
|
||||||
|
import { resetRateLimit } from '../../../server/utils/rateLimit.js'
|
||||||
|
import { sendWelcomeEmail } from '../../../server/utils/resend.js'
|
||||||
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||||
|
|
||||||
|
// Deliberately does NOT mock server/utils/auth.js — the bridge-cookie
|
||||||
|
// hand-off between the two handlers is the contract under test.
|
||||||
|
|
||||||
|
vi.mock('../../../server/models/member.js', () => ({
|
||||||
|
default: {
|
||||||
|
findOne: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
findByIdAndUpdate: vi.fn(),
|
||||||
|
findById: vi.fn(),
|
||||||
|
findOneAndUpdate: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
|
||||||
|
vi.mock('../../../server/utils/helcim.js', () => ({
|
||||||
|
createHelcimCustomer: vi.fn(),
|
||||||
|
createHelcimSubscription: vi.fn(),
|
||||||
|
generateIdempotencyKey: vi.fn().mockReturnValue('idem-1'),
|
||||||
|
listHelcimCustomerTransactions: vi.fn().mockResolvedValue([])
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/magicLink.js', () => ({
|
||||||
|
sendMagicLink: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/resend.js', () => ({
|
||||||
|
sendWelcomeEmail: vi.fn().mockResolvedValue({ success: true })
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/slack.ts', () => ({
|
||||||
|
getSlackService: vi.fn().mockReturnValue(null)
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/payments.js', () => ({
|
||||||
|
upsertPaymentFromHelcim: vi.fn().mockResolvedValue({ created: true })
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.stubGlobal('helcimCustomerSchema', {})
|
||||||
|
vi.stubGlobal('helcimSubscriptionSchema', {})
|
||||||
|
|
||||||
|
const ALLOWED_ORIGIN = 'https://ghostguild.test'
|
||||||
|
const MEMBER_ID = '69f231152939bf109ac79d83'
|
||||||
|
|
||||||
|
const SUBSCRIPTION_BODY = {
|
||||||
|
customerId: 999,
|
||||||
|
customerCode: 'CST999',
|
||||||
|
contributionAmount: 0,
|
||||||
|
cadence: 'monthly',
|
||||||
|
cardToken: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBridgeCookie(event) {
|
||||||
|
const setCookie = event.node.res.getHeader('set-cookie')
|
||||||
|
const cookies = Array.isArray(setCookie) ? setCookie : [setCookie].filter(Boolean)
|
||||||
|
const match = cookies.find(c => typeof c === 'string' && c.startsWith('payment-bridge='))
|
||||||
|
if (!match) return null
|
||||||
|
return match.match(/payment-bridge=([^;]+)/)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('signup → subscription bridge-cookie hand-off', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
resetRateLimit()
|
||||||
|
process.env.BASE_URL = ALLOWED_ORIGIN
|
||||||
|
|
||||||
|
createHelcimCustomer.mockResolvedValue({ id: 999, customerCode: 'CST999' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('$0 signup: customer endpoint issues a bridge cookie that subscription endpoint accepts', async () => {
|
||||||
|
Member.findOne.mockResolvedValue(null)
|
||||||
|
Member.create.mockResolvedValue({
|
||||||
|
_id: MEMBER_ID,
|
||||||
|
email: 'free@example.com',
|
||||||
|
name: 'Free User',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0,
|
||||||
|
status: 'pending_payment'
|
||||||
|
})
|
||||||
|
|
||||||
|
const customerEvent = createMockEvent({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/helcim/customer',
|
||||||
|
headers: { origin: ALLOWED_ORIGIN },
|
||||||
|
body: {
|
||||||
|
name: 'Free User',
|
||||||
|
email: 'free@example.com',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0,
|
||||||
|
agreedToGuidelines: true,
|
||||||
|
billingAddress: { country: 'CA' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result1 = await customerHandler(customerEvent)
|
||||||
|
expect(result1.success).toBe(true)
|
||||||
|
expect(result1.member.status).toBe('pending_payment')
|
||||||
|
|
||||||
|
const bridgeToken = extractBridgeCookie(customerEvent)
|
||||||
|
expect(bridgeToken, 'payment-bridge cookie missing on $0 signup').toBeTruthy()
|
||||||
|
|
||||||
|
Member.findOneAndUpdate.mockResolvedValue({ _id: MEMBER_ID, status: 'pending_payment' })
|
||||||
|
Member.findById.mockResolvedValue({
|
||||||
|
_id: MEMBER_ID,
|
||||||
|
email: 'free@example.com',
|
||||||
|
name: 'Free User',
|
||||||
|
circle: 'community',
|
||||||
|
contributionAmount: 0,
|
||||||
|
status: 'active'
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscriptionEvent = createMockEvent({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/helcim/subscription',
|
||||||
|
headers: { origin: ALLOWED_ORIGIN },
|
||||||
|
cookies: { 'payment-bridge': bridgeToken },
|
||||||
|
body: SUBSCRIPTION_BODY
|
||||||
|
})
|
||||||
|
|
||||||
|
const result2 = await subscriptionHandler(subscriptionEvent)
|
||||||
|
expect(result2.success).toBe(true)
|
||||||
|
expect(result2.member.status).toBe('active')
|
||||||
|
expect(sendWelcomeEmail).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('$0 signup with no bridge cookie carried forward → subscription returns 401', async () => {
|
||||||
|
const subscriptionEvent = createMockEvent({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/helcim/subscription',
|
||||||
|
headers: { origin: ALLOWED_ORIGIN },
|
||||||
|
body: SUBSCRIPTION_BODY
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(subscriptionHandler(subscriptionEvent)).rejects.toMatchObject({
|
||||||
|
statusCode: 401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue