Initial commit - Black Canyon Tickets whitelabel platform
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
284
src/components/AgeVerification.astro
Normal file
284
src/components/AgeVerification.astro
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
// 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 }}>
|
||||
class AgeVerification {
|
||||
private modal: HTMLElement;
|
||||
private dateInput: HTMLInputElement;
|
||||
private confirmButton: HTMLButtonElement;
|
||||
private errorDiv: HTMLElement;
|
||||
private errorText: HTMLElement;
|
||||
private coppaNotice: HTMLElement;
|
||||
private isVerified: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.modal = document.getElementById('age-verification-modal')!;
|
||||
this.dateInput = document.getElementById('date-of-birth') as HTMLInputElement;
|
||||
this.confirmButton = document.getElementById('age-verification-confirm') as HTMLButtonElement;
|
||||
this.errorDiv = document.getElementById('age-verification-error')!;
|
||||
this.errorText = document.getElementById('age-verification-error-text')!;
|
||||
this.coppaNotice = document.getElementById('coppa-notice')!;
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
|
||||
private 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 window[onVerified] === 'function') {
|
||||
window[onVerified]();
|
||||
}
|
||||
|
||||
// Hide modal
|
||||
this.hide();
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('ageVerified', {
|
||||
detail: { verified: true, minimumAge }
|
||||
}));
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
this.errorText.textContent = message;
|
||||
this.errorDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private hideError() {
|
||||
this.errorDiv.classList.add('hidden');
|
||||
}
|
||||
|
||||
private showCoppaNotice() {
|
||||
this.coppaNotice.classList.remove('hidden');
|
||||
}
|
||||
|
||||
private hideCoppaNotice() {
|
||||
this.coppaNotice.classList.add('hidden');
|
||||
}
|
||||
|
||||
public 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 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);
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
public isAgeVerified(): boolean {
|
||||
return this.isVerified;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and expose globally
|
||||
const ageVerification = new AgeVerification();
|
||||
(window as any).ageVerification = ageVerification;
|
||||
(window as any).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>
|
||||
Reference in New Issue
Block a user