Major additions: - Territory manager system with application workflow - Custom pricing and page builder with Craft.js - Enhanced Stripe Connect onboarding - CodeReadr QR scanning integration - Kiosk mode for venue sales - Super admin dashboard and analytics - MCP integration for AI-powered operations Infrastructure improvements: - Centralized API client and routing system - Enhanced authentication with organization context - Comprehensive theme management system - Advanced event management with custom tabs - Performance monitoring and accessibility features Database schema updates: - Territory management tables - Custom pages and pricing structures - Kiosk PIN system - Enhanced organization profiles - CodeReadr integration tables 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
288 lines
9.3 KiB
Plaintext
288 lines
9.3 KiB
Plaintext
---
|
|
// Age verification component for ticket purchases
|
|
export interface Props {
|
|
minimumAge?: number;
|
|
eventTitle?: string;
|
|
onVerified?: string; // Callback function name
|
|
}
|
|
|
|
const { minimumAge = 18, eventTitle = "this event", onVerified = "onAgeVerified" } = Astro.props;
|
|
---
|
|
|
|
<div
|
|
id="age-verification-modal"
|
|
class="fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center p-4"
|
|
style="display: none;"
|
|
>
|
|
<div class="bg-white rounded-2xl shadow-2xl max-w-md w-full">
|
|
<!-- Header -->
|
|
<div class="text-center p-6 border-b border-gray-200">
|
|
<div class="w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<svg class="w-8 h-8 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-gray-900 mb-2">Age Verification Required</h2>
|
|
<p class="text-gray-600">
|
|
You must be at least <strong>{minimumAge} years old</strong> to purchase tickets for {eventTitle}.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="p-6">
|
|
<div class="space-y-4">
|
|
<!-- Date of birth input -->
|
|
<div>
|
|
<label for="date-of-birth" class="block text-sm font-medium text-gray-700 mb-2">
|
|
Date of Birth
|
|
</label>
|
|
<input
|
|
type="date"
|
|
id="date-of-birth"
|
|
name="dateOfBirth"
|
|
max={new Date().toISOString().split('T')[0]}
|
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
|
|
required
|
|
/>
|
|
<p class="mt-1 text-xs text-gray-500">
|
|
We use this information solely for age verification and do not store it.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Error message -->
|
|
<div id="age-verification-error" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span id="age-verification-error-text" class="text-sm text-red-700"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- COPPA notice for under 13 -->
|
|
<div id="coppa-notice" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
|
|
<div class="flex items-start">
|
|
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div class="text-sm text-blue-700">
|
|
<p class="font-medium">Parental Consent Required</p>
|
|
<p>Users under 13 require verifiable parental consent. Please contact our support team for assistance.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="flex flex-col sm:flex-row gap-3 p-6 border-t border-gray-200">
|
|
<button
|
|
id="age-verification-cancel"
|
|
class="flex-1 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-lg transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
id="age-verification-confirm"
|
|
class="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
disabled
|
|
>
|
|
Verify & Continue
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Privacy notice -->
|
|
<div class="px-6 pb-6">
|
|
<p class="text-xs text-gray-500 text-center">
|
|
By proceeding, you confirm that the information provided is accurate.
|
|
This information is used solely for age verification and is not stored or shared.
|
|
<a href="/privacy" target="_blank" class="text-blue-600 hover:text-blue-500 underline">
|
|
Privacy Policy
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script define:vars={{ minimumAge, onVerified }}>
|
|
// Variables passed from define:vars
|
|
// minimumAge: number
|
|
// onVerified: string | undefined
|
|
|
|
class AgeVerification {
|
|
modal;
|
|
dateInput;
|
|
confirmButton;
|
|
errorDiv;
|
|
errorText;
|
|
coppaNotice;
|
|
isVerified = false;
|
|
|
|
constructor() {
|
|
this.modal = document.getElementById('age-verification-modal');
|
|
this.dateInput = document.getElementById('date-of-birth');
|
|
this.confirmButton = document.getElementById('age-verification-confirm');
|
|
this.errorDiv = document.getElementById('age-verification-error');
|
|
this.errorText = document.getElementById('age-verification-error-text');
|
|
this.coppaNotice = document.getElementById('coppa-notice');
|
|
|
|
this.bindEvents();
|
|
}
|
|
|
|
bindEvents() {
|
|
// Date input change
|
|
this.dateInput.addEventListener('change', () => {
|
|
this.validateAge();
|
|
});
|
|
|
|
// Confirm button
|
|
this.confirmButton.addEventListener('click', () => {
|
|
this.confirmAge();
|
|
});
|
|
|
|
// Cancel button
|
|
document.getElementById('age-verification-cancel')?.addEventListener('click', () => {
|
|
this.hide();
|
|
});
|
|
|
|
// Modal backdrop click
|
|
this.modal.addEventListener('click', (e) => {
|
|
if (e.target === this.modal) {
|
|
this.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
validateAge() {
|
|
this.hideError();
|
|
this.hideCoppaNotice();
|
|
|
|
const birthDate = new Date(this.dateInput.value);
|
|
const today = new Date();
|
|
|
|
if (!this.dateInput.value) {
|
|
this.confirmButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// Calculate age
|
|
let age = today.getFullYear() - birthDate.getFullYear();
|
|
const monthDiff = today.getMonth() - birthDate.getMonth();
|
|
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
|
age--;
|
|
}
|
|
|
|
// Check if under 13 (COPPA)
|
|
if (age < 13) {
|
|
this.showCoppaNotice();
|
|
this.confirmButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// Check minimum age requirement
|
|
if (age < minimumAge) {
|
|
this.showError(`You must be at least ${minimumAge} years old to purchase tickets for this event.`);
|
|
this.confirmButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// Valid age
|
|
this.confirmButton.disabled = false;
|
|
}
|
|
|
|
confirmAge() {
|
|
if (this.confirmButton.disabled) return;
|
|
|
|
// Mark as verified
|
|
this.isVerified = true;
|
|
|
|
// Store verification (session only, not persistent)
|
|
sessionStorage.setItem('age_verified', 'true');
|
|
sessionStorage.setItem('age_verified_timestamp', Date.now().toString());
|
|
|
|
// Call the callback function if provided
|
|
if (typeof onVerified === 'string' && typeof window[onVerified] === 'function') {
|
|
window[onVerified]();
|
|
}
|
|
|
|
// Hide modal
|
|
this.hide();
|
|
|
|
// Dispatch custom event
|
|
window.dispatchEvent(new CustomEvent('ageVerified', {
|
|
detail: { verified: true, minimumAge }
|
|
}));
|
|
}
|
|
|
|
showError(message) {
|
|
this.errorText.textContent = message;
|
|
this.errorDiv.classList.remove('hidden');
|
|
}
|
|
|
|
hideError() {
|
|
this.errorDiv.classList.add('hidden');
|
|
}
|
|
|
|
showCoppaNotice() {
|
|
this.coppaNotice.classList.remove('hidden');
|
|
}
|
|
|
|
hideCoppaNotice() {
|
|
this.coppaNotice.classList.add('hidden');
|
|
}
|
|
|
|
show() {
|
|
// Check if already verified in this session
|
|
const verified = sessionStorage.getItem('age_verified');
|
|
const timestamp = sessionStorage.getItem('age_verified_timestamp');
|
|
|
|
if (verified === 'true' && timestamp) {
|
|
// Check if verification is still valid (within 1 hour)
|
|
const verificationAge = Date.now() - parseInt(timestamp);
|
|
if (verificationAge < 60 * 60 * 1000) { // 1 hour
|
|
this.isVerified = true;
|
|
if (typeof onVerified === 'string' && typeof window[onVerified] === 'function') {
|
|
window[onVerified]();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.modal.style.display = 'flex';
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
// Focus on date input
|
|
setTimeout(() => {
|
|
this.dateInput.focus();
|
|
}, 100);
|
|
}
|
|
|
|
hide() {
|
|
this.modal.style.display = 'none';
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
isAgeVerified() {
|
|
return this.isVerified;
|
|
}
|
|
}
|
|
|
|
// Initialize and expose globally
|
|
const ageVerification = new AgeVerification();
|
|
window.ageVerification = ageVerification;
|
|
window.showAgeVerification = () => ageVerification.show();
|
|
</script>
|
|
|
|
<style>
|
|
/* Custom styles for date input */
|
|
input[type="date"]::-webkit-calendar-picker-indicator {
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
input[type="date"]::-webkit-calendar-picker-indicator:hover {
|
|
background-color: #f3f4f6;
|
|
}
|
|
</style> |