ghostguild-org/app/pages/join.vue

752 lines
No EOL
28 KiB
Vue

<template>
<div>
<!-- Page Header -->
<PageHeader
title="Join"
subtitle="Become a member of our cooperative community and start building the future of game development together"
theme="blue"
size="large"
/>
<!-- Membership Sign Up Form -->
<section class="py-20 bg-white dark:bg-gray-900">
<UContainer class="max-w-4xl">
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-4">
Membership Sign Up
</h2>
<p class="text-gray-600 dark:text-gray-300">
Choose your circle and contribution level to get started
</p>
</div>
<!-- Step Indicators -->
<div class="flex justify-center mb-8">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 1
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-500'
]"
>
1
</div>
<span class="ml-2 font-medium" :class="currentStep === 1 ? 'text-blue-600' : 'text-gray-500'">
Information
</span>
</div>
<div v-if="needsPayment" class="w-16 h-1 bg-gray-200">
<div
class="h-full bg-blue-500 transition-all"
:style="{ width: currentStep >= 2 ? '100%' : '0%' }"
/>
</div>
<div v-if="needsPayment" class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 2
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-500'
]"
>
2
</div>
<span class="ml-2 font-medium" :class="currentStep === 2 ? 'text-blue-600' : 'text-gray-500'">
Payment
</span>
</div>
<div class="w-16 h-1 bg-gray-200">
<div
class="h-full bg-blue-500 transition-all"
:style="{ width: currentStep >= 3 ? '100%' : '0%' }"
/>
</div>
<div class="flex items-center">
<div
:class="[
'w-10 h-10 rounded-full flex items-center justify-center font-semibold',
currentStep >= 3
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-500'
]"
>
<span v-if="needsPayment">3</span>
<span v-else>2</span>
</div>
<span class="ml-2 font-medium" :class="currentStep === 3 ? 'text-blue-600' : 'text-gray-500'">
Confirmation
</span>
</div>
</div>
</div>
<!-- Error Message -->
<div v-if="errorMessage" class="mb-6">
<UAlert color="red" :title="errorMessage" />
</div>
<!-- Step 1: Information -->
<div v-if="currentStep === 1" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
<UForm :state="form" class="space-y-8" @submit="handleSubmit">
<!-- Personal Information -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<UFormField label="Full Name" name="name" required>
<UInput
v-model="form.name"
placeholder="Enter your full name"
size="xl"
class="w-full"
/>
</UFormField>
<UFormField label="Email Address" name="email" required>
<UInput
v-model="form.email"
type="email"
size="xl"
class="w-full"
placeholder="Enter your email address"
/>
</UFormField>
</div>
<!-- Circle Selection -->
<div>
<h3 class="text-lg font-semibold mb-4">Choose Your Circle</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label
v-for="option in circleOptions"
:key="option.value"
class="flex flex-col p-6 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md"
:class="
form.circle === option.value
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300'
"
>
<input
v-model="form.circle"
type="radio"
:value="option.value"
name="circle"
class="mb-3"
>
<div class="font-medium text-lg mb-2">{{ option.label }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ option.description }}
</div>
</label>
</div>
</div>
<!-- Contribution Selection -->
<div>
<h3 class="text-lg font-semibold mb-4">Choose Your Monthly Contribution</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<label
v-for="option in contributionOptions"
:key="option.value"
class="flex flex-col p-6 rounded-lg border-2 cursor-pointer transition-all hover:shadow-md"
:class="
form.contributionTier === option.value
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-300'
"
>
<input
v-model="form.contributionTier"
type="radio"
:value="option.value"
name="contributionTier"
class="mb-3"
>
<div class="font-medium text-lg mb-2">{{ option.label }}</div>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li v-for="feature in option.features.slice(0, 2)" :key="feature">
{{ feature }}
</li>
</ul>
</label>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-center pt-6">
<UButton
type="submit"
:loading="isSubmitting"
:disabled="!isFormValid"
size="xl"
class="px-12"
>
{{ needsPayment ? 'Continue to Payment' : 'Complete Registration' }}
</UButton>
</div>
</UForm>
</div>
<!-- Step 2: Payment -->
<div v-if="currentStep === 2" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
<div class="mb-6">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Payment Information
</h3>
<p class="text-gray-600 dark:text-gray-400">
You're signing up for the {{ selectedTier.label }} plan
</p>
<p class="text-lg font-semibold text-blue-600 dark:text-blue-400 mt-2">
${{ selectedTier.amount }} CAD / month
</p>
</div>
<!-- Payment Instructions -->
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6">
<p class="text-gray-700 dark:text-gray-300">
Click "Complete Payment" below to open the secure payment modal and verify your payment method.
</p>
</div>
<!-- Action Buttons -->
<div class="flex justify-between pt-6">
<UButton
variant="outline"
size="lg"
@click="goBack"
:disabled="isSubmitting"
>
Back
</UButton>
<UButton
size="lg"
:loading="isSubmitting"
@click="processPayment"
>
Complete Payment
</UButton>
</div>
</div>
<!-- Step 3: Confirmation -->
<div v-if="currentStep === 3" class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-xl border border-blue-200 dark:border-blue-800">
<div class="text-center">
<div class="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
<svg class="w-10 h-10 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">
Welcome to Ghost Guild!
</h3>
<div v-if="successMessage" class="mb-6">
<UAlert color="green" :title="successMessage" />
</div>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6 mb-6 text-left">
<h4 class="font-semibold mb-3">Membership Details:</h4>
<dl class="space-y-2">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Name:</dt>
<dd class="font-medium">{{ form.name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Email:</dt>
<dd class="font-medium">{{ form.email }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Circle:</dt>
<dd class="font-medium capitalize">{{ form.circle }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Contribution:</dt>
<dd class="font-medium">{{ selectedTier.label }}</dd>
</div>
<div v-if="customerCode" class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Member ID:</dt>
<dd class="font-medium">{{ customerCode }}</dd>
</div>
</dl>
</div>
<p class="text-gray-600 dark:text-gray-400 mb-4">
We've sent a confirmation email to {{ form.email }} with your membership details.
</p>
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 mb-8">
<p class="text-blue-800 dark:text-blue-200 text-center">
You will be automatically redirected to your dashboard in a few seconds...
</p>
</div>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton
to="/member/dashboard"
size="lg"
class="px-8"
>
Go to Dashboard Now
</UButton>
<UButton
variant="outline"
size="lg"
@click="resetForm"
>
Register Another Member
</UButton>
</div>
</div>
</div>
</UContainer>
</section>
<!-- Membership Benefits -->
<section class="py-20 bg-gray-50 dark:bg-gray-800">
<UContainer>
<div class="text-center mb-12">
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-4">
Membership Benefits
</h2>
<p class="text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Join our community and unlock these amazing benefits.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="bg-white dark:bg-gray-900 rounded-xl p-8 shadow-lg">
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center mb-6">
<div class="w-6 h-6 bg-blue-500 rounded" />
</div>
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
Community Access
</h3>
<div class="space-y-2 mb-4">
<div class="h-1 bg-blue-500 rounded-full w-full" />
<div class="h-1 bg-blue-300 rounded-full w-3/4" />
<div class="h-1 bg-blue-200 rounded-full w-1/2" />
</div>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Access to forums and resources.
</p>
</div>
<div class="bg-white dark:bg-gray-900 rounded-xl p-8 shadow-lg">
<div class="w-12 h-12 bg-emerald-100 dark:bg-emerald-900/30 rounded-lg flex items-center justify-center mb-6">
<div class="w-6 h-6 bg-emerald-500" style="clip-path: polygon(50% 0%, 0% 100%, 100% 100%)" />
</div>
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
Learning Resources
</h3>
<div class="space-y-2 mb-4">
<div class="h-1 bg-emerald-500 rounded-full w-full" />
<div class="h-1 bg-emerald-300 rounded-full w-2/3" />
<div class="h-1 bg-emerald-200 rounded-full w-3/4" />
</div>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Educational content and workshops.
</p>
</div>
<div class="bg-white dark:bg-gray-900 rounded-xl p-8 shadow-lg md:col-span-2 lg:col-span-1">
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center mb-6">
<div class="w-6 h-6 bg-purple-500 rounded-full" />
</div>
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
Network & Support
</h3>
<div class="space-y-2 mb-4">
<div class="h-1 bg-purple-500 rounded-full w-5/6" />
<div class="h-1 bg-purple-300 rounded-full w-full" />
<div class="h-1 bg-purple-200 rounded-full w-2/3" />
</div>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Connect with like-minded professionals.
</p>
</div>
</div>
</UContainer>
</section>
<!-- How to Join -->
<section class="py-20 bg-white dark:bg-gray-900">
<UContainer>
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-4">
How to Join
</h2>
<p class="text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Follow these simple steps to become a member.
</p>
</div>
<div class="max-w-4xl mx-auto">
<div class="space-y-12">
<div class="flex flex-col md:flex-row items-center gap-8">
<div class="flex-shrink-0">
<div class="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold text-xl">
1
</div>
</div>
<div class="flex-1 text-center md:text-left">
<div class="space-y-3 mb-4">
<div class="h-2 bg-blue-500 rounded-full max-w-md mx-auto md:mx-0" />
<div class="h-2 bg-blue-300 rounded-full max-w-sm mx-auto md:mx-0" />
<div class="h-2 bg-blue-200 rounded-full max-w-xs mx-auto md:mx-0" />
</div>
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">
Choose Your Circle
</h3>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Select the circle that matches your interests.
</p>
</div>
</div>
<div class="flex flex-col md:flex-row items-center gap-8">
<div class="flex-shrink-0">
<div class="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold text-xl">
2
</div>
</div>
<div class="flex-1 text-center md:text-left">
<div class="space-y-3 mb-4">
<div class="h-2 bg-blue-500 rounded-full max-w-lg mx-auto md:mx-0" />
<div class="h-2 bg-blue-300 rounded-full max-w-md mx-auto md:mx-0" />
<div class="h-2 bg-blue-200 rounded-full max-w-sm mx-auto md:mx-0" />
</div>
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">
Set Your Contribution
</h3>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Choose a contribution level based on your means.
</p>
</div>
</div>
<div class="flex flex-col md:flex-row items-center gap-8">
<div class="flex-shrink-0">
<div class="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold text-xl">
3
</div>
</div>
<div class="flex-1 text-center md:text-left">
<div class="space-y-3 mb-4">
<div class="h-2 bg-blue-500 rounded-full max-w-sm mx-auto md:mx-0" />
<div class="h-2 bg-blue-300 rounded-full max-w-lg mx-auto md:mx-0" />
<div class="h-2 bg-blue-200 rounded-full max-w-md mx-auto md:mx-0" />
</div>
<h3 class="text-xl font-semibold mb-2 text-gray-900 dark:text-white">
Complete Registration
</h3>
<p class="text-gray-600 dark:text-gray-400">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Finalize your membership and start participating.
</p>
</div>
</div>
</div>
</div>
</UContainer>
</section>
<!-- Ready to Join CTA -->
<section class="py-20 bg-blue-50 dark:bg-blue-900/20">
<UContainer>
<div class="text-center max-w-3xl mx-auto">
<h2 class="text-3xl font-bold text-blue-600 dark:text-blue-400 mb-8">
Ready to Join?
</h2>
<div class="flex flex-col md:flex-row items-center justify-center gap-8 mb-8">
<div class="bg-white dark:bg-gray-800 rounded-2xl p-8 shadow-lg border border-blue-200 dark:border-blue-800 flex-1 max-w-md">
<div class="text-2xl font-bold text-blue-600 dark:text-blue-400 mb-2">
Start Today
</div>
<p class="text-gray-600 dark:text-gray-300 mb-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<div class="space-y-2">
<div class="h-1 bg-blue-500 rounded-full" />
<div class="h-1 bg-blue-300 rounded-full w-3/4" />
<div class="h-1 bg-blue-200 rounded-full w-1/2" />
</div>
</div>
<div class="flex flex-col items-center gap-4">
<UButton
to="#membership-form"
size="xl"
color="primary"
class="px-8 py-4"
>
Join Now
</UButton>
<div class="h-2 bg-blue-500 rounded-full w-32" />
</div>
</div>
<p class="text-gray-600 dark:text-gray-300">
Questions? Contact us at <a href="mailto:hello@ghostguild.org" class="text-blue-600 dark:text-blue-400 hover:underline">hello@ghostguild.org</a>
</p>
</div>
</UContainer>
</section>
</div>
</template>
<script setup>
import { reactive, ref, computed, onMounted, onUnmounted } from 'vue'
import { getCircleOptions } from '~/config/circles'
import { getContributionOptions, requiresPayment, getContributionTierByValue } from '~/config/contributions'
// Form state
const form = reactive({
email: '',
name: '',
circle: 'community',
contributionTier: '15',
billingAddress: {
street: '',
city: '',
province: '',
postalCode: '',
country: 'CA'
}
})
// UI state
const isSubmitting = ref(false)
const currentStep = ref(1) // 1: Info, 2: Billing (paid only), 3: Payment, 4: Confirmation
const errorMessage = ref('')
const successMessage = ref('')
// Helcim state
const customerId = ref(null)
const customerCode = ref(null)
const subscriptionData = ref(null)
const paymentToken = ref(null)
// Circle options from central config
const circleOptions = getCircleOptions()
// Contribution options from central config
const contributionOptions = getContributionOptions()
// Initialize composables
const { initializeHelcimPay, verifyPayment, cleanup: cleanupHelcimPay } = useHelcimPay()
const { checkMemberStatus } = useAuth()
// Form validation
const isFormValid = computed(() => {
return form.name && form.email && form.circle && form.contributionTier
})
// Check if payment is required
const needsPayment = computed(() => {
return requiresPayment(form.contributionTier)
})
// Get selected tier info
const selectedTier = computed(() => {
return getContributionTierByValue(form.contributionTier)
})
// Step 1: Create customer
const handleSubmit = async () => {
if (isSubmitting.value || !isFormValid.value) return
isSubmitting.value = true
errorMessage.value = ''
try {
// Create customer in Helcim
const response = await $fetch('/api/helcim/customer', {
method: 'POST',
body: {
name: form.name,
email: form.email,
circle: form.circle,
contributionTier: form.contributionTier,
billingAddress: form.billingAddress
}
})
if (response.success) {
console.log('Customer response:', response)
customerId.value = response.customerId
customerCode.value = response.customerCode
// Token is now set as httpOnly cookie by the server
// No need to manually set cookie on client side
// Move to next step
if (needsPayment.value) {
currentStep.value = 2
// Debug log
console.log('Customer ID:', customerId.value, 'Customer Code:', customerCode.value)
// Initialize HelcimPay.js session for card verification
await initializeHelcimPay(customerId.value, customerCode.value, 0)
} else {
// For free tier, create subscription directly
await createSubscription()
// Check member status to ensure user is properly authenticated
await checkMemberStatus()
// Automatically redirect to dashboard after a short delay
setTimeout(() => {
navigateTo('/member/dashboard')
}, 3000) // 3 second delay to show success message
}
}
} catch (error) {
console.error('Error creating customer:', error)
errorMessage.value = error.data?.message || 'Failed to create account. Please try again.'
} finally {
isSubmitting.value = false
}
}
// Step 2: Process payment
const processPayment = async () => {
if (isSubmitting.value) return
console.log('Starting payment process...')
isSubmitting.value = true
errorMessage.value = ''
try {
console.log('Calling verifyPayment()...')
// Verify payment through HelcimPay.js
const paymentResult = await verifyPayment()
console.log('Payment result from HelcimPay:', paymentResult)
if (paymentResult.success) {
paymentToken.value = paymentResult.cardToken
console.log('Payment successful, cardToken:', paymentResult.cardToken)
console.log('Calling verify-payment endpoint...')
// Verify payment on server
const verifyResult = await $fetch('/api/helcim/verify-payment', {
method: 'POST',
body: {
cardToken: paymentResult.cardToken,
customerId: customerId.value
}
})
console.log('Payment verification result:', verifyResult)
console.log('Calling createSubscription...')
// Create subscription (don't let subscription errors prevent form progression)
const subscriptionResult = await createSubscription(paymentResult.cardToken)
if (!subscriptionResult || !subscriptionResult.success) {
console.warn('Subscription creation failed but payment succeeded:', subscriptionResult?.error)
// Still progress to success page since payment worked
currentStep.value = 3
successMessage.value = 'Payment successful! Subscription setup may need manual completion.'
}
}
} catch (error) {
console.error('Payment process error:', error)
console.error('Error details:', {
message: error.message,
statusCode: error.statusCode,
statusMessage: error.statusMessage,
data: error.data
})
errorMessage.value = error.message || 'Payment verification failed. Please try again.'
} finally {
isSubmitting.value = false
}
}
// Create subscription
const createSubscription = async (cardToken = null) => {
try {
console.log('Creating subscription with:', {
customerId: customerId.value,
contributionTier: form.contributionTier,
cardToken: cardToken ? 'present' : 'null'
})
const response = await $fetch('/api/helcim/subscription', {
method: 'POST',
body: {
customerId: customerId.value,
customerCode: customerCode.value,
contributionTier: form.contributionTier,
cardToken: cardToken
}
})
console.log('Subscription creation response:', response)
if (response.success) {
subscriptionData.value = response.subscription
console.log('Moving to step 3 - success!')
currentStep.value = 3
successMessage.value = 'Your membership has been activated successfully!'
// Check member status to ensure user is properly authenticated
await checkMemberStatus()
// Automatically redirect to dashboard after a short delay
setTimeout(() => {
navigateTo('/member/dashboard')
}, 3000) // 3 second delay to show success message
} else {
throw new Error('Subscription creation failed - response not successful')
}
} catch (error) {
console.error('Subscription creation error:', error)
console.error('Error details:', {
message: error.message,
statusCode: error.statusCode,
statusMessage: error.statusMessage,
data: error.data
})
console.error('Subscription creation completely failed, but payment was successful')
// Don't throw error - let the calling function handle progression
return {
success: false,
error: error.data?.message || error.message || 'Failed to create subscription'
}
}
}
// Go back to previous step
const goBack = () => {
if (currentStep.value > 1) {
currentStep.value--
errorMessage.value = ''
}
}
// Reset form
const resetForm = () => {
currentStep.value = 1
customerId.value = null
customerCode.value = null
subscriptionData.value = null
paymentToken.value = null
errorMessage.value = ''
successMessage.value = ''
form.email = ''
form.name = ''
form.circle = 'community'
form.contributionTier = '15'
}
// Cleanup on unmount
onUnmounted(() => {
cleanupHelcimPay()
})
</script>