feat: add advanced analytics and territory management system

- Add comprehensive analytics components with export functionality
- Implement territory management with manager performance tracking
- Add seatmap components for venue layout management
- Create customer management features with modal interface
- Add advanced hooks for dashboard flags and territory data
- Implement seat selection and venue management utilities
- Add type definitions for ticketing and seatmap systems

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-26 09:25:10 -06:00
parent d5c3953888
commit aa81eb5adb
438 changed files with 90509 additions and 2787 deletions

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* Check for hardcoded colors in the codebase
* This script scans for hex colors, rgb(), rgba(), and other hardcoded color patterns
*/
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
const colorPatterns = [
// Hex colors
'#[0-9a-fA-F]{3,8}',
// RGB/RGBA functions
'rgb\\s*\\(',
'rgba\\s*\\(',
// HSL/HSLA functions
'hsl\\s*\\(',
'hsla\\s*\\(',
// Hardcoded Tailwind classes
'bg-white',
'bg-black',
'text-white',
'text-black'
];
const excludePaths = [
'node_modules',
'dist',
'build',
'.git',
'scripts',
'tailwind.config.js',
'tokens.ts',
'tokens.css'
];
async function checkHardcodedColors() {
console.log('🎨 Checking for hardcoded colors in codebase...\n');
let hasViolations = false;
for (const pattern of colorPatterns) {
const excludeArgs = excludePaths.map(path => `--exclude-dir=${path}`).join(' ');
const command = `grep -r -n --color=never ${excludeArgs} "${pattern}" src/ || true`;
try {
const { stdout } = await execAsync(command);
if (stdout.trim()) {
console.log(`❌ Found hardcoded color pattern: ${pattern}`);
console.log(stdout);
hasViolations = true;
}
} catch (error) {
// grep returns non-zero exit code when no matches found, which is expected
if (error.code !== 1) {
console.error(`Error checking pattern ${pattern}:`, error);
}
}
}
if (hasViolations) {
console.log('\n❌ Hardcoded colors found! Please use design tokens instead.');
console.log('📖 See src/theme/tokens.ts for available tokens.');
process.exit(1);
} else {
console.log('✅ No hardcoded colors found! All colors are using design tokens.');
}
}
checkHardcodedColors().catch(console.error);

View File

@@ -0,0 +1,100 @@
#!/bin/bash
# Stripe Connect Functions Deployment Script
# This script helps deploy Firebase Functions with proper environment setup
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}🚀 Stripe Connect Functions Deployment${NC}"
echo "=========================================="
# Check if Firebase CLI is installed
if ! command -v firebase &> /dev/null; then
echo -e "${RED}❌ Firebase CLI not found. Install with: npm install -g firebase-tools${NC}"
exit 1
fi
# Check if logged in to Firebase
if ! firebase projects:list &> /dev/null; then
echo -e "${YELLOW}⚠️ Not logged in to Firebase. Logging in...${NC}"
firebase login
fi
# Get current project
PROJECT=$(firebase use 2>/dev/null | grep "active project" | awk '{print $4}' | tr -d '()')
if [ -z "$PROJECT" ]; then
echo -e "${RED}❌ No Firebase project selected. Run 'firebase use <project-id>' first.${NC}"
exit 1
fi
echo -e "${GREEN}📁 Project: ${PROJECT}${NC}"
# Check Functions directory
if [ ! -d "functions" ]; then
echo -e "${RED}❌ Functions directory not found. Make sure you're in the project root.${NC}"
exit 1
fi
# Install Functions dependencies
echo -e "${BLUE}📦 Installing Functions dependencies...${NC}"
cd functions
npm install
# Check if environment config exists
echo -e "${BLUE}🔧 Checking environment configuration...${NC}"
CONFIG=$(firebase functions:config:get 2>/dev/null || echo "{}")
if [ "$CONFIG" = "{}" ]; then
echo -e "${YELLOW}⚠️ No environment config found. You'll need to set:${NC}"
echo " firebase functions:config:set stripe.secret_key=\"sk_...\""
echo " firebase functions:config:set stripe.webhook_secret=\"whsec_...\""
echo " firebase functions:config:set app.url=\"https://your-domain.com\""
echo ""
read -p "Continue anyway? (y/n): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
else
echo -e "${GREEN}✅ Environment config found${NC}"
fi
# Build Functions
echo -e "${BLUE}🔨 Building Functions...${NC}"
npm run build
# Deploy Functions
echo -e "${BLUE}🚀 Deploying Functions...${NC}"
cd ..
firebase deploy --only functions
if [ $? -eq 0 ]; then
echo ""
echo -e "${GREEN}✅ Functions deployed successfully!${NC}"
echo ""
echo "📋 Next steps:"
echo "1. Configure Stripe webhook endpoint:"
echo " URL: https://us-central1-${PROJECT}.cloudfunctions.net/stripeWebhook"
echo " Events: account.updated"
echo ""
echo "2. Test the integration:"
echo " - Visit your app and try connecting a Stripe account"
echo " - Check function logs: firebase functions:log"
echo ""
echo "3. Update frontend API URL if needed:"
echo " - In useStripeConnect.ts, update getApiUrl() function"
echo ""
else
echo -e "${RED}❌ Deployment failed. Check the logs above.${NC}"
exit 1
fi

View File

@@ -0,0 +1,312 @@
/**
* QR System Demonstration Script
* Shows QR validation, backup code generation, and manual entry flow
*/
// Import the QR validator and generator (simplified for demo)
function createMockQRValidator() {
return {
validateQR: (qrString) => {
if (!qrString || typeof qrString !== 'string') {
return { valid: false, format: 'unknown', errorReason: 'invalid_format' };
}
const trimmed = qrString.trim();
if (trimmed.startsWith('BCT.v')) {
const parts = trimmed.split('.');
if (parts.length === 4) {
return {
valid: true,
format: 'signed',
ticketId: '550e8400-e29b-41d4-a716-446655440000',
eventId: 'evt_789012345',
metadata: {
version: 2,
issuedAt: Math.floor(Date.now() / 1000),
expiresAt: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
zone: 'GA',
seat: 'A12'
}
};
}
}
if (trimmed.startsWith('TICKET_')) {
const ticketId = trimmed.substring(7);
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(ticketId)) {
return { valid: true, format: 'simple', ticketId };
}
}
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
return { valid: true, format: 'simple', ticketId: trimmed };
}
return { valid: false, format: 'unknown', errorReason: 'invalid_format' };
},
validateBackupCode: (code) => {
if (!code || typeof code !== 'string') {
return { valid: false };
}
const normalized = code.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
if (normalized.length !== 8) {
return { valid: false };
}
if (!/^[0-9A-F]{8}$/.test(normalized)) {
return { valid: false };
}
return { valid: true, normalizedCode: normalized };
},
extractTicketId: (qrString) => {
const result = this.validateQR(qrString);
return result.valid ? result.ticketId : null;
},
generateBackupCode: (ticketId) => {
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(ticketId)) {
throw new Error('Invalid ticket ID format');
}
const cleanId = ticketId.replace(/-/g, '');
return cleanId.slice(-8).toUpperCase();
}
};
}
function getQRErrorMessage(result) {
if (result.valid) {
return 'Valid ticket';
}
switch (result.errorReason) {
case 'invalid_format': return 'Invalid QR code format';
case 'expired': return 'Ticket has expired';
case 'signature_invalid': return 'Invalid or tampered ticket';
case 'malformed': return 'Corrupted QR code data';
case 'missing_data': return 'Incomplete ticket information';
default: return 'Unknown error occurred';
}
}
function formatBackupCode(code) {
if (code.length !== 8) {
return code;
}
return `${code.substring(0, 4)}-${code.substring(4)}`;
}
console.log('🎫 Black Canyon Tickets - QR System Demonstration\\n');
// Sample ticket data
const sampleTicketData = {
ticketId: '550e8400-e29b-41d4-a716-446655440000',
eventId: 'evt_789012345',
zone: 'GA',
seat: 'A12',
expiresInDays: 30
};
console.log('1. QR Code Validation');
console.log('====================');
const validator = createMockQRValidator();
// Test different QR formats
const testQRs = [
// Simple format
`TICKET_${sampleTicketData.ticketId}`,
// Signed token format (simulated)
'BCT.v2.eyJraWQiOiIxMjMiLCJlaWQiOiI0NTYifQ.abc123signature',
// Legacy UUID only
sampleTicketData.ticketId,
// Invalid formats
'INVALID_QR_CODE',
'BCT.v2.invalid.signature',
'TICKET_not-a-uuid'
];
testQRs.forEach((qr, index) => {
console.log(`Test ${index + 1}: ${qr.substring(0, 40)}${qr.length > 40 ? '...' : ''}`);
const result = validator.validateQR(qr);
console.log(` ✅ Valid: ${result.valid}`);
console.log(` 📱 Format: ${result.format}`);
if (result.ticketId) {
console.log(` 🎫 Ticket ID: ${result.ticketId}`);
}
if (result.eventId) {
console.log(` 🎪 Event ID: ${result.eventId}`);
}
if (!result.valid) {
console.log(` ❌ Error: ${getQRErrorMessage(result)}`);
}
if (result.metadata) {
if (result.metadata.expiresAt) {
const expires = new Date(result.metadata.expiresAt * 1000);
console.log(` ⏰ Expires: ${expires.toLocaleString()}`);
}
if (result.metadata.zone) {
console.log(` 🏟️ Zone: ${result.metadata.zone}`);
}
if (result.metadata.seat) {
console.log(` 💺 Seat: ${result.metadata.seat}`);
}
}
console.log();
});
console.log('2. Backup Code Validation');
console.log('=========================');
const testBackupCodes = [
'55440000', // Valid (last 8 chars of ticket ID)
'1234ABCD', // Valid hex format
'12345', // Too short
'123456789', // Too long
'1234GHIJ', // Invalid characters
'' // Empty
];
testBackupCodes.forEach((code, index) => {
console.log(`Test ${index + 1}: "${code}"`);
const result = validator.validateBackupCode(code);
console.log(` ✅ Valid: ${result.valid}`);
if (result.valid && result.normalizedCode) {
console.log(` 🔢 Normalized: ${formatBackupCode(result.normalizedCode)}`);
}
console.log();
});
console.log('3. Backup Code Generation');
console.log('========================');
const testTicketIds = [
sampleTicketData.ticketId,
'123e4567-e89b-12d3-a456-426614174000',
'invalid-uuid'
];
testTicketIds.forEach((ticketId, index) => {
console.log(`Test ${index + 1}: ${ticketId}`);
try {
const backupCode = validator.generateBackupCode(ticketId);
console.log(` 🔢 Backup Code: ${formatBackupCode(backupCode)}`);
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
}
console.log();
});
console.log('4. QR Format Detection');
console.log('======================');
const mixedQRs = [
'TICKET_550e8400-e29b-41d4-a716-446655440000',
'BCT.v2.eyJ0aWQiOiIxMjMiLCJlaWQiOiI0NTYifQ.signature',
'550e8400-e29b-41d4-a716-446655440000', // Legacy UUID only
'MANUAL_55440000'
];
mixedQRs.forEach((qr, index) => {
console.log(`QR ${index + 1}: ${qr}`);
const ticketId = validator.extractTicketId(qr);
if (ticketId) {
console.log(` 🎫 Extracted Ticket ID: ${ticketId}`);
try {
const backupCode = validator.generateBackupCode(ticketId);
console.log(` 🔢 Backup Code: ${formatBackupCode(backupCode)}`);
} catch (error) {
console.log(` ❌ Backup Code Error: ${error.message}`);
}
} else {
console.log(` ❌ Could not extract ticket ID`);
}
console.log();
});
console.log('5. Manual Entry Flow Simulation');
console.log('===============================');
console.log('Simulating gate staff manual entry scenarios:');
console.log();
// Scenario 1: Perfect entry
console.log('Scenario 1: Gate staff enters backup code correctly');
const perfectCode = '55440000';
console.log(`Staff enters: "${perfectCode}"`);
const result1 = validator.validateBackupCode(perfectCode);
if (result1.valid) {
console.log(`✅ Code accepted: ${formatBackupCode(result1.normalizedCode)}`);
console.log(`🚪 Entry granted`);
} else {
console.log(`❌ Code rejected`);
}
console.log();
// Scenario 2: Entry with hyphens
console.log('Scenario 2: Gate staff enters code with formatting');
const formattedCode = '5544-0000';
console.log(`Staff enters: "${formattedCode}"`);
const result2 = validator.validateBackupCode(formattedCode);
if (result2.valid) {
console.log(`✅ Code accepted: ${formatBackupCode(result2.normalizedCode)}`);
console.log(`🚪 Entry granted`);
} else {
console.log(`❌ Code rejected`);
}
console.log();
// Scenario 3: Invalid entry
console.log('Scenario 3: Gate staff makes typing error');
const errorCode = '5544000'; // Missing digit
console.log(`Staff enters: "${errorCode}"`);
const result3 = validator.validateBackupCode(errorCode);
if (result3.valid) {
console.log(`✅ Code accepted`);
} else {
console.log(`❌ Code rejected - ask customer to show physical ticket`);
console.log(`💡 Suggestion: Check last 8 characters on ticket bottom`);
}
console.log();
console.log('6. Security Features');
console.log('===================');
console.log('✅ Signed tokens prevent counterfeiting');
console.log('✅ UUID ticket IDs prevent enumeration attacks');
console.log('✅ Backup codes are last 8 characters (not sequential)');
console.log('✅ HMAC signatures detect tampering');
console.log('✅ Time-based expiration prevents replay attacks');
console.log('✅ Offline validation available for signed tokens');
console.log();
console.log('7. Production Recommendations');
console.log('=============================');
console.log('✅ Use signed tokens (BCT.v2.{payload}.{signature}) for security');
console.log('✅ Implement proper HMAC-SHA256 signatures in production');
console.log('✅ Rotate signing keys quarterly');
console.log('✅ Use Error Correction Level M (15%) for most use cases');
console.log('✅ Use Error Correction Level H (30%) for thermal printers');
console.log('✅ Include backup codes on all printed tickets');
console.log('✅ Train gate staff on manual entry procedures');
console.log('✅ Test QR codes across different devices and lighting conditions');
console.log('✅ Monitor QR scan success rates and manual entry frequency');
console.log();
console.log('🚀 QR System Demo Complete!');
console.log('Visit /scanner?eventId=test-event-123 to test the scanner interface');
console.log(`Development server: http://localhost:5174`);
console.log();
console.log('📱 Manual Entry Instructions for Gate Staff:');
console.log('============================================');
console.log('1. If QR code won\'t scan, click the # button');
console.log('2. Enter the last 8 characters from bottom of ticket');
console.log('3. Characters can be numbers 0-9 or letters A-F');
console.log('4. System will show XXXX-XXXX format as you type');
console.log('5. Press Submit when all 8 characters entered');
console.log('6. Green checkmark = valid ticket, allow entry');
console.log('7. Red X = invalid code, ask to see physical ticket');
console.log();

View File

@@ -0,0 +1,175 @@
/**
* QR System Demonstration Script
* Shows QR validation, backup code generation, and manual entry flow
*/
import { QRValidator, createQRValidator, getQRErrorMessage, formatBackupCode } from '../src/lib/qr-validator';
import { QRGenerator, createQRGenerator, generateTicketQR, generateQRForFormat, validateQRData } from '../src/lib/qr-generator';
console.log('🎫 Black Canyon Tickets - QR System Demonstration\n');
// Sample ticket data
const sampleTicketData = {
ticketId: '550e8400-e29b-41d4-a716-446655440000',
eventId: 'evt_789012345',
zone: 'GA',
seat: 'A12',
expiresInDays: 30
};
console.log('1. Ticket Data Validation');
console.log('=========================');
const validation = validateQRData(sampleTicketData);
console.log(`✅ Valid: ${validation.valid}`);
if (!validation.valid) {
console.log(`❌ Errors: ${validation.errors.join(', ')}`);
}
console.log();
console.log('2. QR Code Generation');
console.log('====================');
const generator = createQRGenerator();
// Generate QR for different formats
const formats = ['email', 'print', 'thermal', 'wallet'] as const;
for (const format of formats) {
const { qr, svg } = generateQRForFormat(sampleTicketData, format);
console.log(`📱 ${format.toUpperCase()} Format:`);
console.log(` QR Data: ${qr.qrData.substring(0, 50)}...`);
console.log(` Backup Code: ${qr.backupCode}`);
console.log(` Format: ${qr.metadata.format}`);
console.log(` Generated: ${new Date(qr.metadata.generatedAt).toLocaleString()}`);
if (qr.metadata.expiresAt) {
console.log(` Expires: ${new Date(qr.metadata.expiresAt).toLocaleString()}`);
}
console.log();
}
console.log('3. QR Code Validation');
console.log('====================');
const validator = createQRValidator();
// Test different QR formats
const testQRs = [
// Simple format
`TICKET_${sampleTicketData.ticketId}`,
// Signed token format (generated)
generateTicketQR(sampleTicketData).qrData,
// Invalid formats
'INVALID_QR_CODE',
'BCT.v2.invalid.signature',
'TICKET_not-a-uuid'
];
testQRs.forEach((qr, index) => {
console.log(`Test ${index + 1}: ${qr.substring(0, 40)}${qr.length > 40 ? '...' : ''}`);
const result = validator.validateQR(qr);
console.log(` ✅ Valid: ${result.valid}`);
console.log(` 📱 Format: ${result.format}`);
if (result.ticketId) {
console.log(` 🎫 Ticket ID: ${result.ticketId}`);
}
if (result.eventId) {
console.log(` 🎪 Event ID: ${result.eventId}`);
}
if (!result.valid) {
console.log(` ❌ Error: ${getQRErrorMessage(result)}`);
}
if (result.metadata) {
if (result.metadata.expiresAt) {
const expires = new Date(result.metadata.expiresAt * 1000);
console.log(` ⏰ Expires: ${expires.toLocaleString()}`);
}
if (result.metadata.zone) {
console.log(` 🏟️ Zone: ${result.metadata.zone}`);
}
if (result.metadata.seat) {
console.log(` 💺 Seat: ${result.metadata.seat}`);
}
}
console.log();
});
console.log('4. Backup Code Validation');
console.log('=========================');
const testBackupCodes = [
'55440000', // Valid (last 8 chars of ticket ID)
'1234ABCD', // Valid hex format
'12345', // Too short
'123456789', // Too long
'1234GHIJ', // Invalid characters
'' // Empty
];
testBackupCodes.forEach((code, index) => {
console.log(`Test ${index + 1}: "${code}"`);
const result = validator.validateBackupCode(code);
console.log(` ✅ Valid: ${result.valid}`);
if (result.valid && result.normalizedCode) {
console.log(` 🔢 Normalized: ${formatBackupCode(result.normalizedCode)}`);
}
console.log();
});
console.log('5. Backup Code Generation');
console.log('========================');
const testTicketIds = [
sampleTicketData.ticketId,
'123e4567-e89b-12d3-a456-426614174000',
'invalid-uuid'
];
testTicketIds.forEach((ticketId, index) => {
console.log(`Test ${index + 1}: ${ticketId}`);
try {
const backupCode = generator.generateBackupCode(ticketId);
console.log(` 🔢 Backup Code: ${formatBackupCode(backupCode)}`);
} catch (error) {
console.log(` ❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
console.log();
});
console.log('6. QR Format Detection');
console.log('======================');
const mixedQRs = [
'TICKET_550e8400-e29b-41d4-a716-446655440000',
'BCT.v2.eyJ0aWQiOiIxMjMiLCJlaWQiOiI0NTYifQ.signature',
'550e8400-e29b-41d4-a716-446655440000', // Legacy UUID only
'MANUAL_55440000'
];
mixedQRs.forEach((qr, index) => {
console.log(`QR ${index + 1}: ${qr}`);
const ticketId = validator.extractTicketId(qr);
if (ticketId) {
console.log(` 🎫 Extracted Ticket ID: ${ticketId}`);
const backupCode = generator.generateBackupCode(ticketId);
console.log(` 🔢 Backup Code: ${formatBackupCode(backupCode)}`);
} else {
console.log(` ❌ Could not extract ticket ID`);
}
console.log();
});
console.log('7. Production Recommendations');
console.log('=============================');
console.log('✅ Use signed tokens (BCT.v2.{payload}.{signature}) for security');
console.log('✅ Implement proper HMAC-SHA256 signatures in production');
console.log('✅ Rotate signing keys quarterly');
console.log('✅ Use Error Correction Level M (15%) for most use cases');
console.log('✅ Use Error Correction Level H (30%) for thermal printers');
console.log('✅ Include backup codes on all printed tickets');
console.log('✅ Train gate staff on manual entry procedures');
console.log('✅ Test QR codes across different devices and lighting conditions');
console.log('✅ Monitor QR scan success rates and manual entry frequency');
console.log();
console.log('🚀 QR System Demo Complete!');
console.log('Visit /scanner?eventId=test-event-123 to test the scanner interface');
console.log(`Server running at: http://localhost:5174`);
console.log();
export {};

View File

@@ -0,0 +1,82 @@
#!/bin/bash
# Stripe Connect Setup Script for Black Canyon Tickets
# Run this script to configure Firebase Functions for Stripe Connect
set -e
echo "🚀 Setting up Stripe Connect for Black Canyon Tickets"
echo "=================================================="
# Check if Firebase CLI is installed
if ! command -v firebase &> /dev/null; then
echo "❌ Firebase CLI not found. Install with: npm install -g firebase-tools"
exit 1
fi
# Check if logged in to Firebase
if ! firebase projects:list &> /dev/null; then
echo "⚠️ Not logged in to Firebase. Logging in..."
firebase login
fi
# Set project
echo "📁 Setting Firebase project to black-canyon-tickets..."
firebase use black-canyon-tickets
echo ""
echo "🔑 You need to configure these environment variables:"
echo ""
echo "Required Stripe keys:"
echo " - STRIPE_SECRET_KEY (from Stripe Dashboard)"
echo " - STRIPE_WEBHOOK_SECRET (from webhook endpoint setup)"
echo " - APP_URL (your application URL)"
echo ""
read -p "Enter your Stripe Secret Key (sk_test_...): " STRIPE_SECRET_KEY
read -p "Enter your Stripe Webhook Secret (whsec_...): " STRIPE_WEBHOOK_SECRET
read -p "Enter your App URL (e.g., https://portal.blackcanyontickets.com): " APP_URL
if [ -z "$STRIPE_SECRET_KEY" ] || [ -z "$STRIPE_WEBHOOK_SECRET" ] || [ -z "$APP_URL" ]; then
echo "❌ All fields are required. Please run the script again."
exit 1
fi
echo ""
echo "🔧 Setting Firebase Functions configuration..."
# Set environment variables
firebase functions:config:set \
stripe.secret_key="$STRIPE_SECRET_KEY" \
stripe.webhook_secret="$STRIPE_WEBHOOK_SECRET" \
app.url="$APP_URL"
echo ""
echo "📦 Installing Functions dependencies..."
cd functions
npm install
echo ""
echo "🔨 Building Functions..."
npm run build
echo ""
echo "✅ Setup complete!"
echo ""
echo "📋 Next steps:"
echo "1. Deploy functions: firebase deploy --only functions"
echo ""
echo "2. Configure Stripe webhooks:"
echo " Platform webhook URL: https://us-central1-black-canyon-tickets.cloudfunctions.net/stripeWebhook"
echo " Events: account.updated"
echo ""
echo " Connect webhook URL: https://us-central1-black-canyon-tickets.cloudfunctions.net/stripeConnectWebhook"
echo " Events: checkout.session.completed, payment_intent.succeeded"
echo ""
echo "3. Test the integration:"
echo " - Start your React app: npm run dev"
echo " - Navigate to /org/{orgId}/payments"
echo " - Click 'Connect Stripe' to test onboarding"
echo ""
cd ..

View File

@@ -0,0 +1,196 @@
#!/usr/bin/env node
/**
* Theme Validation Script
* Ensures no hardcoded colors are used in the codebase
*/
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Color patterns to detect
const FORBIDDEN_PATTERNS = [
// Hardcoded hex colors
/#[0-9a-fA-F]{3,6}/g,
// Tailwind color classes we don't want
/\b(slate|gray|zinc|neutral|stone)-\d+/g,
/\b(red|green|blue|yellow|purple|pink|indigo|cyan|teal|orange|amber|lime|emerald|sky|violet|fuchsia|rose)-\d+/g,
// Generic color names in class names
/\b(text|bg|border)-(white|black)\b/g,
// RGB/RGBA functions
/rgba?\s*\([^)]+\)/g,
// HSL functions
/hsla?\s*\([^)]+\)/g,
// Gradient utilities with hardcoded colors
/\bfrom-(slate|gray|white|black|red|green|blue|yellow|purple|pink|indigo|cyan|teal|orange|amber|lime|emerald|sky|violet|fuchsia|rose)-\d+/g,
/\bto-(slate|gray|white|black|red|green|blue|yellow|purple|pink|indigo|cyan|teal|orange|amber|lime|emerald|sky|violet|fuchsia|rose)-\d+/g,
];
// Files to check
const EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.css'];
const EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'build'];
const EXCLUDED_FILES = [
'tailwind.config.js', // Allow CSS variables in config
'tokens.css', // Allow generated CSS variables
'validate-theme.js', // Allow this script itself
];
// Allowed exceptions (for documentation, comments, etc.)
const ALLOWED_EXCEPTIONS = [
// Comments about colors
/\/\*.*?(#[0-9a-fA-F]{3,6}|slate-\d+).*?\*\//g,
/\/\/.*?(#[0-9a-fA-F]{3,6}|slate-\d+)/g,
// String literals (not class names)
/"[^"]*?(#[0-9a-fA-F]{3,6}|slate-\d+)[^"]*?"/g,
/'[^']*?(#[0-9a-fA-F]{3,6}|slate-\d+)[^']*?'/g,
// Console.log and similar
/console\.(log|warn|error).*?(#[0-9a-fA-F]{3,6}|slate-\d+)/g,
];
function getAllFiles(dir, extensions = EXTENSIONS) {
let results = [];
const list = fs.readdirSync(dir);
list.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
if (!EXCLUDED_DIRS.includes(file)) {
results = results.concat(getAllFiles(filePath, extensions));
}
} else {
const ext = path.extname(file);
const basename = path.basename(file);
if (extensions.includes(ext) && !EXCLUDED_FILES.includes(basename)) {
results.push(filePath);
}
}
});
return results;
}
function removeAllowedExceptions(content) {
let cleaned = content;
ALLOWED_EXCEPTIONS.forEach(pattern => {
cleaned = cleaned.replace(pattern, '');
});
return cleaned;
}
function validateFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const cleanedContent = removeAllowedExceptions(content);
const violations = [];
FORBIDDEN_PATTERNS.forEach(pattern => {
const matches = [...cleanedContent.matchAll(pattern)];
matches.forEach(match => {
const lineNumber = content.substring(0, match.index).split('\n').length;
violations.push({
file: filePath,
line: lineNumber,
match: match[0],
pattern: pattern.toString(),
});
});
});
return violations;
}
function main() {
console.log('🎨 Validating theme system...\n');
const projectRoot = path.join(__dirname, '..');
const srcDir = path.join(projectRoot, 'src');
const files = getAllFiles(srcDir);
console.log(`Checking ${files.length} files for hardcoded colors...\n`);
let totalViolations = 0;
const violationsByFile = {};
files.forEach(file => {
const violations = validateFile(file);
if (violations.length > 0) {
violationsByFile[file] = violations;
totalViolations += violations.length;
}
});
if (totalViolations === 0) {
console.log('✅ No hardcoded colors found! Theme system is clean.\n');
// Additional check: ensure semantic classes are being used
console.log('🔍 Checking for proper semantic token usage...\n');
const semanticPatterns = [
/\btext-text-(primary|secondary|muted|inverse|disabled)\b/g,
/\bbg-bg-(primary|secondary|tertiary|elevated|overlay)\b/g,
/\bborder-border\b/g,
/\baccent-(primary|secondary|gold)-\d+/g,
];
let semanticUsageCount = 0;
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
semanticPatterns.forEach(pattern => {
const matches = content.match(pattern);
if (matches) {
semanticUsageCount += matches.length;
}
});
});
console.log(`✅ Found ${semanticUsageCount} instances of semantic token usage.\n`);
return process.exit(0);
}
console.log(`❌ Found ${totalViolations} hardcoded color violations:\n`);
Object.entries(violationsByFile).forEach(([file, violations]) => {
console.log(`📄 ${file}:`);
violations.forEach(violation => {
console.log(` Line ${violation.line}: "${violation.match}"`);
});
console.log('');
});
console.log('🚨 VALIDATION FAILED!\n');
console.log('Please replace hardcoded colors with semantic tokens from the design system.');
console.log('See THEMING.md for guidance on proper token usage.\n');
console.log('Common replacements:');
console.log(' text-slate-900 → text-text-primary');
console.log(' text-slate-600 → text-text-secondary');
console.log(' bg-white → bg-bg-primary');
console.log(' border-slate-200 → border-border');
console.log(' #3b82f6 → Use accent-primary-500 or similar\n');
process.exit(1);
}
main();