Compare commits
3 Commits
6746fc72b7
...
a049472a13
| Author | SHA1 | Date | |
|---|---|---|---|
| a049472a13 | |||
| 92ab9406be | |||
| 988294a55d |
49
.mcp.json
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"context": {
|
||||||
|
"modes": ["sequential-thinking"]
|
||||||
|
},
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"supabase": {
|
"supabase": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
@@ -10,6 +13,52 @@
|
|||||||
"env": {
|
"env": {
|
||||||
"SUPABASE_ACCESS_TOKEN": "sbp_d27758bc99df08610f063d2b8964cc0ddd94d00b"
|
"SUPABASE_ACCESS_TOKEN": "sbp_d27758bc99df08610f063d2b8964cc0ddd94d00b"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"stripe": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@stripe/mcp@latest",
|
||||||
|
"--tools=all"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"STRIPE_SECRET_KEY": "${STRIPE_SECRET_KEY}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ide": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-ide@latest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"playwright_ui_login": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-ui-login@latest"]
|
||||||
|
},
|
||||||
|
"playwright_cookie_restore": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-cookie-restore@latest"]
|
||||||
|
},
|
||||||
|
"playwright_check_auth_routes": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-check-auth-routes@latest"]
|
||||||
|
},
|
||||||
|
"playwright_multi_role_simulation": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-multi-role@latest"]
|
||||||
|
},
|
||||||
|
"playwright_screenshot_compare": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-screenshot-compare@latest"]
|
||||||
|
},
|
||||||
|
"playwright_network_inspector": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-network-inspector@latest"]
|
||||||
|
},
|
||||||
|
"playwright_trace_debugger": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@playwright/mcp-trace-debugger@latest"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
81
CLAUDE.md
@@ -16,16 +16,30 @@ npm run dev # Start development server at localhost:4321
|
|||||||
npm run start # Alias for npm run dev
|
npm run start # Alias for npm run dev
|
||||||
|
|
||||||
# Building & Testing
|
# Building & Testing
|
||||||
npm run build # Type check and build for production
|
npm run build # Type check and build for production (8GB memory allocated)
|
||||||
npm run typecheck # Run Astro type checking only
|
npm run typecheck # Run Astro type checking only
|
||||||
npm run preview # Preview production build locally
|
npm run preview # Preview production build locally
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
npm run lint # Run ESLint on codebase
|
||||||
|
npm run lint:fix # Run ESLint with auto-fix
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npx playwright test # Run Playwright end-to-end tests
|
||||||
|
npx playwright test --headed # Run tests with visible browser
|
||||||
|
npx playwright test --ui # Run tests with Playwright UI
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
node setup-schema.js # Initialize database schema (run once)
|
node setup-schema.js # Initialize database schema (run once)
|
||||||
|
|
||||||
# Docker Development (IMPORTANT: Always use --no-cache when rebuilding)
|
# Docker Development (IMPORTANT: Always use --no-cache when rebuilding)
|
||||||
|
npm run docker:build # Build Docker images using script
|
||||||
|
npm run docker:up # Start development containers
|
||||||
|
npm run docker:down # Stop development containers
|
||||||
|
npm run docker:logs # View container logs
|
||||||
|
npm run docker:prod:up # Start production containers
|
||||||
|
npm run docker:prod:down # Stop production containers
|
||||||
docker-compose build --no-cache # Clean rebuild when cache issues occur
|
docker-compose build --no-cache # Clean rebuild when cache issues occur
|
||||||
docker-compose down && docker-compose up -d # Clean restart containers
|
|
||||||
|
|
||||||
# Stripe MCP (Model Context Protocol)
|
# Stripe MCP (Model Context Protocol)
|
||||||
npm run mcp:stripe # Start Stripe MCP server for AI integration
|
npm run mcp:stripe # Start Stripe MCP server for AI integration
|
||||||
@@ -197,8 +211,16 @@ const formattedDate = api.formatDate(dateString);
|
|||||||
|
|
||||||
## Testing & Monitoring
|
## Testing & Monitoring
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
- **End-to-End Tests**: Playwright for critical user flows and authentication
|
||||||
|
- **Test Configuration**: `playwright.config.js` configured for localhost:3000
|
||||||
|
- **Test Files**: Pattern `test-*.js` and `test-*.cjs` for various scenarios
|
||||||
|
- **Test Execution**: Tests assume server is running (use `npm run dev` first)
|
||||||
|
- **Authentication Tests**: Comprehensive login/logout flow validation
|
||||||
|
- **Mobile Testing**: Responsive design and mobile menu testing
|
||||||
|
|
||||||
### Error Tracking
|
### Error Tracking
|
||||||
- **Sentry**: Configured for both client and server-side errors
|
- **Sentry**: Configured for both client and server-side errors (currently disabled in config)
|
||||||
- **Logging**: Winston for server-side logging to files
|
- **Logging**: Winston for server-side logging to files
|
||||||
- **Performance**: Sentry performance monitoring enabled
|
- **Performance**: Sentry performance monitoring enabled
|
||||||
|
|
||||||
@@ -228,6 +250,13 @@ SENTRY_DSN=https://...
|
|||||||
2. **API Endpoints**: Create in `/src/pages/api/` with proper validation
|
2. **API Endpoints**: Create in `/src/pages/api/` with proper validation
|
||||||
3. **UI Components**: Follow glassmorphism design system patterns
|
3. **UI Components**: Follow glassmorphism design system patterns
|
||||||
4. **Types**: Update `database.types.ts` or regenerate from Supabase
|
4. **Types**: Update `database.types.ts` or regenerate from Supabase
|
||||||
|
5. **Testing**: Add Playwright tests for critical user flows
|
||||||
|
6. **Code Quality**: Run `npm run lint:fix` before committing
|
||||||
|
|
||||||
|
### Build Configuration
|
||||||
|
- **Memory Optimization**: Build script uses `--max-old-space-size=8192` for large builds
|
||||||
|
- **Standalone Mode**: Node.js adapter configured for self-hosting
|
||||||
|
- **Server Configuration**: Default port 3000 with HMR support
|
||||||
|
|
||||||
### Event Management System
|
### Event Management System
|
||||||
The `/events/[id]/manage.astro` page is the core of the platform:
|
The `/events/[id]/manage.astro` page is the core of the platform:
|
||||||
@@ -277,3 +306,49 @@ The `/events/[id]/manage.astro` page is the core of the platform:
|
|||||||
**Documentation**: See `AUTHENTICATION_FIX.md` for complete technical details
|
**Documentation**: See `AUTHENTICATION_FIX.md` for complete technical details
|
||||||
|
|
||||||
**⚠️ IMPORTANT**: Do NOT modify the authentication system without understanding this fix. The httpOnly cookie approach is intentional for security and requires server-side validation for client scripts.
|
**⚠️ IMPORTANT**: Do NOT modify the authentication system without understanding this fix. The httpOnly cookie approach is intentional for security and requires server-side validation for client scripts.
|
||||||
|
|
||||||
|
## Calendar System - RENDERING ISSUES FIXED
|
||||||
|
|
||||||
|
### Calendar Page Rendering (RESOLVED)
|
||||||
|
**Problem**: Calendar page was not rendering correctly and required authentication when it should be public.
|
||||||
|
|
||||||
|
**Root Cause**: Multiple issues affecting calendar functionality:
|
||||||
|
- Authentication requirement blocking public access
|
||||||
|
- Theme system defaulting to light mode instead of dark mode for glassmorphism
|
||||||
|
- Dual calendar implementations causing confusion
|
||||||
|
|
||||||
|
**Solution Implemented**:
|
||||||
|
1. **Made Calendar Public**: Removed authentication requirement from `/src/pages/calendar.astro`
|
||||||
|
2. **Fixed Theme System**: Changed default theme to dark mode for better glassmorphism appearance
|
||||||
|
3. **Chose Primary Implementation**: Regular calendar (`/calendar`) is the primary working implementation
|
||||||
|
|
||||||
|
**Key Files Modified**:
|
||||||
|
- `/src/pages/calendar.astro` - Removed auth requirement, fixed theme default
|
||||||
|
- `/src/pages/calendar-enhanced.astro` - Removed forced dark mode theme blocking
|
||||||
|
|
||||||
|
**Current Status**:
|
||||||
|
- ✅ Calendar page loads correctly at `/calendar`
|
||||||
|
- ✅ Beautiful glassmorphism theme with purple gradients
|
||||||
|
- ✅ Full calendar functionality (navigation, filters, search, view toggles)
|
||||||
|
- ✅ All navigation links point to working calendar page
|
||||||
|
- ✅ Responsive design works on desktop and mobile
|
||||||
|
- ⚠️ Enhanced calendar at `/calendar-enhanced` has React component mounting issues (not used in production)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Code Quality Standards
|
||||||
|
- **ESLint**: Configured with TypeScript support and custom rules
|
||||||
|
- **Astro Files**: ESLint parsing disabled for `.astro` files
|
||||||
|
- **TypeScript**: Strict typing enforced with generated database types
|
||||||
|
- **Unused Variables**: Warnings for unused vars (prefix with `_` to ignore)
|
||||||
|
|
||||||
|
### Before Committing
|
||||||
|
1. Run `npm run lint:fix` to fix code style issues
|
||||||
|
2. Run `npm run typecheck` to validate TypeScript
|
||||||
|
3. Run `npm run build` to ensure production build works
|
||||||
|
4. Test critical flows with `npx playwright test`
|
||||||
|
|
||||||
|
### Development Server
|
||||||
|
- **Port**: Defaults to 3000 (configurable via PORT env var)
|
||||||
|
- **HMR**: Hot module replacement enabled on all interfaces
|
||||||
|
- **Security**: Origin checking enabled for production security
|
||||||
BIN
after-add-ticket-click-authenticated.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
after-create-first-authenticated.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
after-ticket-types-load.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
calendar-diagnosis.png
Normal file
|
After Width: | Height: | Size: 833 KiB |
BIN
dashboard-debug.png
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
homepage-access.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
homepage-debug.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
login-before-auth.png
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
login-failed.png
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
login-page.png
|
Before Width: | Height: | Size: 437 KiB After Width: | Height: | Size: 436 KiB |
BIN
manage-page-authenticated.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
manage-page-debug.png
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
manage-page-dev.png
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
manage-page.png
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
modal-light-mode-test.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
@@ -26,7 +26,11 @@
|
|||||||
"docker:astro:down": "docker-compose -f docker-compose.astro.yml down",
|
"docker:astro:down": "docker-compose -f docker-compose.astro.yml down",
|
||||||
"docker:astro:logs": "docker-compose -f docker-compose.astro.yml logs -f",
|
"docker:astro:logs": "docker-compose -f docker-compose.astro.yml logs -f",
|
||||||
"docker:dev": "docker-compose up -d",
|
"docker:dev": "docker-compose up -d",
|
||||||
"docker:dev:build": "docker-compose up -d --build"
|
"docker:dev:build": "docker-compose up -d --build",
|
||||||
|
"cache:clear": "./scripts/clear-cache.sh",
|
||||||
|
"cache:clear:hard": "./scripts/clear-cache.sh && npm run docker:build --no-cache && npm run docker:up",
|
||||||
|
"dev:clean": "./scripts/clear-cache.sh && npm run dev",
|
||||||
|
"build:clean": "./scripts/clear-cache.sh && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,122 @@
|
|||||||
|
# Page snapshot
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- link "Skip to main content":
|
||||||
|
- /url: "#main-content"
|
||||||
|
- link "Skip to navigation":
|
||||||
|
- /url: "#navigation"
|
||||||
|
- main:
|
||||||
|
- navigation:
|
||||||
|
- link "BCT":
|
||||||
|
- /url: /dashboard
|
||||||
|
- img
|
||||||
|
- text: BCT
|
||||||
|
- link "All Events":
|
||||||
|
- /url: /dashboard
|
||||||
|
- img
|
||||||
|
- text: All Events
|
||||||
|
- text: "| Event Management"
|
||||||
|
- button "Switch to dark mode":
|
||||||
|
- img
|
||||||
|
- button "T Tyler Admin":
|
||||||
|
- text: T Tyler Admin
|
||||||
|
- img
|
||||||
|
- main:
|
||||||
|
- heading "Garfield County Fair & Rodeo 2025" [level=1]
|
||||||
|
- img
|
||||||
|
- text: Garfield County Fairgrounds - Rifle, CO
|
||||||
|
- img
|
||||||
|
- text: Wednesday, August 6, 2025 at 12:00 AM
|
||||||
|
- paragraph: Secure your spot at the 87-year-strong Garfield County Fair & Rodeo, returning to Rifle, Colorado August 2 and 6-9, 2025...
|
||||||
|
- button "Show more"
|
||||||
|
- text: $0 Total Revenue
|
||||||
|
- link "Preview":
|
||||||
|
- /url: /e/firebase-event-zmkx63pewurqyhtw6tsn
|
||||||
|
- img
|
||||||
|
- text: Preview
|
||||||
|
- button "Embed":
|
||||||
|
- img
|
||||||
|
- text: Embed
|
||||||
|
- button "Edit":
|
||||||
|
- img
|
||||||
|
- text: Edit
|
||||||
|
- link "Scanner":
|
||||||
|
- /url: /scan
|
||||||
|
- img
|
||||||
|
- text: Scanner
|
||||||
|
- link "Kiosk":
|
||||||
|
- /url: /kiosk/firebase-event-zmkx63pewurqyhtw6tsn
|
||||||
|
- img
|
||||||
|
- text: Kiosk
|
||||||
|
- button "PIN":
|
||||||
|
- img
|
||||||
|
- text: PIN
|
||||||
|
- paragraph: Tickets Sold
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: +12% from last week
|
||||||
|
- img
|
||||||
|
- paragraph: Available
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: Ready to sell
|
||||||
|
- img
|
||||||
|
- paragraph: Check-ins
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: 85% attendance rate
|
||||||
|
- img
|
||||||
|
- paragraph: Net Revenue
|
||||||
|
- paragraph: $0
|
||||||
|
- img
|
||||||
|
- text: +24% this month
|
||||||
|
- img
|
||||||
|
- button "Ticketing & Access":
|
||||||
|
- img
|
||||||
|
- text: Ticketing & Access
|
||||||
|
- button "Custom Pages":
|
||||||
|
- img
|
||||||
|
- text: Custom Pages
|
||||||
|
- button "Sales":
|
||||||
|
- img
|
||||||
|
- text: Sales
|
||||||
|
- button "Attendees":
|
||||||
|
- img
|
||||||
|
- text: Attendees
|
||||||
|
- button "Event Settings":
|
||||||
|
- img
|
||||||
|
- text: Event Settings
|
||||||
|
- button "Ticket Types"
|
||||||
|
- button "Access Codes"
|
||||||
|
- button "Discounts"
|
||||||
|
- button "Printed Tickets"
|
||||||
|
- heading "Ticket Types & Pricing" [level=2]
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- button "Add Ticket Type":
|
||||||
|
- img
|
||||||
|
- text: Add Ticket Type
|
||||||
|
- img
|
||||||
|
- paragraph: No ticket types created yet
|
||||||
|
- button "Create Your First Ticket Type"
|
||||||
|
- contentinfo:
|
||||||
|
- link "Terms of Service":
|
||||||
|
- /url: /terms
|
||||||
|
- link "Privacy Policy":
|
||||||
|
- /url: /privacy
|
||||||
|
- link "Support":
|
||||||
|
- /url: /support
|
||||||
|
- text: © 2025 All rights reserved Powered by
|
||||||
|
- link "blackcanyontickets.com":
|
||||||
|
- /url: https://blackcanyontickets.com
|
||||||
|
- img
|
||||||
|
- heading "Cookie Preferences" [level=3]
|
||||||
|
- paragraph:
|
||||||
|
- text: We use essential cookies to make our website work and analytics cookies to understand how you interact with our site.
|
||||||
|
- link "Learn more in our Privacy Policy":
|
||||||
|
- /url: /privacy
|
||||||
|
- button "Manage Preferences"
|
||||||
|
- button "Accept All"
|
||||||
|
```
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
const { defineConfig, devices } = require('@playwright/test');
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
module.exports = defineConfig({
|
export default defineConfig({
|
||||||
testDir: './',
|
testDir: './',
|
||||||
testMatch: 'test-calendar-theme.cjs',
|
testMatch: 'test-*.cjs',
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
@@ -20,9 +20,9 @@ module.exports = defineConfig({
|
|||||||
use: { ...devices['Desktop Chrome'] },
|
use: { ...devices['Desktop Chrome'] },
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
webServer: {
|
// webServer: {
|
||||||
command: 'echo "Server assumed to be running"',
|
// command: 'echo "Server assumed to be running"',
|
||||||
url: 'http://localhost:3000',
|
// url: 'http://192.168.0.46:3000',
|
||||||
reuseExistingServer: true,
|
// reuseExistingServer: true,
|
||||||
},
|
// },
|
||||||
});
|
});
|
||||||
49
src/components/EmbedModalWrapper.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import EmbedCodeModal from './modals/EmbedCodeModal.tsx';
|
||||||
|
|
||||||
|
interface EmbedModalWrapperProps {
|
||||||
|
eventId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global state interface
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
embedModalState?: {
|
||||||
|
isOpen: boolean;
|
||||||
|
eventId: string;
|
||||||
|
eventSlug: string;
|
||||||
|
};
|
||||||
|
openEmbedModal?: (eventId: string, eventSlug: string) => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmbedModalWrapper({ eventId }: EmbedModalWrapperProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [eventSlug, setEventSlug] = useState('loading');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Function to open modal from JavaScript
|
||||||
|
window.openEmbedModal = (eventId: string, eventSlug: string) => {
|
||||||
|
setEventSlug(eventSlug);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
delete window.openEmbedModal;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbedCodeModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
eventId={eventId}
|
||||||
|
eventSlug={eventSlug}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
import SimpleEmbedTest from './SimpleEmbedTest.tsx';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,12 @@ const { eventId } = Astro.props;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple Embed Test -->
|
||||||
|
<SimpleEmbedTest
|
||||||
|
client:load
|
||||||
|
eventId={eventId}
|
||||||
|
/>
|
||||||
|
|
||||||
<script define:vars={{ eventId }}>
|
<script define:vars={{ eventId }}>
|
||||||
// Initialize event header when page loads
|
// Initialize event header when page loads
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
@@ -220,82 +228,7 @@ const { eventId } = Astro.props;
|
|||||||
|
|
||||||
// Event handlers will be added after functions are defined
|
// Event handlers will be added after functions are defined
|
||||||
|
|
||||||
// Function to show embed modal
|
|
||||||
function showEmbedModal(eventId, eventSlug, eventTitle) {
|
|
||||||
// Create modal backdrop
|
|
||||||
const backdrop = document.createElement('div');
|
|
||||||
backdrop.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center p-4';
|
|
||||||
backdrop.style.display = 'flex';
|
|
||||||
|
|
||||||
// Create modal content
|
|
||||||
const modal = document.createElement('div');
|
|
||||||
modal.className = 'bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto';
|
|
||||||
|
|
||||||
const embedUrl = `${window.location.origin}/e/${eventSlug}`;
|
|
||||||
const iframeCode = `<iframe src="${embedUrl}" width="100%" height="600" frameborder="0"></iframe>`;
|
|
||||||
|
|
||||||
modal.innerHTML = `
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900">Embed Your Event</h2>
|
|
||||||
<button id="close-embed-modal" class="text-gray-400 hover:text-gray-600">
|
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Direct Link</label>
|
|
||||||
<div class="flex">
|
|
||||||
<input type="text" value="${embedUrl}" readonly
|
|
||||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md bg-gray-50 text-sm" />
|
|
||||||
<button onclick="copyToClipboard('${embedUrl}')"
|
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded-r-md hover:bg-blue-700 text-sm">
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Embed Code</label>
|
|
||||||
<div class="flex">
|
|
||||||
<textarea readonly rows="3"
|
|
||||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md bg-gray-50 text-sm font-mono">${iframeCode}</textarea>
|
|
||||||
<button onclick="copyToClipboard('${iframeCode.replace(/'/g, '\\\'')}')"
|
|
||||||
class="px-4 py-2 bg-blue-600 text-white rounded-r-md hover:bg-blue-700 text-sm">
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-blue-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-medium text-blue-900 mb-2">How to use:</h3>
|
|
||||||
<ul class="text-sm text-blue-800 space-y-1">
|
|
||||||
<li>• Copy the direct link to share via email or social media</li>
|
|
||||||
<li>• Use the embed code to add this event to your website</li>
|
|
||||||
<li>• The embedded page is fully responsive and mobile-friendly</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
backdrop.appendChild(modal);
|
|
||||||
document.body.appendChild(backdrop);
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
document.getElementById('close-embed-modal').addEventListener('click', () => {
|
|
||||||
document.body.removeChild(backdrop);
|
|
||||||
});
|
|
||||||
|
|
||||||
backdrop.addEventListener('click', (e) => {
|
|
||||||
if (e.target === backdrop) {
|
|
||||||
document.body.removeChild(backdrop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to show edit event modal
|
// Function to show edit event modal
|
||||||
function showEditEventModal(event) {
|
function showEditEventModal(event) {
|
||||||
@@ -540,22 +473,31 @@ const { eventId } = Astro.props;
|
|||||||
if (embedBtn) {
|
if (embedBtn) {
|
||||||
embedBtn.addEventListener('click', () => {
|
embedBtn.addEventListener('click', () => {
|
||||||
// Get event details for the embed modal
|
// Get event details for the embed modal
|
||||||
const eventTitle = document.getElementById('event-title').textContent;
|
const previewLink = document.getElementById('preview-link').href;
|
||||||
const eventSlug = document.getElementById('preview-link').href.split('/e/')[1];
|
const eventSlug = previewLink ? previewLink.split('/e/')[1] : 'loading';
|
||||||
|
|
||||||
// Get eventId from the URL
|
// Get eventId from the URL
|
||||||
const urlParts = window.location.pathname.split('/');
|
const urlParts = window.location.pathname.split('/');
|
||||||
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
|
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
|
||||||
|
|
||||||
// Create and show embed modal
|
// Show embed modal using React component
|
||||||
showEmbedModal(currentEventId, eventSlug, eventTitle);
|
if (window.openEmbedModal) {
|
||||||
|
window.openEmbedModal(currentEventId, eventSlug);
|
||||||
|
} else {
|
||||||
|
// Fallback: show simple alert for debugging
|
||||||
|
alert(`Embed Modal Debug:\nEvent ID: ${currentEventId}\nEvent Slug: ${eventSlug}\nwindow.openEmbedModal: ${typeof window.openEmbedModal}`);
|
||||||
|
console.log('Embed button clicked but window.openEmbedModal not available');
|
||||||
|
console.log('Available window properties:', Object.keys(window).filter(k => k.includes('embed')));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listeners after DOM is loaded
|
// Add event listeners after DOM is loaded
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', addEventListeners);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
addEventListeners();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
addEventListeners();
|
addEventListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/components/SimpleEmbedTest.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface SimpleEmbedTestProps {
|
||||||
|
eventId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SimpleEmbedTest({ eventId }: SimpleEmbedTestProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('SimpleEmbedTest component mounted for event:', eventId);
|
||||||
|
|
||||||
|
// Set up the global function
|
||||||
|
window.openEmbedModal = (eventId: string, eventSlug: string) => {
|
||||||
|
alert(`Simple Embed Test!\nEvent ID: ${eventId}\nEvent Slug: ${eventSlug}`);
|
||||||
|
console.log('Simple embed modal triggered:', { eventId, eventSlug });
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('window.openEmbedModal set up successfully');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete window.openEmbedModal;
|
||||||
|
console.log('SimpleEmbedTest cleanup');
|
||||||
|
};
|
||||||
|
}, [eventId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'none' }}>
|
||||||
|
Simple Embed Test Component Loaded - Event: {eventId}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { loadTicketTypes, deleteTicketType, toggleTicketTypeStatus } from '../../lib/ticket-management';
|
import { loadTicketTypes, deleteTicketType, toggleTicketTypeStatus } from '../../lib/ticket-management';
|
||||||
import { loadSalesData, calculateSalesMetrics } from '../../lib/sales-analytics';
|
import { loadSalesData, calculateSalesMetrics } from '../../lib/sales-analytics';
|
||||||
import { formatCurrency } from '../../lib/event-management';
|
import { formatCurrency } from '../../lib/event-management';
|
||||||
import TicketTypeModal from '../modals/TicketTypeModal';
|
import TicketTypeModal from '../modals/TicketTypeModal.tsx';
|
||||||
import type { TicketType } from '../../lib/ticket-management';
|
import type { TicketType } from '../../lib/ticket-management';
|
||||||
|
|
||||||
interface TicketsTabProps {
|
interface TicketsTabProps {
|
||||||
@@ -39,8 +39,10 @@ export default function TicketsTab({ eventId }: TicketsTabProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateTicketType = () => {
|
const handleCreateTicketType = () => {
|
||||||
|
console.log('handleCreateTicketType called');
|
||||||
setEditingTicketType(undefined);
|
setEditingTicketType(undefined);
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
|
console.log('showModal set to true');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditTicketType = (ticketType: TicketType) => {
|
const handleEditTicketType = (ticketType: TicketType) => {
|
||||||
@@ -378,12 +380,14 @@ export default function TicketsTab({ eventId }: TicketsTabProps) {
|
|||||||
|
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<TicketTypeModal
|
<TicketTypeModal
|
||||||
|
isOpen={showModal}
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
ticketType={editingTicketType}
|
ticketType={editingTicketType}
|
||||||
onClose={() => setShowModal(false)}
|
onClose={() => setShowModal(false)}
|
||||||
onSave={loadData}
|
onSave={() => loadData()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{console.log('TicketsTab render - showModal:', showModal)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,14 +67,27 @@ export default function EmbedCodeModal({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center p-4 z-50" style={{ background: 'rgba(0, 0, 0, 0.3)' }}>
|
||||||
<div className="bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-2xl w-full max-w-sm sm:max-w-2xl lg:max-w-4xl max-h-[90vh] overflow-y-auto">
|
<div className="backdrop-blur-xl rounded-2xl shadow-2xl w-full max-w-sm sm:max-w-2xl lg:max-w-4xl max-h-[90vh] overflow-y-auto" style={{ background: 'var(--glass-bg)', border: '1px solid var(--glass-border)' }}>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h2 className="text-2xl font-light text-white">Embed Event Widget</h2>
|
<h2 className="text-2xl font-light" style={{ color: 'var(--glass-text-primary)' }}>Embed Event Widget</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-white/60 hover:text-white transition-colors p-2 rounded-full hover:bg-white/10 touch-manipulation"
|
className="transition-colors p-2 rounded-full hover:scale-105 touch-manipulation"
|
||||||
|
style={{
|
||||||
|
color: 'var(--glass-text-secondary)',
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.color = 'var(--glass-text-primary)';
|
||||||
|
e.currentTarget.style.background = 'var(--glass-bg-button-hover)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.color = 'var(--glass-text-secondary)';
|
||||||
|
e.currentTarget.style.background = 'var(--glass-bg-button)';
|
||||||
|
}}
|
||||||
aria-label="Close modal"
|
aria-label="Close modal"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -87,17 +100,22 @@ export default function EmbedCodeModal({
|
|||||||
{/* Configuration Panel */}
|
{/* Configuration Panel */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-white mb-4">Direct Link</h3>
|
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Direct Link</h3>
|
||||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
<div className="rounded-lg p-4" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<label className="text-sm text-white/80">Event URL</label>
|
<label className="text-sm" style={{ color: 'var(--glass-text-secondary)' }}>Event URL</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={directLink}
|
value={directLink}
|
||||||
readOnly
|
readOnly
|
||||||
className="flex-1 px-3 py-2 bg-white/10 border border-white/20 rounded-l-lg text-white text-sm"
|
className="flex-1 px-3 py-2 rounded-l-lg text-sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCopy(directLink, 'link')}
|
onClick={() => handleCopy(directLink, 'link')}
|
||||||
@@ -114,11 +132,11 @@ export default function EmbedCodeModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-white mb-4">Embed Options</h3>
|
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Embed Options</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/80 mb-2">Embed Type</label>
|
<label className="block text-sm mb-2" style={{ color: 'var(--glass-text-secondary)' }}>Embed Type</label>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@@ -126,9 +144,10 @@ export default function EmbedCodeModal({
|
|||||||
value="basic"
|
value="basic"
|
||||||
checked={embedType === 'basic'}
|
checked={embedType === 'basic'}
|
||||||
onChange={(e) => setEmbedType(e.target.value as 'basic' | 'custom')}
|
onChange={(e) => setEmbedType(e.target.value as 'basic' | 'custom')}
|
||||||
className="w-4 h-4 text-blue-600 bg-white/10 border-white/20"
|
className="w-4 h-4"
|
||||||
|
style={{ accentColor: 'var(--glass-text-accent)' }}
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-white text-sm">Basic</span>
|
<span className="ml-2 text-sm" style={{ color: 'var(--glass-text-primary)' }}>Basic</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
@@ -136,32 +155,43 @@ export default function EmbedCodeModal({
|
|||||||
value="custom"
|
value="custom"
|
||||||
checked={embedType === 'custom'}
|
checked={embedType === 'custom'}
|
||||||
onChange={(e) => setEmbedType(e.target.value as 'basic' | 'custom')}
|
onChange={(e) => setEmbedType(e.target.value as 'basic' | 'custom')}
|
||||||
className="w-4 h-4 text-blue-600 bg-white/10 border-white/20"
|
className="w-4 h-4"
|
||||||
|
style={{ accentColor: 'var(--glass-text-accent)' }}
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-white text-sm">Custom</span>
|
<span className="ml-2 text-sm" style={{ color: 'var(--glass-text-primary)' }}>Custom</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/80 mb-2">Width</label>
|
<label className="block text-sm mb-2" style={{ color: 'var(--glass-text-secondary)' }}>Width</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={width}
|
value={width}
|
||||||
onChange={(e) => setWidth(parseInt(e.target.value) || 400)}
|
onChange={(e) => setWidth(parseInt(e.target.value) || 400)}
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-sm"
|
className="w-full px-3 py-2 rounded-lg text-sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
min="300"
|
min="300"
|
||||||
max="800"
|
max="800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/80 mb-2">Height</label>
|
<label className="block text-sm mb-2" style={{ color: 'var(--glass-text-secondary)' }}>Height</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={height}
|
value={height}
|
||||||
onChange={(e) => setHeight(parseInt(e.target.value) || 600)}
|
onChange={(e) => setHeight(parseInt(e.target.value) || 600)}
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-sm"
|
className="w-full px-3 py-2 rounded-lg text-sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
min="400"
|
min="400"
|
||||||
max="1000"
|
max="1000"
|
||||||
/>
|
/>
|
||||||
@@ -171,11 +201,16 @@ export default function EmbedCodeModal({
|
|||||||
{embedType === 'custom' && (
|
{embedType === 'custom' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/80 mb-2">Theme</label>
|
<label className="block text-sm mb-2" style={{ color: 'var(--glass-text-secondary)' }}>Theme</label>
|
||||||
<select
|
<select
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
|
onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
|
||||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-sm"
|
className="w-full px-3 py-2 rounded-lg text-sm"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="light">Light</option>
|
<option value="light">Light</option>
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
@@ -183,12 +218,16 @@ export default function EmbedCodeModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-white/80 mb-2">Primary Color</label>
|
<label className="block text-sm mb-2" style={{ color: 'var(--glass-text-secondary)' }}>Primary Color</label>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={primaryColor}
|
value={primaryColor}
|
||||||
onChange={(e) => setPrimaryColor(e.target.value)}
|
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||||
className="w-full h-10 bg-white/10 border border-white/20 rounded-lg"
|
className="w-full h-10 rounded-lg"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)'
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -198,18 +237,20 @@ export default function EmbedCodeModal({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showHeader}
|
checked={showHeader}
|
||||||
onChange={(e) => setShowHeader(e.target.checked)}
|
onChange={(e) => setShowHeader(e.target.checked)}
|
||||||
className="w-4 h-4 text-blue-600 bg-white/10 border-white/20 rounded"
|
className="w-4 h-4 rounded"
|
||||||
|
style={{ accentColor: 'var(--glass-text-accent)' }}
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-white text-sm">Show Header</span>
|
<span className="ml-2 text-sm" style={{ color: 'var(--glass-text-primary)' }}>Show Header</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showDescription}
|
checked={showDescription}
|
||||||
onChange={(e) => setShowDescription(e.target.checked)}
|
onChange={(e) => setShowDescription(e.target.checked)}
|
||||||
className="w-4 h-4 text-blue-600 bg-white/10 border-white/20 rounded"
|
className="w-4 h-4 rounded"
|
||||||
|
style={{ accentColor: 'var(--glass-text-accent)' }}
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-white text-sm">Show Description</span>
|
<span className="ml-2 text-sm" style={{ color: 'var(--glass-text-primary)' }}>Show Description</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,13 +259,14 @@ export default function EmbedCodeModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-white mb-4">Embed Code</h3>
|
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Embed Code</h3>
|
||||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
<div className="rounded-lg p-4" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||||
<textarea
|
<textarea
|
||||||
value={generateEmbedCode()}
|
value={generateEmbedCode()}
|
||||||
readOnly
|
readOnly
|
||||||
rows={6}
|
rows={6}
|
||||||
className="w-full bg-transparent text-white text-sm font-mono resize-none"
|
className="w-full bg-transparent text-sm font-mono resize-none"
|
||||||
|
style={{ color: 'var(--glass-text-primary)' }}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex justify-end">
|
<div className="mt-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -244,8 +286,8 @@ export default function EmbedCodeModal({
|
|||||||
|
|
||||||
{/* Preview Panel */}
|
{/* Preview Panel */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-medium text-white mb-4">Preview</h3>
|
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Preview</h3>
|
||||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
<div className="rounded-lg p-4" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||||
<div className="bg-white rounded-lg overflow-hidden">
|
<div className="bg-white rounded-lg overflow-hidden">
|
||||||
<iframe
|
<iframe
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
@@ -264,7 +306,10 @@ export default function EmbedCodeModal({
|
|||||||
<div className="flex justify-end mt-6">
|
<div className="flex justify-end mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg font-medium transition-all duration-200"
|
className="px-6 py-3 text-white rounded-lg font-medium transition-all duration-200 hover:shadow-lg hover:scale-105"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-text-accent)'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export default function TicketTypeModal({
|
|||||||
const [formData, setFormData] = useState<TicketTypeFormData>({
|
const [formData, setFormData] = useState<TicketTypeFormData>({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
price_cents: 0,
|
price: 0,
|
||||||
quantity: 100,
|
quantity_available: 100,
|
||||||
is_active: true
|
is_active: true
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -32,16 +32,16 @@ export default function TicketTypeModal({
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: ticketType.name,
|
name: ticketType.name,
|
||||||
description: ticketType.description,
|
description: ticketType.description,
|
||||||
price_cents: ticketType.price_cents,
|
price: ticketType.price,
|
||||||
quantity: ticketType.quantity,
|
quantity_available: ticketType.quantity_available,
|
||||||
is_active: ticketType.is_active
|
is_active: ticketType.is_active
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
price_cents: 0,
|
price: 0,
|
||||||
quantity: 100,
|
quantity_available: 100,
|
||||||
is_active: true
|
is_active: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export default function TicketTypeModal({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center p-4 z-50" style={{ background: 'rgba(0, 0, 0, 0.5)' }}>
|
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center p-4 z-50" style={{ background: 'rgba(0, 0, 0, 0.75)' }}>
|
||||||
<div className="backdrop-blur-xl rounded-2xl shadow-2xl w-full max-w-md sm:max-w-lg max-h-[90vh] overflow-y-auto" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
<div className="backdrop-blur-xl rounded-2xl shadow-2xl w-full max-w-md sm:max-w-lg max-h-[90vh] overflow-y-auto" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
@@ -136,13 +136,18 @@ export default function TicketTypeModal({
|
|||||||
required
|
required
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-4 py-3 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
placeholder="e.g., General Admission"
|
placeholder="e.g., General Admission"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="description" className="block text-sm font-medium text-white/80 mb-2">
|
<label htmlFor="description" className="block text-sm font-medium mb-2" style={{ color: 'var(--glass-text-secondary)' }}>
|
||||||
Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -151,49 +156,64 @@ export default function TicketTypeModal({
|
|||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
className="w-full px-4 py-3 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
placeholder="Brief description of this ticket type..."
|
placeholder="Brief description of this ticket type..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="price_cents" className="block text-sm font-medium text-white/80 mb-2">
|
<label htmlFor="price" className="block text-sm font-medium mb-2" style={{ color: 'var(--glass-text-secondary)' }}>
|
||||||
Price ($) *
|
Price ($) *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="price_cents"
|
id="price"
|
||||||
name="price_cents"
|
name="price"
|
||||||
required
|
required
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.price_cents / 100}
|
value={formData.price}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const dollars = parseFloat(e.target.value) || 0;
|
const price = parseFloat(e.target.value) || 0;
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
price_cents: Math.round(dollars * 100)
|
price: price
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-4 py-3 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="quantity" className="block text-sm font-medium text-white/80 mb-2">
|
<label htmlFor="quantity_available" className="block text-sm font-medium mb-2" style={{ color: 'var(--glass-text-secondary)' }}>
|
||||||
Quantity *
|
Quantity *
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
id="quantity"
|
id="quantity_available"
|
||||||
name="quantity"
|
name="quantity_available"
|
||||||
required
|
required
|
||||||
min="1"
|
min="1"
|
||||||
value={formData.quantity}
|
value={formData.quantity_available}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full px-4 py-3 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
style={{
|
||||||
|
background: 'var(--glass-bg-button)',
|
||||||
|
border: '1px solid var(--glass-border)',
|
||||||
|
color: 'var(--glass-text-primary)'
|
||||||
|
}}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,9 +226,10 @@ export default function TicketTypeModal({
|
|||||||
name="is_active"
|
name="is_active"
|
||||||
checked={formData.is_active}
|
checked={formData.is_active}
|
||||||
onChange={handleCheckboxChange}
|
onChange={handleCheckboxChange}
|
||||||
className="w-4 h-4 text-blue-600 bg-white/10 border-white/20 rounded focus:ring-blue-500 focus:ring-2"
|
className="w-4 h-4 rounded focus:ring-blue-500 focus:ring-2"
|
||||||
|
style={{ accentColor: 'var(--glass-text-accent)' }}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="is_active" className="ml-2 text-sm text-white/80">
|
<label htmlFor="is_active" className="ml-2 text-sm" style={{ color: 'var(--glass-text-secondary)' }}>
|
||||||
Active (available for purchase)
|
Active (available for purchase)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,14 +238,16 @@ export default function TicketTypeModal({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-6 py-3 text-white/80 hover:text-white transition-colors"
|
className="px-6 py-3 transition-colors"
|
||||||
|
style={{ color: 'var(--glass-text-secondary)' }}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg font-medium transition-all duration-200 disabled:opacity-50"
|
className="px-6 py-3 text-white rounded-lg font-medium transition-all duration-200 disabled:opacity-50"
|
||||||
|
style={{ background: 'var(--glass-text-accent)' }}
|
||||||
>
|
>
|
||||||
{loading ? 'Saving...' : ticketType ? 'Update' : 'Create'}
|
{loading ? 'Saving...' : ticketType ? 'Update' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { supabase } from './supabase';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin API Router for centralized admin dashboard API calls
|
* Admin API Router for centralized admin dashboard API calls
|
||||||
* This provides a centralized way to handle admin-specific API operations
|
* This provides a centralized way to handle admin-specific API operations
|
||||||
|
* All database queries now go through server-side API endpoints to avoid CORS issues
|
||||||
*/
|
*/
|
||||||
export class AdminApiRouter {
|
export class AdminApiRouter {
|
||||||
private session: any = null;
|
private session: any = null;
|
||||||
@@ -64,46 +63,28 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [organizationsResult, eventsResult, ticketsResult] = await Promise.all([
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
supabase.from('organizations').select('id'),
|
const response = await fetch('/api/admin/stats', {
|
||||||
supabase.from('events').select('id'),
|
|
||||||
supabase.from('tickets').select('price')
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Get user count from API endpoint to bypass RLS
|
|
||||||
let users = 0;
|
|
||||||
try {
|
|
||||||
const usersResponse = await fetch('/api/admin/users', {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
if (usersResponse.ok) {
|
|
||||||
const usersResult = await usersResponse.json();
|
if (!response.ok) {
|
||||||
if (usersResult.success) {
|
console.error('Failed to fetch platform stats:', response.status, response.statusText);
|
||||||
users = usersResult.data?.length || 0;
|
return { organizations: 0, events: 0, tickets: 0, revenue: 0, platformFees: 0, users: 0, error: 'Failed to load platform statistics' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Platform stats API error:', result.error);
|
||||||
|
return { organizations: 0, events: 0, tickets: 0, revenue: 0, platformFees: 0, users: 0, error: result.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching user count for stats:', error);
|
console.error('Platform stats error:', error);
|
||||||
}
|
|
||||||
|
|
||||||
const organizations = organizationsResult.data?.length || 0;
|
|
||||||
const events = eventsResult.data?.length || 0;
|
|
||||||
const tickets = ticketsResult.data || [];
|
|
||||||
const ticketCount = tickets.length;
|
|
||||||
const revenue = tickets.reduce((sum, ticket) => sum + (ticket.price || 0), 0);
|
|
||||||
const platformFees = revenue * 0.05; // Assuming 5% platform fee
|
|
||||||
|
|
||||||
return {
|
|
||||||
organizations,
|
|
||||||
events,
|
|
||||||
tickets: ticketCount,
|
|
||||||
revenue,
|
|
||||||
platformFees,
|
|
||||||
users
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
return { organizations: 0, events: 0, tickets: 0, revenue: 0, platformFees: 0, users: 0, error: 'Failed to load platform statistics' };
|
return { organizations: 0, events: 0, tickets: 0, revenue: 0, platformFees: 0, users: 0, error: 'Failed to load platform statistics' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,75 +101,28 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [eventsResult, ticketsResult] = await Promise.all([
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
supabase.from('events').select('*, organizations(name)').order('created_at', { ascending: false }).limit(5),
|
const response = await fetch('/api/admin/activity', {
|
||||||
supabase.from('tickets').select('*, events(title)').order('created_at', { ascending: false }).limit(10)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Get recent users from API endpoint to bypass RLS
|
|
||||||
let usersResult = { data: [] };
|
|
||||||
try {
|
|
||||||
const usersResponse = await fetch('/api/admin/users', {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include'
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
if (usersResponse.ok) {
|
|
||||||
const result = await usersResponse.json();
|
if (!response.ok) {
|
||||||
if (result.success) {
|
console.error('Failed to fetch recent activity:', response.status, response.statusText);
|
||||||
// Limit to 5 most recent users
|
return [];
|
||||||
usersResult.data = result.data.slice(0, 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Recent activity API error:', result.error);
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching users for recent activity:', error);
|
console.error('Recent activity error:', error);
|
||||||
}
|
|
||||||
|
|
||||||
const activities = [];
|
|
||||||
|
|
||||||
// Add recent events
|
|
||||||
if (eventsResult.data) {
|
|
||||||
eventsResult.data.forEach(event => {
|
|
||||||
activities.push({
|
|
||||||
type: 'event',
|
|
||||||
title: `New event created: ${event.title}`,
|
|
||||||
subtitle: `by ${event.organizations?.name || 'Unknown'}`,
|
|
||||||
time: new Date(event.created_at),
|
|
||||||
icon: '📅'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add recent users
|
|
||||||
if (usersResult.data) {
|
|
||||||
usersResult.data.forEach(user => {
|
|
||||||
activities.push({
|
|
||||||
type: 'user',
|
|
||||||
title: `New user registered: ${user.name || user.email}`,
|
|
||||||
subtitle: `Organization: ${user.organizations?.name || 'None'}`,
|
|
||||||
time: new Date(user.created_at),
|
|
||||||
icon: '👤'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add recent tickets
|
|
||||||
if (ticketsResult.data) {
|
|
||||||
ticketsResult.data.slice(0, 5).forEach(ticket => {
|
|
||||||
activities.push({
|
|
||||||
type: 'ticket',
|
|
||||||
title: `Ticket sold: $${ticket.price}`,
|
|
||||||
subtitle: `for ${ticket.events?.title || 'Unknown Event'}`,
|
|
||||||
time: new Date(ticket.created_at),
|
|
||||||
icon: '🎫'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by time and take the most recent 10
|
|
||||||
activities.sort((a, b) => b.time - a.time);
|
|
||||||
return activities.slice(0, 10);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,20 +139,33 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: org, error } = await supabase
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
.from('organizations')
|
const response = await fetch(`/api/admin/organizations?id=${orgId}`, {
|
||||||
.select('*')
|
method: 'GET',
|
||||||
.eq('id', orgId)
|
credentials: 'include',
|
||||||
.single();
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
if (error) {
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch organization:', response.status, response.statusText);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return org;
|
const result = await response.json();
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Organization API error:', result.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ID parameter was provided, return the first matching organization
|
||||||
|
if (result.data && result.data.length > 0) {
|
||||||
|
return result.data.find(org => org.id === orgId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Organization error:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,30 +182,28 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: orgs, error } = await supabase
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
.from('organizations')
|
const response = await fetch('/api/admin/organizations', {
|
||||||
.select('*')
|
method: 'GET',
|
||||||
.order('created_at', { ascending: false });
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
if (error) {
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch organizations:', response.status, response.statusText);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user counts for each organization
|
const result = await response.json();
|
||||||
if (orgs) {
|
|
||||||
for (const org of orgs) {
|
if (!result.success) {
|
||||||
const { data: users } = await supabase
|
console.error('Organizations API error:', result.error);
|
||||||
.from('users')
|
return [];
|
||||||
.select('id')
|
|
||||||
.eq('organization_id', org.id);
|
|
||||||
org.user_count = users ? users.length : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return orgs || [];
|
return result.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Organizations error:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,35 +260,28 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: events, error } = await supabase
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
.from('events')
|
const response = await fetch('/api/admin/admin-events', {
|
||||||
.select(`
|
method: 'GET',
|
||||||
*,
|
credentials: 'include',
|
||||||
organizations(name),
|
headers: { 'Content-Type': 'application/json' }
|
||||||
users(name, email),
|
});
|
||||||
venues(name)
|
|
||||||
`)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch events:', response.status, response.statusText);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get ticket type counts for each event
|
const result = await response.json();
|
||||||
if (events) {
|
|
||||||
for (const event of events) {
|
if (!result.success) {
|
||||||
const { data: ticketTypes } = await supabase
|
console.error('Events API error:', result.error);
|
||||||
.from('ticket_types')
|
return [];
|
||||||
.select('id')
|
|
||||||
.eq('event_id', event.id);
|
|
||||||
event.ticket_type_count = ticketTypes ? ticketTypes.length : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return events || [];
|
return result.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Events error:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,34 +298,28 @@ export class AdminApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: tickets, error } = await supabase
|
// Use server-side API endpoint to avoid CORS issues
|
||||||
.from('tickets')
|
const response = await fetch('/api/admin/admin-tickets', {
|
||||||
.select(`
|
method: 'GET',
|
||||||
*,
|
credentials: 'include',
|
||||||
ticket_types (
|
headers: { 'Content-Type': 'application/json' }
|
||||||
name,
|
});
|
||||||
price
|
|
||||||
),
|
|
||||||
events (
|
|
||||||
title,
|
|
||||||
venue,
|
|
||||||
start_time,
|
|
||||||
organizations (
|
|
||||||
name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.order('created_at', { ascending: false })
|
|
||||||
.limit(100);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch tickets:', response.status, response.statusText);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return tickets || [];
|
const result = await response.json();
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('Tickets API error:', result.error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tickets error:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,36 @@ export class ApiRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all events for the current user/organization
|
||||||
|
*/
|
||||||
|
static async loadUserEvents(): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/events', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Failed to fetch user events:', response.status, response.statusText);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error('User events API error:', result.error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('User events error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load ticket types for an event
|
* Load ticket types for an event
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
|||||||
httpOnly: true, // JS-inaccessible for security
|
httpOnly: true, // JS-inaccessible for security
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Removed credentials: 'include' to fix CORS issues with Supabase
|
||||||
|
// Client-side operations that need auth should use API endpoints instead
|
||||||
})
|
})
|
||||||
|
|
||||||
// Service role client for server-side operations that need to bypass RLS
|
// Service role client for server-side operations that need to bypass RLS
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
import { supabase } from './supabase';
|
||||||
import type { Database } from './database.types';
|
import type { Database } from './database.types';
|
||||||
|
|
||||||
const supabase = createClient<Database>(
|
|
||||||
import.meta.env.PUBLIC_SUPABASE_URL,
|
|
||||||
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface TicketType {
|
export interface TicketType {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -66,5 +66,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|||||||
response.headers.set(key, value);
|
response.headers.set(key, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add cache-busting headers for development and API routes
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
const isApiRoute = context.url.pathname.startsWith('/api/');
|
||||||
|
|
||||||
|
if (isDevelopment || isApiRoute) {
|
||||||
|
// Prevent caching in development and for API routes
|
||||||
|
response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
|
||||||
|
response.headers.set('Pragma', 'no-cache');
|
||||||
|
response.headers.set('Expires', '0');
|
||||||
|
|
||||||
|
// Add ETag to help with cache validation
|
||||||
|
response.headers.set('ETag', `"${Date.now()}-${Math.random()}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add timestamp header for debugging cache issues
|
||||||
|
if (isDevelopment) {
|
||||||
|
response.headers.set('X-Dev-Timestamp', new Date().toISOString());
|
||||||
|
response.headers.set('X-Dev-Random', Math.random().toString(36).substring(7));
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
@@ -1,44 +1,62 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import PublicHeader from '../components/PublicHeader.astro';
|
import PublicHeader from '../components/PublicHeader.astro';
|
||||||
|
import ThemeToggle from '../components/ThemeToggle.tsx';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Page Not Found - Black Canyon Tickets">
|
<Layout title="Page Not Found - Black Canyon Tickets">
|
||||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50/30">
|
<div class="min-h-screen" style="background: var(--bg-gradient);">
|
||||||
<PublicHeader />
|
<PublicHeader />
|
||||||
|
|
||||||
|
<!-- Theme Toggle -->
|
||||||
|
<div class="fixed top-20 right-4 z-50">
|
||||||
|
<ThemeToggle client:load />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 404 Hero Section -->
|
<!-- 404 Hero Section -->
|
||||||
<section class="relative overflow-hidden min-h-screen flex items-center justify-center">
|
<section class="relative overflow-hidden min-h-screen flex items-center justify-center">
|
||||||
<!-- Animated Background -->
|
<!-- Animated Background Elements -->
|
||||||
<div class="absolute inset-0 opacity-30">
|
<div class="absolute inset-0 opacity-20">
|
||||||
<div class="absolute top-1/4 left-1/4 w-64 h-64 bg-gradient-to-br from-blue-400 to-purple-500 rounded-full blur-3xl animate-pulse"></div>
|
<div class="absolute top-20 left-20 w-64 h-64 rounded-full blur-3xl animate-pulse" style="background: var(--bg-orb-1);"></div>
|
||||||
<div class="absolute bottom-1/4 right-1/4 w-96 h-96 bg-gradient-to-br from-purple-400 to-pink-500 rounded-full blur-3xl animate-pulse delay-1000"></div>
|
<div class="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse delay-1000" style="background: var(--bg-orb-2);"></div>
|
||||||
<div class="absolute top-1/2 right-1/3 w-48 h-48 bg-gradient-to-br from-cyan-400 to-blue-500 rounded-full blur-3xl animate-pulse delay-500"></div>
|
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse delay-500" style="background: var(--bg-orb-3);"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Geometric Patterns -->
|
||||||
|
<div class="absolute inset-0" style="opacity: var(--grid-opacity, 0.1);">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5" style="color: var(--grid-pattern);"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100" height="100" fill="url(#grid)" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Floating Elements -->
|
<!-- Floating Elements -->
|
||||||
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
<div class="absolute top-20 left-20 w-8 h-8 bg-blue-200 rounded-full animate-float opacity-60"></div>
|
<div class="absolute top-20 left-20 w-8 h-8 rounded-full animate-float opacity-60" style="background: var(--glass-text-accent);"></div>
|
||||||
<div class="absolute top-40 right-32 w-6 h-6 bg-purple-200 rounded-full animate-float opacity-50" style="animation-delay: 1s;"></div>
|
<div class="absolute top-40 right-32 w-6 h-6 rounded-full animate-float opacity-50" style="background: var(--glass-text-accent); animation-delay: 1s;"></div>
|
||||||
<div class="absolute bottom-40 left-1/3 w-10 h-10 bg-pink-200 rounded-full animate-float opacity-40" style="animation-delay: 2s;"></div>
|
<div class="absolute bottom-40 left-1/3 w-10 h-10 rounded-full animate-float opacity-40" style="background: var(--glass-text-accent); animation-delay: 2s;"></div>
|
||||||
<div class="absolute bottom-20 right-20 w-12 h-12 bg-cyan-200 rounded-full animate-float opacity-70" style="animation-delay: 1.5s;"></div>
|
<div class="absolute bottom-20 right-20 w-12 h-12 rounded-full animate-float opacity-70" style="background: var(--glass-text-accent); animation-delay: 1.5s;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
<!-- 404 Illustration -->
|
<!-- 404 Illustration -->
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<div class="relative inline-block">
|
<div class="relative inline-block">
|
||||||
<!-- Large 404 Text with Gradient -->
|
<!-- Large 404 Text with Theme Colors -->
|
||||||
<h1 class="text-[12rem] sm:text-[16rem] lg:text-[20rem] font-black leading-none">
|
<h1 class="text-[12rem] sm:text-[16rem] lg:text-[20rem] font-black leading-none">
|
||||||
<span class="bg-gradient-to-br from-gray-200 via-gray-300 to-gray-400 bg-clip-text text-transparent drop-shadow-2xl">
|
<span class="bg-gradient-to-br from-gray-400 via-gray-300 to-gray-200 bg-clip-text text-transparent drop-shadow-2xl" style="color: var(--glass-text-secondary);">
|
||||||
404
|
404
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Floating Calendar Icon -->
|
<!-- Floating Calendar Icon -->
|
||||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 animate-bounce">
|
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 animate-bounce">
|
||||||
<div class="w-24 h-24 bg-gradient-to-br from-blue-600 to-purple-600 rounded-2xl shadow-2xl flex items-center justify-center transform rotate-12 hover:rotate-0 transition-transform duration-500">
|
<div class="w-24 h-24 rounded-2xl shadow-2xl flex items-center justify-center transform rotate-12 hover:rotate-0 transition-transform duration-500 backdrop-blur-lg" style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);">
|
||||||
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-12 h-12" style="color: var(--glass-text-primary);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,26 +66,28 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<h2 class="text-4xl lg:text-6xl font-light text-gray-900 mb-6 tracking-tight">
|
<h2 class="text-4xl lg:text-6xl font-light mb-6 tracking-tight" style="color: var(--glass-text-primary);">
|
||||||
Oops! Event Not Found
|
Oops! Event Not Found
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xl lg:text-2xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
|
<p class="text-xl lg:text-2xl mb-8 max-w-2xl mx-auto leading-relaxed" style="color: var(--glass-text-secondary);">
|
||||||
It seems like this page decided to skip the party. Let's get you back to where the action is.
|
It seems like this page decided to skip the party. Let's get you back to where the action is.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Search Suggestion -->
|
<!-- Search Suggestion -->
|
||||||
<div class="bg-white/70 backdrop-blur-lg border border-white/50 rounded-2xl p-8 shadow-2xl max-w-lg mx-auto mb-8">
|
<div class="backdrop-blur-xl rounded-2xl p-8 shadow-2xl max-w-lg mx-auto mb-8" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Looking for something specific?</h3>
|
<h3 class="text-lg font-semibold mb-4" style="color: var(--glass-text-primary);">Looking for something specific?</h3>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="error-search"
|
id="error-search"
|
||||||
placeholder="Search events..."
|
placeholder="Search events..."
|
||||||
class="w-full px-4 py-3 pr-12 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200"
|
class="w-full px-4 py-3 pr-12 rounded-xl transition-all duration-200 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-bg-input); border: 1px solid var(--glass-border); color: var(--glass-text-primary);"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
id="error-search-btn"
|
id="error-search-btn"
|
||||||
class="absolute right-2 top-2 p-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-200"
|
class="absolute right-2 top-2 p-2 rounded-lg transition-all duration-200 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-text-accent); color: white;"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||||
@@ -81,7 +101,8 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
||||||
<a
|
<a
|
||||||
href="/calendar"
|
href="/calendar"
|
||||||
class="group inline-flex items-center space-x-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-300"
|
class="group inline-flex items-center space-x-3 px-8 py-4 rounded-xl font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-lg"
|
||||||
|
style="background: var(--glass-text-accent); color: white;"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6 group-hover:animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 group-hover:animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
@@ -91,10 +112,11 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="group inline-flex items-center space-x-3 bg-white border-2 border-gray-200 hover:border-gray-300 text-gray-700 hover:text-gray-900 px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300"
|
class="group inline-flex items-center space-x-3 px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-lg"
|
||||||
|
style="background: var(--glass-bg-button); border: 1px solid var(--glass-border); color: var(--glass-text-primary);"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Go Home</span>
|
<span>Go Home</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -102,38 +124,42 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<!-- Popular Suggestions -->
|
<!-- Popular Suggestions -->
|
||||||
<div class="max-w-2xl mx-auto">
|
<div class="max-w-2xl mx-auto">
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-6">Or explore these popular sections:</h3>
|
<h3 class="text-lg font-semibold mb-6" style="color: var(--glass-text-primary);">Or explore these popular sections:</h3>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
<a
|
<a
|
||||||
href="/calendar?featured=true"
|
href="/calendar?featured=true"
|
||||||
class="group p-4 bg-white/50 backdrop-blur-sm border border-white/50 rounded-xl hover:bg-white/70 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300"
|
class="group p-4 rounded-xl hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-bg); border: 1px solid var(--glass-border);"
|
||||||
>
|
>
|
||||||
<div class="text-2xl mb-2 group-hover:animate-pulse">⭐</div>
|
<div class="text-2xl mb-2 group-hover:animate-pulse">⭐</div>
|
||||||
<div class="text-sm font-medium text-gray-700">Featured Events</div>
|
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Featured Events</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/calendar?category=music"
|
href="/calendar?category=music"
|
||||||
class="group p-4 bg-white/50 backdrop-blur-sm border border-white/50 rounded-xl hover:bg-white/70 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300"
|
class="group p-4 rounded-xl hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-bg); border: 1px solid var(--glass-border);"
|
||||||
>
|
>
|
||||||
<div class="text-2xl mb-2 group-hover:animate-pulse">🎵</div>
|
<div class="text-2xl mb-2 group-hover:animate-pulse">🎵</div>
|
||||||
<div class="text-sm font-medium text-gray-700">Music</div>
|
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Music</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/calendar?category=arts"
|
href="/calendar?category=arts"
|
||||||
class="group p-4 bg-white/50 backdrop-blur-sm border border-white/50 rounded-xl hover:bg-white/70 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300"
|
class="group p-4 rounded-xl hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-bg); border: 1px solid var(--glass-border);"
|
||||||
>
|
>
|
||||||
<div class="text-2xl mb-2 group-hover:animate-pulse">🎨</div>
|
<div class="text-2xl mb-2 group-hover:animate-pulse">🎨</div>
|
||||||
<div class="text-sm font-medium text-gray-700">Arts</div>
|
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Arts</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/calendar?category=community"
|
href="/calendar?category=community"
|
||||||
class="group p-4 bg-white/50 backdrop-blur-sm border border-white/50 rounded-xl hover:bg-white/70 hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300"
|
class="group p-4 rounded-xl hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-sm"
|
||||||
|
style="background: var(--glass-bg); border: 1px solid var(--glass-border);"
|
||||||
>
|
>
|
||||||
<div class="text-2xl mb-2 group-hover:animate-pulse">🤝</div>
|
<div class="text-2xl mb-2 group-hover:animate-pulse">🤝</div>
|
||||||
<div class="text-sm font-medium text-gray-700">Community</div>
|
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Community</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,15 +185,6 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% {
|
|
||||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 40px rgba(59, 130, 246, 0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 6s ease-in-out infinite;
|
animation: float 6s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@@ -176,10 +193,6 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
animation: fadeInUp 0.6s ease-out;
|
animation: fadeInUp 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse-glow {
|
|
||||||
animation: pulse-glow 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Interactive hover effects */
|
/* Interactive hover effects */
|
||||||
.hover-lift {
|
.hover-lift {
|
||||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
@@ -188,9 +201,25 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
.hover-lift:hover {
|
.hover-lift:hover {
|
||||||
transform: translateY(-8px) scale(1.02);
|
transform: translateY(-8px) scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme-aware input styles */
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--glass-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--glass-border-focus);
|
||||||
|
box-shadow: 0 0 0 3px var(--glass-border-focus-shadow);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { initializeTheme } from '../lib/theme';
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
initializeTheme();
|
||||||
|
|
||||||
// Search functionality from 404 page
|
// Search functionality from 404 page
|
||||||
const errorSearch = document.getElementById('error-search');
|
const errorSearch = document.getElementById('error-search');
|
||||||
const errorSearchBtn = document.getElementById('error-search-btn');
|
const errorSearchBtn = document.getElementById('error-search-btn');
|
||||||
|
|||||||
@@ -1,36 +1,54 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import PublicHeader from '../components/PublicHeader.astro';
|
import PublicHeader from '../components/PublicHeader.astro';
|
||||||
|
import ThemeToggle from '../components/ThemeToggle.tsx';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Server Error - Black Canyon Tickets">
|
<Layout title="Server Error - Black Canyon Tickets">
|
||||||
<div class="min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50/30">
|
<div class="min-h-screen" style="background: var(--bg-gradient);">
|
||||||
<PublicHeader />
|
<PublicHeader />
|
||||||
|
|
||||||
|
<!-- Theme Toggle -->
|
||||||
|
<div class="fixed top-20 right-4 z-50">
|
||||||
|
<ThemeToggle client:load />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 500 Hero Section -->
|
<!-- 500 Hero Section -->
|
||||||
<section class="relative overflow-hidden min-h-screen flex items-center justify-center">
|
<section class="relative overflow-hidden min-h-screen flex items-center justify-center">
|
||||||
<!-- Animated Background -->
|
<!-- Animated Background Elements -->
|
||||||
<div class="absolute inset-0 opacity-20">
|
<div class="absolute inset-0 opacity-20">
|
||||||
<div class="absolute top-1/4 left-1/4 w-64 h-64 bg-gradient-to-br from-red-400 to-orange-500 rounded-full blur-3xl animate-pulse"></div>
|
<div class="absolute top-20 left-20 w-64 h-64 rounded-full blur-3xl animate-pulse" style="background: var(--bg-orb-1);"></div>
|
||||||
<div class="absolute bottom-1/4 right-1/4 w-96 h-96 bg-gradient-to-br from-orange-400 to-red-500 rounded-full blur-3xl animate-pulse delay-1000"></div>
|
<div class="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse delay-1000" style="background: var(--bg-orb-2);"></div>
|
||||||
<div class="absolute top-1/2 right-1/3 w-48 h-48 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-full blur-3xl animate-pulse delay-500"></div>
|
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse delay-500" style="background: var(--bg-orb-3);"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Geometric Patterns -->
|
||||||
|
<div class="absolute inset-0" style="opacity: var(--grid-opacity, 0.1);">
|
||||||
|
<svg class="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.5" style="color: var(--grid-pattern);"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect width="100" height="100" fill="url(#grid)" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
<!-- Error Illustration -->
|
<!-- Error Illustration -->
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<div class="relative inline-block">
|
<div class="relative inline-block">
|
||||||
<!-- Large 500 Text -->
|
<!-- Large 500 Text with Theme Colors -->
|
||||||
<h1 class="text-[8rem] sm:text-[12rem] lg:text-[16rem] font-black leading-none">
|
<h1 class="text-[8rem] sm:text-[12rem] lg:text-[16rem] font-black leading-none">
|
||||||
<span class="bg-gradient-to-br from-red-200 via-orange-300 to-red-400 bg-clip-text text-transparent drop-shadow-2xl">
|
<span class="bg-gradient-to-br from-red-400 via-orange-300 to-red-300 bg-clip-text text-transparent drop-shadow-2xl" style="color: var(--glass-text-secondary);">
|
||||||
500
|
500
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Floating Warning Icon -->
|
<!-- Floating Warning Icon -->
|
||||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 animate-bounce">
|
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 animate-bounce">
|
||||||
<div class="w-24 h-24 bg-gradient-to-br from-red-600 to-orange-600 rounded-2xl shadow-2xl flex items-center justify-center transform rotate-12 hover:rotate-0 transition-transform duration-500">
|
<div class="w-24 h-24 rounded-2xl shadow-2xl flex items-center justify-center transform rotate-12 hover:rotate-0 transition-transform duration-500 backdrop-blur-lg" style="background: var(--glass-bg-button); border: 1px solid var(--glass-border);">
|
||||||
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-12 h-12" style="color: var(--error-color, #ef4444);" 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 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
<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 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,24 +58,24 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<!-- Error Message -->
|
<!-- Error Message -->
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<h2 class="text-4xl lg:text-6xl font-light text-gray-900 mb-6 tracking-tight">
|
<h2 class="text-4xl lg:text-6xl font-light mb-6 tracking-tight" style="color: var(--glass-text-primary);">
|
||||||
Something Went Wrong
|
Something Went Wrong
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xl lg:text-2xl text-gray-600 mb-8 max-w-2xl mx-auto leading-relaxed">
|
<p class="text-xl lg:text-2xl mb-8 max-w-2xl mx-auto leading-relaxed" style="color: var(--glass-text-secondary);">
|
||||||
Our servers are experiencing some technical difficulties. Don't worry, our team has been notified and is working to fix this.
|
Our servers are experiencing some technical difficulties. Don't worry, our team has been notified and is working to fix this.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Status Card -->
|
<!-- Status Card -->
|
||||||
<div class="bg-white/70 backdrop-blur-lg border border-red-200/50 rounded-2xl p-8 shadow-2xl max-w-lg mx-auto mb-8">
|
<div class="backdrop-blur-xl rounded-2xl p-8 shadow-2xl max-w-lg mx-auto mb-8" style="background: var(--glass-bg-lg); border: 1px solid var(--glass-border);">
|
||||||
<div class="flex items-center justify-center space-x-3 mb-4">
|
<div class="flex items-center justify-center space-x-3 mb-4">
|
||||||
<div class="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
<div class="w-3 h-3 rounded-full animate-pulse" style="background: var(--error-color, #ef4444);"></div>
|
||||||
<span class="text-lg font-semibold text-gray-900">Server Status</span>
|
<span class="text-lg font-semibold" style="color: var(--glass-text-primary);">Server Status</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-600 mb-4">
|
<p class="mb-4" style="color: var(--glass-text-secondary);">
|
||||||
We're working hard to restore full functionality. This is usually resolved within a few minutes.
|
We're working hard to restore full functionality. This is usually resolved within a few minutes.
|
||||||
</p>
|
</p>
|
||||||
<div class="text-sm text-gray-500">
|
<div class="text-sm" style="color: var(--glass-text-tertiary);">
|
||||||
Error Code: <span class="font-mono bg-gray-100 px-2 py-1 rounded">TEMP_500</span>
|
Error Code: <span class="font-mono px-2 py-1 rounded backdrop-blur-sm" style="background: var(--glass-bg); color: var(--glass-text-primary);">TEMP_500</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +84,8 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
|
||||||
<button
|
<button
|
||||||
onclick="window.location.reload()"
|
onclick="window.location.reload()"
|
||||||
class="group inline-flex items-center space-x-3 bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-300"
|
class="group inline-flex items-center space-x-3 px-8 py-4 rounded-xl font-semibold text-lg shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-lg"
|
||||||
|
style="background: var(--glass-text-accent); color: white;"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6 group-hover:animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 group-hover:animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
@@ -76,10 +95,11 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="group inline-flex items-center space-x-3 bg-white border-2 border-gray-200 hover:border-gray-300 text-gray-700 hover:text-gray-900 px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300"
|
class="group inline-flex items-center space-x-3 px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all duration-300 backdrop-blur-lg"
|
||||||
|
style="background: var(--glass-bg-button); border: 1px solid var(--glass-border); color: var(--glass-text-primary);"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Go Home</span>
|
<span>Go Home</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -87,14 +107,15 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
|
|
||||||
<!-- Support Contact -->
|
<!-- Support Contact -->
|
||||||
<div class="max-w-lg mx-auto">
|
<div class="max-w-lg mx-auto">
|
||||||
<div class="bg-gradient-to-r from-gray-50 to-gray-100 border border-gray-200 rounded-2xl p-6">
|
<div class="backdrop-blur-xl rounded-2xl p-6" style="background: var(--glass-bg); border: 1px solid var(--glass-border);">
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-3">Need Immediate Help?</h3>
|
<h3 class="text-lg font-semibold mb-3" style="color: var(--glass-text-primary);">Need Immediate Help?</h3>
|
||||||
<p class="text-gray-600 mb-4 text-sm">
|
<p class="mb-4 text-sm" style="color: var(--glass-text-secondary);">
|
||||||
If this error persists, please reach out to our support team.
|
If this error persists, please reach out to our support team.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/support"
|
href="/support"
|
||||||
class="inline-flex items-center space-x-2 text-blue-600 hover:text-blue-700 font-medium transition-colors"
|
class="inline-flex items-center space-x-2 font-medium transition-colors hover:opacity-80"
|
||||||
|
style="color: var(--glass-text-accent);"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||||
@@ -132,9 +153,23 @@ import PublicHeader from '../components/PublicHeader.astro';
|
|||||||
.animate-fade-in-up {
|
.animate-fade-in-up {
|
||||||
animation: fadeInUp 0.6s ease-out;
|
animation: fadeInUp 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Interactive hover effects */
|
||||||
|
.hover-lift {
|
||||||
|
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { initializeTheme } from '../lib/theme';
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
initializeTheme();
|
||||||
|
|
||||||
// Auto-retry functionality
|
// Auto-retry functionality
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 3;
|
const maxRetries = 3;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
import Navigation from '../../components/Navigation.astro';
|
||||||
import { createSupabaseServerClient } from '../../lib/supabase-ssr';
|
import { createSupabaseServerClient } from '../../lib/supabase-ssr';
|
||||||
|
|
||||||
// Enable server-side rendering for auth checks
|
// Enable server-side rendering for auth checks
|
||||||
@@ -61,46 +62,35 @@ const auth = {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sticky Navigation -->
|
<!-- Modern Navigation Component -->
|
||||||
<nav class="sticky top-0 z-50 bg-white/10 backdrop-blur-xl shadow-xl border-b border-white/20">
|
<Navigation title="Admin Dashboard" />
|
||||||
|
|
||||||
|
<!-- Admin-specific navigation bar -->
|
||||||
|
<div class="bg-red-50 border-b border-red-200">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex justify-between h-20">
|
<div class="flex justify-between items-center h-12">
|
||||||
<div class="flex items-center space-x-8">
|
|
||||||
<a href="/admin/dashboard" class="flex items-center">
|
|
||||||
<span class="text-xl font-light text-white">
|
|
||||||
<span class="font-bold">P</span>ortal
|
|
||||||
</span>
|
|
||||||
<span class="ml-2 px-2 py-1 bg-white/20 text-white rounded-md text-xs font-medium">Admin</span>
|
|
||||||
</a>
|
|
||||||
<div class="hidden md:flex items-center space-x-6">
|
|
||||||
<span class="text-white font-semibold">Admin Dashboard</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="text-xs font-medium text-red-800 bg-red-100 px-2 py-1 rounded">Admin Mode</span>
|
||||||
|
<span class="text-sm text-red-700">Platform Administration</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
<a
|
<a
|
||||||
id="super-admin-link"
|
id="super-admin-link"
|
||||||
href="/admin/super-dashboard"
|
href="/admin/super-dashboard"
|
||||||
class="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 hover:shadow-md hover:scale-105 hidden"
|
class="bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white px-3 py-1 rounded-lg text-xs font-medium transition-all duration-200 hover:shadow-md hover:scale-105 hidden"
|
||||||
>
|
>
|
||||||
Super Admin
|
Super Admin
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 hover:shadow-md hover:scale-105"
|
class="bg-white border border-red-200 hover:bg-red-50 text-red-700 px-3 py-1 rounded-lg text-xs font-medium transition-all duration-200"
|
||||||
>
|
>
|
||||||
Organizer View
|
Organizer View
|
||||||
</a>
|
</a>
|
||||||
<span id="user-name" class="text-sm text-white font-medium"></span>
|
|
||||||
<button
|
|
||||||
id="logout-btn"
|
|
||||||
class="bg-white/10 backdrop-blur-xl border border-white/20 hover:bg-white/20 text-white px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 hover:shadow-md hover:scale-105"
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
|
|
||||||
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8 relative z-10">
|
<main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8 relative z-10">
|
||||||
<div class="px-4 py-6 sm:px-0">
|
<div class="px-4 py-6 sm:px-0">
|
||||||
@@ -281,8 +271,6 @@ const auth = {
|
|||||||
<script>
|
<script>
|
||||||
import { adminApi } from '../../lib/admin-api-router';
|
import { adminApi } from '../../lib/admin-api-router';
|
||||||
|
|
||||||
const userNameSpan = document.getElementById('user-name');
|
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
|
||||||
const statsContainer = document.getElementById('stats-container');
|
const statsContainer = document.getElementById('stats-container');
|
||||||
|
|
||||||
let currentTab = 'overview';
|
let currentTab = 'overview';
|
||||||
@@ -296,11 +284,6 @@ const auth = {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userInfo = adminApi.getUserInfo();
|
|
||||||
if (userInfo && userNameSpan) {
|
|
||||||
userNameSpan.textContent = userInfo.name || userInfo.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has super admin privileges
|
// Check if user has super admin privileges
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/admin/check-super-admin', {
|
const response = await fetch('/api/admin/check-super-admin', {
|
||||||
@@ -945,7 +928,7 @@ const auth = {
|
|||||||
<span class="text-sm font-medium text-white">$${ticket.price}</span>
|
<span class="text-sm font-medium text-white">$${ticket.price}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4">
|
<td class="py-3 px-4">
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ticket.checked_in ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
|
<span class="${ticket.checked_in ? 'ticket-status-checked-in' : 'ticket-status-pending'}">
|
||||||
${ticket.checked_in ? 'Checked In' : 'Not Checked In'}
|
${ticket.checked_in ? 'Checked In' : 'Not Checked In'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -970,7 +953,7 @@ const auth = {
|
|||||||
<h4 class="text-lg font-medium text-white mb-1">${ticket.events?.title || 'Unknown Event'}</h4>
|
<h4 class="text-lg font-medium text-white mb-1">${ticket.events?.title || 'Unknown Event'}</h4>
|
||||||
<p class="text-sm text-white/80 font-mono">#${ticket.uuid.substring(0, 8)}...</p>
|
<p class="text-sm text-white/80 font-mono">#${ticket.uuid.substring(0, 8)}...</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="px-2 py-1 text-xs font-semibold rounded-full ${ticket.checked_in ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
|
<span class="${ticket.checked_in ? 'ticket-status-checked-in' : 'ticket-status-pending'}">
|
||||||
${ticket.checked_in ? 'Checked In' : 'Pending'}
|
${ticket.checked_in ? 'Checked In' : 'Pending'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1250,12 +1233,6 @@ const auth = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (logoutBtn) {
|
|
||||||
logoutBtn.addEventListener('click', async () => {
|
|
||||||
await adminApi.signOut();
|
|
||||||
window.location.href = '/';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fee management functions
|
// Fee management functions
|
||||||
function setupFeeFormListeners() {
|
function setupFeeFormListeners() {
|
||||||
@@ -1506,21 +1483,20 @@ const auth = {
|
|||||||
`;
|
`;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
// Get platform data
|
// Get platform data using server-side API to avoid CORS issues
|
||||||
const supabase = adminApi.getSupabaseClient();
|
|
||||||
const [eventsResult, usersResult, ticketsResult, orgsResult] = await Promise.all([
|
const [eventsResult, usersResult, ticketsResult, orgsResult] = await Promise.all([
|
||||||
supabase.from('events').select('*'),
|
adminApi.getEvents(),
|
||||||
supabase.from('users').select('*'),
|
adminApi.getUsers(),
|
||||||
supabase.from('tickets').select('*'),
|
adminApi.getTickets(),
|
||||||
supabase.from('organizations').select('*')
|
adminApi.getOrganizations()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create CSV content
|
// Create CSV content
|
||||||
const csvData = {
|
const csvData = {
|
||||||
events: eventsResult.data || [],
|
events: eventsResult || [],
|
||||||
users: usersResult.data || [],
|
users: usersResult || [],
|
||||||
tickets: ticketsResult.data || [],
|
tickets: ticketsResult || [],
|
||||||
organizations: orgsResult.data || []
|
organizations: orgsResult || []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create summary report
|
// Create summary report
|
||||||
|
|||||||
@@ -308,31 +308,11 @@ const search = url.searchParams.get('search');
|
|||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Force dark mode for this page - no theme toggle allowed
|
// Default to dark theme for better glassmorphism design
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
const preferredTheme = localStorage.getItem('theme') || 'dark';
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.setAttribute('data-theme', preferredTheme);
|
||||||
document.documentElement.classList.remove('light');
|
document.documentElement.classList.add(preferredTheme);
|
||||||
|
document.documentElement.classList.remove(preferredTheme === 'dark' ? 'light' : 'dark');
|
||||||
// Override any global theme logic for this page
|
|
||||||
(window as any).__FORCE_DARK_MODE__ = true;
|
|
||||||
|
|
||||||
// Prevent theme changes on this page
|
|
||||||
if (window.localStorage) {
|
|
||||||
const originalTheme = localStorage.getItem('theme');
|
|
||||||
if (originalTheme && originalTheme !== 'dark') {
|
|
||||||
sessionStorage.setItem('originalTheme', originalTheme);
|
|
||||||
}
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block any theme toggle attempts
|
|
||||||
window.addEventListener('themeChanged', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.documentElement.classList.remove('light');
|
|
||||||
}, true);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -634,6 +614,11 @@ const search = url.searchParams.get('search');
|
|||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initializeLocation();
|
initializeLocation();
|
||||||
|
|
||||||
|
// Load components immediately with empty data if no location
|
||||||
|
if (!userLocation) {
|
||||||
|
loadComponents();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import PublicHeader from '../components/PublicHeader.astro';
|
import PublicHeader from '../components/PublicHeader.astro';
|
||||||
import { verifyAuth } from '../lib/auth';
|
|
||||||
|
|
||||||
// Enable server-side rendering for auth checks
|
// Enable server-side rendering for dynamic content
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
// Required authentication check for calendar access
|
|
||||||
const auth = await verifyAuth(Astro.request);
|
|
||||||
if (!auth) {
|
|
||||||
return Astro.redirect('/login-new');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get query parameters for filtering
|
// Get query parameters for filtering
|
||||||
const url = new URL(Astro.request.url);
|
const url = new URL(Astro.request.url);
|
||||||
const featured = url.searchParams.get('featured');
|
const featured = url.searchParams.get('featured');
|
||||||
@@ -147,7 +140,7 @@ const search = url.searchParams.get('search');
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Premium Filter Controls -->
|
<!-- Premium Filter Controls -->
|
||||||
<section class="sticky top-0 z-50 backdrop-blur-xl shadow-2xl" data-filter-controls style="background: var(--glass-bg-lg); border-bottom: 1px solid var(--glass-border);">
|
<section class="sticky backdrop-blur-xl shadow-2xl" data-filter-controls style="background: var(--glass-bg-lg); border-bottom: 1px solid var(--glass-border); top: var(--hero-height, 0px); z-index: 45;">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
||||||
<!-- View Toggle - Premium Design -->
|
<!-- View Toggle - Premium Design -->
|
||||||
@@ -512,7 +505,8 @@ const search = url.searchParams.get('search');
|
|||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
if (savedTheme) return savedTheme;
|
if (savedTheme) return savedTheme;
|
||||||
|
|
||||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
// Default to dark theme for better glassmorphism design
|
||||||
|
return 'dark';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
@@ -1623,7 +1617,7 @@ const search = url.searchParams.get('search');
|
|||||||
|
|
||||||
// Old theme toggle code removed - using simpler onclick approach
|
// Old theme toggle code removed - using simpler onclick approach
|
||||||
|
|
||||||
// Smooth sticky header behavior
|
// Simplified sticky header - just keep hero visible
|
||||||
window.initStickyHeader = function initStickyHeader() {
|
window.initStickyHeader = function initStickyHeader() {
|
||||||
const heroSection = document.getElementById('hero-section');
|
const heroSection = document.getElementById('hero-section');
|
||||||
const filterControls = document.querySelector('[data-filter-controls]');
|
const filterControls = document.querySelector('[data-filter-controls]');
|
||||||
@@ -1634,63 +1628,21 @@ const search = url.searchParams.get('search');
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add smooth transition styles
|
// Calculate hero section height
|
||||||
heroSection.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
|
|
||||||
|
|
||||||
let lastScrollY = window.scrollY;
|
|
||||||
let isTransitioning = false;
|
|
||||||
|
|
||||||
function handleScroll() {
|
|
||||||
const currentScrollY = window.scrollY;
|
|
||||||
const heroHeight = heroSection.offsetHeight;
|
const heroHeight = heroSection.offsetHeight;
|
||||||
const filterControlsOffsetTop = filterControls.offsetTop;
|
|
||||||
|
|
||||||
// Calculate transition point - when filter controls should take over
|
// Set CSS variable for filter controls positioning
|
||||||
const transitionThreshold = filterControlsOffsetTop - heroHeight;
|
document.documentElement.style.setProperty('--hero-height', `${heroHeight}px`);
|
||||||
|
|
||||||
if (currentScrollY >= transitionThreshold) {
|
// Ensure hero section stays visible with proper styling
|
||||||
// Smoothly transition hero out and let filter controls take over
|
|
||||||
if (!isTransitioning) {
|
|
||||||
isTransitioning = true;
|
|
||||||
heroSection.style.transform = 'translateY(-100%)';
|
|
||||||
heroSection.style.opacity = '0.8';
|
|
||||||
heroSection.style.zIndex = '20'; // Below filter controls (z-50)
|
|
||||||
|
|
||||||
// After transition, change position to avoid layout issues
|
|
||||||
setTimeout(() => {
|
|
||||||
heroSection.style.position = 'relative';
|
|
||||||
heroSection.style.top = 'auto';
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Hero section is visible and sticky
|
|
||||||
if (isTransitioning) {
|
|
||||||
isTransitioning = false;
|
|
||||||
heroSection.style.position = 'sticky';
|
heroSection.style.position = 'sticky';
|
||||||
heroSection.style.top = '0px';
|
heroSection.style.top = '0px';
|
||||||
|
heroSection.style.zIndex = '40';
|
||||||
heroSection.style.transform = 'translateY(0)';
|
heroSection.style.transform = 'translateY(0)';
|
||||||
heroSection.style.opacity = '1';
|
heroSection.style.opacity = '1';
|
||||||
heroSection.style.zIndex = '40'; // Above content but below filter controls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastScrollY = currentScrollY;
|
console.log('Hero section initialized and kept visible. Height:', heroHeight);
|
||||||
}
|
console.log('Filter controls positioned below hero at:', `${heroHeight}px`);
|
||||||
|
|
||||||
// Add scroll listener with throttling for performance
|
|
||||||
let ticking = false;
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (!ticking) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
handleScroll();
|
|
||||||
ticking = false;
|
|
||||||
});
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
// Initial call
|
|
||||||
handleScroll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize sticky header
|
// Initialize sticky header
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { verifyAuth } from '../../../lib/auth';
|
|||||||
// Server-side authentication check using cookies
|
// Server-side authentication check using cookies
|
||||||
const auth = await verifyAuth(Astro.cookies);
|
const auth = await verifyAuth(Astro.cookies);
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return Astro.redirect('/login-new');
|
// Store the current URL to redirect back after login
|
||||||
|
const currentUrl = Astro.url.pathname;
|
||||||
|
return Astro.redirect(`/login-new?redirect=${encodeURIComponent(currentUrl)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get event ID from URL parameters
|
// Get event ID from URL parameters
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { verifyAuth } from '../../lib/auth';
|
|||||||
// Enable server-side rendering for auth checks
|
// Enable server-side rendering for auth checks
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
// Server-side authentication check
|
// Server-side auth check using cookies for better SSR compatibility
|
||||||
const auth = await verifyAuth(Astro.request);
|
const auth = await verifyAuth(Astro.cookies);
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return Astro.redirect('/login-new');
|
return Astro.redirect('/login-new');
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,15 @@ if (!auth) {
|
|||||||
|
|
||||||
// Load user data (auth already verified server-side)
|
// Load user data (auth already verified server-side)
|
||||||
async function loadUserData() {
|
async function loadUserData() {
|
||||||
const { data: { user: authUser } } = await supabase.auth.getUser();
|
try {
|
||||||
|
// Try getSession first, then getUser as fallback
|
||||||
|
const { data: session } = await supabase.auth.getSession();
|
||||||
|
let authUser = session?.user;
|
||||||
|
|
||||||
|
if (!authUser) {
|
||||||
|
const { data: { user: userData } } = await supabase.auth.getUser();
|
||||||
|
authUser = userData;
|
||||||
|
}
|
||||||
|
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
// Silently handle client-side auth failure - user might be logged out
|
// Silently handle client-side auth failure - user might be logged out
|
||||||
@@ -344,6 +352,11 @@ if (!auth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return authUser;
|
return authUser;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth error:', error);
|
||||||
|
window.location.href = '/login-new';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate slug from title
|
// Generate slug from title
|
||||||
@@ -505,7 +518,15 @@ if (!auth) {
|
|||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (eventError) throw eventError;
|
if (eventError) {
|
||||||
|
console.error('Event creation error:', eventError);
|
||||||
|
throw eventError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
console.error('Event creation returned null data');
|
||||||
|
throw new Error('Event creation failed - no data returned');
|
||||||
|
}
|
||||||
|
|
||||||
// Premium add-ons will be handled in future updates
|
// Premium add-ons will be handled in future updates
|
||||||
|
|
||||||
@@ -513,8 +534,22 @@ if (!auth) {
|
|||||||
window.location.href = `/events/${event.id}/manage`;
|
window.location.href = `/events/${event.id}/manage`;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle errors gracefully without exposing details
|
console.error('Event creation error:', error);
|
||||||
errorMessage.textContent = 'An error occurred creating the event. Please try again.';
|
|
||||||
|
// Show specific error message if available
|
||||||
|
let message = 'An error occurred creating the event. Please try again.';
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message?.includes('slug')) {
|
||||||
|
message = 'An event with this title already exists. Please choose a different title.';
|
||||||
|
} else if (error.message?.includes('organization')) {
|
||||||
|
message = 'Organization access error. Please try logging out and back in.';
|
||||||
|
} else if (error.message?.includes('venue')) {
|
||||||
|
message = 'Please select or enter a venue for your event.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage.textContent = message;
|
||||||
errorMessage.classList.remove('hidden');
|
errorMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,8 +130,10 @@ import LoginLayout from '../layouts/LoginLayout.astro';
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok && data.success) {
|
if (response.ok && data.success) {
|
||||||
// Login successful, redirect to dashboard
|
// Login successful, redirect to intended destination or dashboard
|
||||||
window.location.href = data.redirectTo || '/dashboard';
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const redirectTo = urlParams.get('redirect') || data.redirectTo || '/dashboard';
|
||||||
|
window.location.href = redirectTo;
|
||||||
} else {
|
} else {
|
||||||
// Show error message
|
// Show error message
|
||||||
errorMessage.textContent = data.error || 'Login failed. Please try again.';
|
errorMessage.textContent = data.error || 'Login failed. Please try again.';
|
||||||
|
|||||||
@@ -1176,6 +1176,31 @@ nav a:hover {
|
|||||||
border: 1px solid var(--error-border);
|
border: 1px solid var(--error-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ticket Status Badge Classes */
|
||||||
|
.ticket-status-checked-in {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--success-color);
|
||||||
|
background: var(--success-bg);
|
||||||
|
border: 1px solid var(--success-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-status-pending {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--warning-color);
|
||||||
|
background: var(--warning-bg);
|
||||||
|
border: 1px solid var(--warning-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* Range Slider Styling for Glassmorphism */
|
/* Range Slider Styling for Glassmorphism */
|
||||||
.slider-thumb {
|
.slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|||||||
132
test-buttons-auth.cjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Button Functionality with Auth Bypass', () => {
|
||||||
|
test('should test buttons with direct component access', async ({ page }) => {
|
||||||
|
console.log('Testing button functionality...');
|
||||||
|
|
||||||
|
// Navigate directly to manage page and check what happens
|
||||||
|
await page.goto('http://localhost:3000/events/test/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`Current URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
// Take screenshot
|
||||||
|
await page.screenshot({ path: 'manage-page-debug.png', fullPage: true });
|
||||||
|
|
||||||
|
// Check what's actually loaded on the page
|
||||||
|
const pageContent = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
title: document.title,
|
||||||
|
hasReactScript: !!document.querySelector('script[src*="react"]'),
|
||||||
|
hasAstroScript: !!document.querySelector('script[src*="astro"]'),
|
||||||
|
hasEmbedModalWrapper: !!document.querySelector('embed-modal-wrapper'),
|
||||||
|
reactRoots: document.querySelectorAll('[data-reactroot]').length,
|
||||||
|
allScripts: Array.from(document.querySelectorAll('script[src]')).map(s => s.src.split('/').pop()),
|
||||||
|
embedButton: !!document.getElementById('embed-code-btn'),
|
||||||
|
hasClientLoad: !!document.querySelector('[data-astro-cid]'),
|
||||||
|
bodyHTML: document.body.innerHTML.substring(0, 1000) // First 1000 chars
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Page analysis:', JSON.stringify(pageContent, null, 2));
|
||||||
|
|
||||||
|
// Check for console errors during page load
|
||||||
|
const errors = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(msg.text());
|
||||||
|
console.log('Console error:', msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
errors.push(error.message);
|
||||||
|
console.log('Page error:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to directly access a manage page that might not require auth
|
||||||
|
await page.goto('http://localhost:3000/dashboard');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
console.log('Dashboard URL:', page.url());
|
||||||
|
await page.screenshot({ path: 'dashboard-debug.png', fullPage: true });
|
||||||
|
|
||||||
|
// Check if we can access any page that has buttons
|
||||||
|
await page.goto('http://localhost:3000/');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
console.log('Homepage URL:', page.url());
|
||||||
|
await page.screenshot({ path: 'homepage-debug.png', fullPage: true });
|
||||||
|
|
||||||
|
// Look for any buttons on homepage
|
||||||
|
const homeButtons = await page.locator('button').count();
|
||||||
|
console.log(`Homepage buttons found: ${homeButtons}`);
|
||||||
|
|
||||||
|
if (homeButtons > 0) {
|
||||||
|
const buttonTexts = await page.locator('button').allTextContents();
|
||||||
|
console.log('Homepage button texts:', buttonTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for React components on homepage
|
||||||
|
const homeReactInfo = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
reactElements: document.querySelectorAll('[data-reactroot], [data-react-class]').length,
|
||||||
|
clientLoadElements: document.querySelectorAll('[client\\:load]').length,
|
||||||
|
astroIslands: document.querySelectorAll('astro-island').length,
|
||||||
|
embedModalWrapper: !!document.querySelector('embed-modal-wrapper')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Homepage React info:', homeReactInfo);
|
||||||
|
|
||||||
|
console.log('All errors found:', errors);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should check specific component loading issues', async ({ page }) => {
|
||||||
|
console.log('Checking component loading...');
|
||||||
|
|
||||||
|
// Navigate to homepage first
|
||||||
|
await page.goto('http://localhost:3000/');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Wait for potential React hydration
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Check if React is loaded
|
||||||
|
const reactStatus = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
hasReact: typeof React !== 'undefined',
|
||||||
|
hasReactDOM: typeof ReactDOM !== 'undefined',
|
||||||
|
windowKeys: Object.keys(window).filter(key => key.toLowerCase().includes('react')),
|
||||||
|
astroIslands: Array.from(document.querySelectorAll('astro-island')).map(island => ({
|
||||||
|
component: island.getAttribute('component-export'),
|
||||||
|
clientDirective: island.getAttribute('client'),
|
||||||
|
props: island.getAttribute('props')
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('React status:', JSON.stringify(reactStatus, null, 2));
|
||||||
|
|
||||||
|
// Try to manually test if we can create the embed modal
|
||||||
|
const manualTest = await page.evaluate(() => {
|
||||||
|
// Try to simulate what should happen when embed button is clicked
|
||||||
|
const embedBtn = document.getElementById('embed-code-btn');
|
||||||
|
if (embedBtn) {
|
||||||
|
console.log('Found embed button');
|
||||||
|
return {
|
||||||
|
embedButtonExists: true,
|
||||||
|
hasClickListener: embedBtn.onclick !== null,
|
||||||
|
buttonHTML: embedBtn.outerHTML
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { embedButtonExists: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Manual embed test:', manualTest);
|
||||||
|
});
|
||||||
|
});
|
||||||
156
test-buttons.cjs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Button Functionality Tests', () => {
|
||||||
|
test('should test ticket pricing buttons', async ({ page }) => {
|
||||||
|
console.log('Starting button functionality test...');
|
||||||
|
|
||||||
|
// Navigate to a manage page (assuming there's a test event)
|
||||||
|
// First, let's try to login and then go to manage page
|
||||||
|
await page.goto('http://localhost:3000/login-new');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Take screenshot of login page
|
||||||
|
await page.screenshot({ path: 'login-page.png', fullPage: true });
|
||||||
|
console.log('Login page screenshot saved');
|
||||||
|
|
||||||
|
// Check if we can find any buttons on the current page
|
||||||
|
const buttons = await page.locator('button').count();
|
||||||
|
console.log(`Found ${buttons} buttons on login page`);
|
||||||
|
|
||||||
|
// Try to navigate directly to manage page (might redirect if not authenticated)
|
||||||
|
await page.goto('http://localhost:3000/events/test/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(2000); // Wait for any React components to load
|
||||||
|
|
||||||
|
// Take screenshot of manage page
|
||||||
|
await page.screenshot({ path: 'manage-page.png', fullPage: true });
|
||||||
|
console.log('Manage page screenshot saved');
|
||||||
|
|
||||||
|
// Check if we're redirected to login
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`Current URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
if (currentUrl.includes('/login')) {
|
||||||
|
console.log('Redirected to login - need authentication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for ticket pricing buttons
|
||||||
|
const addTicketButton = page.locator('button').filter({ hasText: 'Add Ticket Type' });
|
||||||
|
const createFirstTicketButton = page.locator('button').filter({ hasText: 'Create Your First Ticket Type' });
|
||||||
|
const embedButton = page.locator('button#embed-code-btn');
|
||||||
|
|
||||||
|
console.log('Checking for ticket buttons...');
|
||||||
|
|
||||||
|
// Count all buttons on manage page
|
||||||
|
const allButtons = await page.locator('button').count();
|
||||||
|
console.log(`Found ${allButtons} total buttons on manage page`);
|
||||||
|
|
||||||
|
// Get all button texts
|
||||||
|
const buttonTexts = await page.locator('button').allTextContents();
|
||||||
|
console.log('Button texts found:', buttonTexts);
|
||||||
|
|
||||||
|
// Test embed button
|
||||||
|
const embedButtonExists = await embedButton.count();
|
||||||
|
console.log(`Embed button exists: ${embedButtonExists > 0}`);
|
||||||
|
|
||||||
|
if (embedButtonExists > 0) {
|
||||||
|
console.log('Testing embed button click...');
|
||||||
|
|
||||||
|
// Click embed button and check for modal
|
||||||
|
await embedButton.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for embed modal
|
||||||
|
const modal = page.locator('[data-testid="embed-modal"], .modal, [role="dialog"]');
|
||||||
|
const modalExists = await modal.count();
|
||||||
|
console.log(`Modal appeared after embed button click: ${modalExists > 0}`);
|
||||||
|
|
||||||
|
// Take screenshot after button click
|
||||||
|
await page.screenshot({ path: 'after-embed-click.png', fullPage: true });
|
||||||
|
console.log('Screenshot after embed button click saved');
|
||||||
|
|
||||||
|
// Check console errors
|
||||||
|
const consoleMessages = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
consoleMessages.push(msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Console errors:', consoleMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ticket pricing buttons if they exist
|
||||||
|
const addTicketExists = await addTicketButton.count();
|
||||||
|
console.log(`Add Ticket Type button exists: ${addTicketExists > 0}`);
|
||||||
|
|
||||||
|
if (addTicketExists > 0) {
|
||||||
|
console.log('Testing Add Ticket Type button...');
|
||||||
|
await addTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for ticket modal
|
||||||
|
const ticketModal = page.locator('[data-testid="ticket-modal"], .modal, [role="dialog"]');
|
||||||
|
const ticketModalExists = await ticketModal.count();
|
||||||
|
console.log(`Ticket modal appeared: ${ticketModalExists > 0}`);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-ticket-button-click.png', fullPage: true });
|
||||||
|
console.log('Screenshot after ticket button click saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also test "Create Your First Ticket Type" button
|
||||||
|
const createFirstExists = await createFirstTicketButton.count();
|
||||||
|
console.log(`Create First Ticket button exists: ${createFirstExists > 0}`);
|
||||||
|
|
||||||
|
if (createFirstExists > 0) {
|
||||||
|
console.log('Testing Create Your First Ticket Type button...');
|
||||||
|
await createFirstTicketButton.click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-create-first-ticket-click.png', fullPage: true });
|
||||||
|
console.log('Screenshot after create first ticket click saved');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should check for JavaScript errors', async ({ page }) => {
|
||||||
|
const errors = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
errors.push(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/events/test/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(3000); // Wait for all JS to load
|
||||||
|
|
||||||
|
console.log('JavaScript errors found:', errors);
|
||||||
|
|
||||||
|
// Try to access window.openEmbedModal
|
||||||
|
const hasOpenEmbedModal = await page.evaluate(() => {
|
||||||
|
return typeof window.openEmbedModal === 'function';
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('window.openEmbedModal function exists:', hasOpenEmbedModal);
|
||||||
|
|
||||||
|
// Check if React components are mounted
|
||||||
|
const reactComponents = await page.evaluate(() => {
|
||||||
|
const components = [];
|
||||||
|
const elements = document.querySelectorAll('[data-reactroot], [data-react-class]');
|
||||||
|
components.push(`React elements found: ${elements.length}`);
|
||||||
|
|
||||||
|
// Check for our specific components
|
||||||
|
const embedWrapper = document.querySelector('embed-modal-wrapper, [class*="EmbedModal"]');
|
||||||
|
components.push(`EmbedModalWrapper found: ${!!embedWrapper}`);
|
||||||
|
|
||||||
|
return components;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('React component status:', reactComponents);
|
||||||
|
});
|
||||||
|
});
|
||||||
106
test-calendar-diagnosis.cjs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Calendar Page Diagnosis', () => {
|
||||||
|
test('should open calendar page and take screenshot', async ({ page }) => {
|
||||||
|
console.log('Opening calendar page...');
|
||||||
|
|
||||||
|
// Navigate to calendar page
|
||||||
|
await page.goto('http://localhost:3000/calendar');
|
||||||
|
|
||||||
|
// Wait for page to load
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Take a full page screenshot
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'calendar-diagnosis.png',
|
||||||
|
fullPage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Screenshot saved as calendar-diagnosis.png');
|
||||||
|
|
||||||
|
// Get page title
|
||||||
|
const title = await page.title();
|
||||||
|
console.log('Page title:', title);
|
||||||
|
|
||||||
|
// Check if navigation is present
|
||||||
|
const navigation = await page.$('nav');
|
||||||
|
console.log('Navigation present:', navigation !== null);
|
||||||
|
|
||||||
|
// Look for Create Event button/link
|
||||||
|
const createEventLinks = await page.$$('a[href="/events/new"]');
|
||||||
|
console.log('Create Event links found:', createEventLinks.length);
|
||||||
|
|
||||||
|
// Look for any manage links
|
||||||
|
const manageLinks = await page.$$('a[href*="manage"]');
|
||||||
|
console.log('Manage links found:', manageLinks.length);
|
||||||
|
|
||||||
|
// Check if user dropdown exists
|
||||||
|
const userDropdown = await page.$('#user-dropdown');
|
||||||
|
console.log('User dropdown present:', userDropdown !== null);
|
||||||
|
|
||||||
|
// Check if user menu button exists
|
||||||
|
const userMenuBtn = await page.$('#user-menu-btn');
|
||||||
|
console.log('User menu button present:', userMenuBtn !== null);
|
||||||
|
|
||||||
|
// If user menu exists, click it to see dropdown
|
||||||
|
if (userMenuBtn) {
|
||||||
|
console.log('Clicking user menu button...');
|
||||||
|
await userMenuBtn.click();
|
||||||
|
|
||||||
|
// Wait a bit for dropdown to appear
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Take another screenshot with dropdown open
|
||||||
|
await page.screenshot({
|
||||||
|
path: 'calendar-diagnosis-dropdown.png',
|
||||||
|
fullPage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Dropdown screenshot saved as calendar-diagnosis-dropdown.png');
|
||||||
|
|
||||||
|
// Check what links are in the dropdown
|
||||||
|
const dropdownLinks = await page.$$('#user-dropdown a');
|
||||||
|
console.log('Dropdown links found:', dropdownLinks.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < dropdownLinks.length; i++) {
|
||||||
|
const link = dropdownLinks[i];
|
||||||
|
const href = await link.getAttribute('href');
|
||||||
|
const text = await link.textContent();
|
||||||
|
console.log(`Dropdown link ${i + 1}: "${text}" -> ${href}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current URL to confirm where we are
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log('Current URL:', currentUrl);
|
||||||
|
|
||||||
|
// Check console errors
|
||||||
|
const consoleMessages = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
consoleMessages.push(`${msg.type()}: ${msg.text()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for any JavaScript errors
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
errors.push(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh page to catch any console messages
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
console.log('Console messages:', consoleMessages);
|
||||||
|
console.log('JavaScript errors:', errors);
|
||||||
|
|
||||||
|
// Check if authentication is working
|
||||||
|
try {
|
||||||
|
const response = await page.evaluate(() => {
|
||||||
|
return fetch('/api/auth/user').then(r => r.json());
|
||||||
|
});
|
||||||
|
console.log('Auth status:', response);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Auth check failed:', error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
171
test-dark-mode-modal.cjs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Dark Mode Modal Testing', () => {
|
||||||
|
test('should test modal in dark mode with proper opacity', async ({ page }) => {
|
||||||
|
console.log('Testing modal in dark mode...');
|
||||||
|
|
||||||
|
// First, go to login page
|
||||||
|
await page.goto('http://localhost:3001/login-new');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Login
|
||||||
|
await page.fill('input[type="email"], input[name="email"]', 'tmartinez@gmail.com');
|
||||||
|
await page.fill('input[type="password"], input[name="password"]', 'TestPassword123!');
|
||||||
|
await page.click('button[type="submit"], button:has-text("Sign In"), button:has-text("Login")');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
if (page.url().includes('/login')) {
|
||||||
|
console.log('Login failed, skipping test');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to manage page
|
||||||
|
await page.goto('http://localhost:3001/events/d89dd7ec-01b3-4b89-a52e-b08afaf7661d/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Click on Ticket Types tab
|
||||||
|
const ticketTypesTab = page.locator('button').filter({ hasText: /Ticket Types/i });
|
||||||
|
if (await ticketTypesTab.count() > 0) {
|
||||||
|
await ticketTypesTab.first().click();
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test in light mode first
|
||||||
|
console.log('Testing modal in light mode...');
|
||||||
|
const addTicketButton = page.locator('button').filter({ hasText: /Add Ticket Type/i });
|
||||||
|
|
||||||
|
if (await addTicketButton.count() > 0) {
|
||||||
|
await addTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check modal background opacity in light mode
|
||||||
|
const lightModeOpacity = await page.evaluate(() => {
|
||||||
|
const modal = document.querySelector('[style*="background: rgba(0, 0, 0, 0.75)"]');
|
||||||
|
if (modal) {
|
||||||
|
const style = window.getComputedStyle(modal);
|
||||||
|
return {
|
||||||
|
background: style.background || modal.style.background,
|
||||||
|
isVisible: modal.offsetParent !== null,
|
||||||
|
zIndex: style.zIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Light mode modal background:', lightModeOpacity);
|
||||||
|
|
||||||
|
// Close modal by clicking X button
|
||||||
|
const closeButton = page.locator('button[aria-label="Close modal"]');
|
||||||
|
if (await closeButton.count() > 0) {
|
||||||
|
console.log('Testing X button close functionality...');
|
||||||
|
await closeButton.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Verify modal is closed
|
||||||
|
const modalClosed = await page.evaluate(() => {
|
||||||
|
const modal = document.querySelector('[style*="background: rgba(0, 0, 0, 0.75)"]');
|
||||||
|
return !modal || modal.offsetParent === null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal closed after X button click:', modalClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'modal-light-mode-test.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to dark mode - look for theme toggle
|
||||||
|
console.log('Switching to dark mode...');
|
||||||
|
const themeToggle = page.locator('button').filter({ hasText: /theme|dark|light/i }).first();
|
||||||
|
const themeToggleIcon = page.locator('button svg').first(); // Often theme toggles are just icons
|
||||||
|
|
||||||
|
// Try different approaches to toggle dark mode
|
||||||
|
if (await themeToggle.count() > 0) {
|
||||||
|
await themeToggle.click();
|
||||||
|
} else if (await themeToggleIcon.count() > 0) {
|
||||||
|
await themeToggleIcon.click();
|
||||||
|
} else {
|
||||||
|
// Try to find theme toggle by looking for common selectors
|
||||||
|
const possibleToggles = await page.locator('button, [role="button"]').all();
|
||||||
|
for (const toggle of possibleToggles) {
|
||||||
|
const title = await toggle.getAttribute('title').catch(() => null);
|
||||||
|
const ariaLabel = await toggle.getAttribute('aria-label').catch(() => null);
|
||||||
|
if (title?.includes('theme') || title?.includes('dark') ||
|
||||||
|
ariaLabel?.includes('theme') || ariaLabel?.includes('dark')) {
|
||||||
|
await toggle.click();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Test modal in dark mode
|
||||||
|
console.log('Testing modal in dark mode...');
|
||||||
|
if (await addTicketButton.count() > 0) {
|
||||||
|
await addTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check modal background opacity in dark mode
|
||||||
|
const darkModeOpacity = await page.evaluate(() => {
|
||||||
|
const modal = document.querySelector('[style*="background: rgba(0, 0, 0, 0.75)"]');
|
||||||
|
if (modal) {
|
||||||
|
const style = window.getComputedStyle(modal);
|
||||||
|
return {
|
||||||
|
background: style.background || modal.style.background,
|
||||||
|
isVisible: modal.offsetParent !== null,
|
||||||
|
zIndex: style.zIndex,
|
||||||
|
modalContent: modal.innerHTML.length > 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Dark mode modal background:', darkModeOpacity);
|
||||||
|
|
||||||
|
// Test close button in dark mode
|
||||||
|
const closeButtonDark = page.locator('button[aria-label="Close modal"]');
|
||||||
|
if (await closeButtonDark.count() > 0) {
|
||||||
|
console.log('Testing X button in dark mode...');
|
||||||
|
await closeButtonDark.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
const modalClosedDark = await page.evaluate(() => {
|
||||||
|
const modal = document.querySelector('[style*="background: rgba(0, 0, 0, 0.75)"]');
|
||||||
|
return !modal || modal.offsetParent === null;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Dark mode modal closed after X button click:', modalClosedDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'modal-dark-mode-test.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final verification - check if background opacity is correct (0.75)
|
||||||
|
const opacityVerification = await page.evaluate(() => {
|
||||||
|
// Look for any modals with the expected opacity
|
||||||
|
const modalSelectors = [
|
||||||
|
'[style*="rgba(0, 0, 0, 0.75)"]',
|
||||||
|
'[style*="background: rgba(0, 0, 0, 0.75)"]',
|
||||||
|
'.fixed.inset-0.backdrop-blur-sm'
|
||||||
|
];
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (const selector of modalSelectors) {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
if (elements.length > 0) {
|
||||||
|
found = true;
|
||||||
|
console.log(`Found modal with selector: ${selector}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
correctOpacityFound: found,
|
||||||
|
totalModalElements: document.querySelectorAll('.fixed.inset-0').length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Final opacity verification:', opacityVerification);
|
||||||
|
});
|
||||||
|
});
|
||||||
50
test-manage-redirect.cjs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Event Management Page', () => {
|
||||||
|
test('should redirect to login with proper redirect parameter when not authenticated', async ({ page }) => {
|
||||||
|
// Navigate to a management page without being authenticated
|
||||||
|
await page.goto('/events/7ac12bd2-8509-4db3-b1bc-98a808646311/manage');
|
||||||
|
|
||||||
|
// Should redirect to login page with redirect parameter
|
||||||
|
await expect(page).toHaveURL(/\/login-new\?redirect=.*manage/);
|
||||||
|
|
||||||
|
// Verify the redirect parameter contains the original URL
|
||||||
|
const url = page.url();
|
||||||
|
const urlParams = new URLSearchParams(url.split('?')[1]);
|
||||||
|
const redirectParam = urlParams.get('redirect');
|
||||||
|
|
||||||
|
expect(redirectParam).toContain('/events/7ac12bd2-8509-4db3-b1bc-98a808646311/manage');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should redirect back to original page after successful login', async ({ page }) => {
|
||||||
|
// Go to login page with redirect parameter
|
||||||
|
await page.goto('/login-new?redirect=%2Fevents%2F7ac12bd2-8509-4db3-b1bc-98a808646311%2Fmanage');
|
||||||
|
|
||||||
|
// Wait for the login form to load
|
||||||
|
await page.waitForSelector('#email');
|
||||||
|
await page.waitForSelector('#password');
|
||||||
|
|
||||||
|
// Fill in login credentials (you might need to adjust these)
|
||||||
|
await page.fill('#email', 'test@example.com');
|
||||||
|
await page.fill('#password', 'password123');
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
await page.click('#login-btn');
|
||||||
|
|
||||||
|
// Wait for redirect (this might fail if credentials are invalid, but shows intent)
|
||||||
|
try {
|
||||||
|
await page.waitForURL(/\/events\/.*\/manage/, { timeout: 5000 });
|
||||||
|
console.log('Successfully redirected to manage page');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Login failed or redirect timeout - this is expected with test credentials');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show 404 page for invalid event ID', async ({ page }) => {
|
||||||
|
// Navigate to an invalid event ID
|
||||||
|
await page.goto('/events/invalid-event-id/manage');
|
||||||
|
|
||||||
|
// Should still redirect to login first
|
||||||
|
await expect(page).toHaveURL(/\/login-new\?redirect=.*invalid-event-id.*manage/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"status": "passed",
|
"status": "failed",
|
||||||
"failedTests": []
|
"failedTests": [
|
||||||
|
"02f76897f4525b4c9d46-0bfd6441b30ce541a2ca"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Page snapshot
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- link "Skip to main content":
|
||||||
|
- /url: "#main-content"
|
||||||
|
- link "Skip to navigation":
|
||||||
|
- /url: "#navigation"
|
||||||
|
- main:
|
||||||
|
- navigation:
|
||||||
|
- link "BCT":
|
||||||
|
- /url: /dashboard
|
||||||
|
- img
|
||||||
|
- text: BCT
|
||||||
|
- link "All Events":
|
||||||
|
- /url: /dashboard
|
||||||
|
- img
|
||||||
|
- text: All Events
|
||||||
|
- text: "| Event Management"
|
||||||
|
- button "Switch to dark mode":
|
||||||
|
- img
|
||||||
|
- button "T Tyler Admin":
|
||||||
|
- text: T Tyler Admin
|
||||||
|
- img
|
||||||
|
- main:
|
||||||
|
- heading "Garfield County Fair & Rodeo 2025" [level=1]
|
||||||
|
- img
|
||||||
|
- text: Garfield County Fairgrounds - Rifle, CO
|
||||||
|
- img
|
||||||
|
- text: Wednesday, August 6, 2025 at 12:00 AM
|
||||||
|
- paragraph: Secure your spot at the 87-year-strong Garfield County Fair & Rodeo, returning to Rifle, Colorado August 2 and 6-9, 2025...
|
||||||
|
- button "Show more"
|
||||||
|
- text: $0 Total Revenue
|
||||||
|
- link "Preview":
|
||||||
|
- /url: /e/firebase-event-zmkx63pewurqyhtw6tsn
|
||||||
|
- img
|
||||||
|
- text: Preview
|
||||||
|
- button "Embed":
|
||||||
|
- img
|
||||||
|
- text: Embed
|
||||||
|
- button "Edit":
|
||||||
|
- img
|
||||||
|
- text: Edit
|
||||||
|
- link "Scanner":
|
||||||
|
- /url: /scan
|
||||||
|
- img
|
||||||
|
- text: Scanner
|
||||||
|
- link "Kiosk":
|
||||||
|
- /url: /kiosk/firebase-event-zmkx63pewurqyhtw6tsn
|
||||||
|
- img
|
||||||
|
- text: Kiosk
|
||||||
|
- button "PIN":
|
||||||
|
- img
|
||||||
|
- text: PIN
|
||||||
|
- paragraph: Tickets Sold
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: +12% from last week
|
||||||
|
- img
|
||||||
|
- paragraph: Available
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: Ready to sell
|
||||||
|
- img
|
||||||
|
- paragraph: Check-ins
|
||||||
|
- paragraph: "0"
|
||||||
|
- img
|
||||||
|
- text: 85% attendance rate
|
||||||
|
- img
|
||||||
|
- paragraph: Net Revenue
|
||||||
|
- paragraph: $0
|
||||||
|
- img
|
||||||
|
- text: +24% this month
|
||||||
|
- img
|
||||||
|
- button "Ticketing & Access":
|
||||||
|
- img
|
||||||
|
- text: Ticketing & Access
|
||||||
|
- button "Custom Pages":
|
||||||
|
- img
|
||||||
|
- text: Custom Pages
|
||||||
|
- button "Sales":
|
||||||
|
- img
|
||||||
|
- text: Sales
|
||||||
|
- button "Attendees":
|
||||||
|
- img
|
||||||
|
- text: Attendees
|
||||||
|
- button "Event Settings":
|
||||||
|
- img
|
||||||
|
- text: Event Settings
|
||||||
|
- button "Ticket Types"
|
||||||
|
- button "Access Codes"
|
||||||
|
- button "Discounts"
|
||||||
|
- button "Printed Tickets"
|
||||||
|
- heading "Ticket Types & Pricing" [level=2]
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- button:
|
||||||
|
- img
|
||||||
|
- button "Add Ticket Type":
|
||||||
|
- img
|
||||||
|
- text: Add Ticket Type
|
||||||
|
- img
|
||||||
|
- paragraph: No ticket types created yet
|
||||||
|
- button "Create Your First Ticket Type"
|
||||||
|
- contentinfo:
|
||||||
|
- link "Terms of Service":
|
||||||
|
- /url: /terms
|
||||||
|
- link "Privacy Policy":
|
||||||
|
- /url: /privacy
|
||||||
|
- link "Support":
|
||||||
|
- /url: /support
|
||||||
|
- text: © 2025 All rights reserved Powered by
|
||||||
|
- link "blackcanyontickets.com":
|
||||||
|
- /url: https://blackcanyontickets.com
|
||||||
|
- img
|
||||||
|
- heading "Cookie Preferences" [level=3]
|
||||||
|
- paragraph:
|
||||||
|
- text: We use essential cookies to make our website work and analytics cookies to understand how you interact with our site.
|
||||||
|
- link "Learn more in our Privacy Policy":
|
||||||
|
- /url: /privacy
|
||||||
|
- button "Manage Preferences"
|
||||||
|
- button "Accept All"
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 100 KiB |
109
test-simple-buttons.cjs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Simple Button Test', () => {
|
||||||
|
test('should test buttons with dev server', async ({ page }) => {
|
||||||
|
console.log('Testing buttons on dev server...');
|
||||||
|
|
||||||
|
// Navigate to the manage page that's actually being accessed
|
||||||
|
await page.goto('http://localhost:3001/events/d89dd7ec-01b3-4b89-a52e-b08afaf7661d/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(3000); // Give time for React components to load
|
||||||
|
|
||||||
|
// Take screenshot
|
||||||
|
await page.screenshot({ path: 'manage-page-dev.png', fullPage: true });
|
||||||
|
console.log('Manage page screenshot saved');
|
||||||
|
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`Current URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
if (currentUrl.includes('/login')) {
|
||||||
|
console.log('Still redirected to login - authentication required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for our simple embed test component
|
||||||
|
const simpleTestCheck = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
hasReact: typeof React !== 'undefined',
|
||||||
|
hasReactDOM: typeof ReactDOM !== 'undefined',
|
||||||
|
simpleEmbedFunction: typeof window.openEmbedModal === 'function',
|
||||||
|
astroIslands: document.querySelectorAll('astro-island').length,
|
||||||
|
embedButton: !!document.getElementById('embed-code-btn'),
|
||||||
|
allButtons: document.querySelectorAll('button').length,
|
||||||
|
buttonTexts: Array.from(document.querySelectorAll('button')).map(b => b.textContent?.trim()).filter(t => t),
|
||||||
|
consoleMessages: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Page analysis:', JSON.stringify(simpleTestCheck, null, 2));
|
||||||
|
|
||||||
|
// Look specifically for the embed button
|
||||||
|
const embedButton = page.locator('#embed-code-btn');
|
||||||
|
const embedExists = await embedButton.count();
|
||||||
|
console.log(`Embed button exists: ${embedExists > 0}`);
|
||||||
|
|
||||||
|
if (embedExists > 0) {
|
||||||
|
console.log('Testing embed button...');
|
||||||
|
|
||||||
|
// Set up dialog handler for the alert
|
||||||
|
page.on('dialog', async dialog => {
|
||||||
|
console.log('Alert dialog appeared:', dialog.message());
|
||||||
|
await dialog.accept();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click the embed button
|
||||||
|
await embedButton.click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
console.log('Embed button clicked');
|
||||||
|
|
||||||
|
// Check if window.openEmbedModal exists after click
|
||||||
|
const afterClick = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
openEmbedModalExists: typeof window.openEmbedModal === 'function',
|
||||||
|
windowKeys: Object.keys(window).filter(k => k.includes('embed')),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('After click analysis:', afterClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for ticket pricing buttons
|
||||||
|
const ticketButtons = page.locator('button').filter({ hasText: /Add Ticket|Create.*Ticket/i });
|
||||||
|
const ticketButtonCount = await ticketButtons.count();
|
||||||
|
console.log(`Ticket pricing buttons found: ${ticketButtonCount}`);
|
||||||
|
|
||||||
|
if (ticketButtonCount > 0) {
|
||||||
|
console.log('Testing ticket button...');
|
||||||
|
await ticketButtons.first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Look for modal or any response
|
||||||
|
const modalCheck = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('[role="dialog"], .modal, [data-testid*="modal"]');
|
||||||
|
return {
|
||||||
|
modalCount: modals.length,
|
||||||
|
modalInfo: Array.from(modals).map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
className: m.className,
|
||||||
|
visible: !m.hidden && m.style.display !== 'none'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal check after ticket button:', modalCheck);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-ticket-button-dev.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any JavaScript errors
|
||||||
|
const errors = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('JavaScript errors:', errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
189
test-ticket-auth.cjs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Ticket Buttons with Authentication', () => {
|
||||||
|
test('should login and test ticket pricing buttons', async ({ page }) => {
|
||||||
|
console.log('Testing ticket buttons with authentication...');
|
||||||
|
|
||||||
|
// First, go to login page
|
||||||
|
await page.goto('http://localhost:3001/login-new');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Take screenshot of login page
|
||||||
|
await page.screenshot({ path: 'login-before-auth.png', fullPage: true });
|
||||||
|
|
||||||
|
// Fill in login credentials
|
||||||
|
console.log('Filling login form...');
|
||||||
|
await page.fill('input[type="email"], input[name="email"]', 'tmartinez@gmail.com');
|
||||||
|
await page.fill('input[type="password"], input[name="password"]', 'TestPassword123!');
|
||||||
|
|
||||||
|
// Submit login form
|
||||||
|
await page.click('button[type="submit"], button:has-text("Sign In"), button:has-text("Login")');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
console.log('Login submitted, current URL:', page.url());
|
||||||
|
|
||||||
|
// Check if we're logged in successfully
|
||||||
|
if (page.url().includes('/login')) {
|
||||||
|
console.log('Still on login page - checking for errors');
|
||||||
|
await page.screenshot({ path: 'login-failed.png', fullPage: true });
|
||||||
|
|
||||||
|
// Look for error messages
|
||||||
|
const errorMsg = await page.locator('.error, [class*="error"], [role="alert"]').textContent().catch(() => null);
|
||||||
|
console.log('Login error message:', errorMsg);
|
||||||
|
|
||||||
|
return; // Exit if login failed
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Login successful! Now navigating to manage page...');
|
||||||
|
|
||||||
|
// Navigate to the manage page
|
||||||
|
await page.goto('http://localhost:3001/events/d89dd7ec-01b3-4b89-a52e-b08afaf7661d/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(5000); // Wait for React components to load
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'manage-page-authenticated.png', fullPage: true });
|
||||||
|
|
||||||
|
console.log('Manage page loaded, current URL:', page.url());
|
||||||
|
|
||||||
|
// Click on Ticket Types tab specifically
|
||||||
|
const ticketTypesTab = page.locator('button').filter({ hasText: /Ticket Types/i });
|
||||||
|
const ticketTypesCount = await ticketTypesTab.count();
|
||||||
|
console.log(`Ticket Types tab found: ${ticketTypesCount > 0}`);
|
||||||
|
|
||||||
|
if (ticketTypesCount > 0) {
|
||||||
|
console.log('Clicking Ticket Types tab...');
|
||||||
|
await ticketTypesTab.first().click();
|
||||||
|
await page.waitForTimeout(5000); // Wait longer for React component to load
|
||||||
|
await page.screenshot({ path: 'ticket-types-tab-active.png', fullPage: true });
|
||||||
|
|
||||||
|
// Wait for any loading states to complete
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
// Wait for either a loading indicator to disappear or ticket content to appear
|
||||||
|
return !document.querySelector('[data-loading="true"]') ||
|
||||||
|
document.querySelector('[class*="ticket"]') ||
|
||||||
|
document.querySelector('button[contains(., "Add Ticket")]');
|
||||||
|
}, { timeout: 10000 }).catch(() => {
|
||||||
|
console.log('Timeout waiting for ticket content to load');
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-ticket-types-load.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now look for ticket pricing buttons
|
||||||
|
console.log('Looking for ticket pricing buttons...');
|
||||||
|
|
||||||
|
// Get all buttons and their text
|
||||||
|
const allButtons = await page.locator('button').all();
|
||||||
|
const buttonInfo = [];
|
||||||
|
for (const button of allButtons) {
|
||||||
|
const text = await button.textContent();
|
||||||
|
const isVisible = await button.isVisible();
|
||||||
|
const isEnabled = await button.isEnabled();
|
||||||
|
if (text && text.trim()) {
|
||||||
|
buttonInfo.push({
|
||||||
|
text: text.trim(),
|
||||||
|
visible: isVisible,
|
||||||
|
enabled: isEnabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All buttons found:', buttonInfo);
|
||||||
|
|
||||||
|
// Look specifically for ticket buttons
|
||||||
|
const addTicketButton = page.locator('button').filter({ hasText: /Add Ticket Type/i });
|
||||||
|
const createFirstButton = page.locator('button').filter({ hasText: /Create.*First.*Ticket/i });
|
||||||
|
|
||||||
|
const addTicketCount = await addTicketButton.count();
|
||||||
|
const createFirstCount = await createFirstButton.count();
|
||||||
|
|
||||||
|
console.log(`"Add Ticket Type" buttons: ${addTicketCount}`);
|
||||||
|
console.log(`"Create Your First Ticket Type" buttons: ${createFirstCount}`);
|
||||||
|
|
||||||
|
// Test the Add Ticket Type button
|
||||||
|
if (addTicketCount > 0) {
|
||||||
|
console.log('Testing "Add Ticket Type" button...');
|
||||||
|
|
||||||
|
// Check if button is visible and enabled
|
||||||
|
const isVisible = await addTicketButton.first().isVisible();
|
||||||
|
const isEnabled = await addTicketButton.first().isEnabled();
|
||||||
|
console.log(`Add Ticket button - Visible: ${isVisible}, Enabled: ${isEnabled}`);
|
||||||
|
|
||||||
|
if (isVisible && isEnabled) {
|
||||||
|
// Set up console message monitoring for ALL console messages
|
||||||
|
const consoleMessages = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
consoleMessages.push(`${msg.type()}: ${msg.text()}`);
|
||||||
|
console.log(`CONSOLE ${msg.type()}: ${msg.text()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click the button
|
||||||
|
await addTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check for modal
|
||||||
|
const modalCheck = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('[role="dialog"], .modal, [class*="Modal"], [data-testid*="modal"]');
|
||||||
|
const visibleModals = Array.from(modals).filter(m => {
|
||||||
|
const style = window.getComputedStyle(m);
|
||||||
|
return style.display !== 'none' && style.visibility !== 'hidden' && m.offsetParent !== null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalModals: modals.length,
|
||||||
|
visibleModals: visibleModals.length,
|
||||||
|
modalElements: Array.from(modals).map(m => ({
|
||||||
|
tagName: m.tagName,
|
||||||
|
id: m.id,
|
||||||
|
className: m.className,
|
||||||
|
isVisible: m.offsetParent !== null
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal check after Add Ticket Type click:', modalCheck);
|
||||||
|
console.log('Console messages:', consoleMessages);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-add-ticket-click-authenticated.png', fullPage: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the Create Your First Ticket Type button
|
||||||
|
if (createFirstCount > 0) {
|
||||||
|
console.log('Testing "Create Your First Ticket Type" button...');
|
||||||
|
|
||||||
|
const isVisible = await createFirstButton.first().isVisible();
|
||||||
|
const isEnabled = await createFirstButton.first().isEnabled();
|
||||||
|
console.log(`Create First Ticket button - Visible: ${isVisible}, Enabled: ${isEnabled}`);
|
||||||
|
|
||||||
|
if (isVisible && isEnabled) {
|
||||||
|
await createFirstButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const modalCheck = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('[role="dialog"], .modal, [class*="Modal"], [data-testid*="modal"]');
|
||||||
|
return {
|
||||||
|
totalModals: modals.length,
|
||||||
|
visibleModals: Array.from(modals).filter(m => m.offsetParent !== null).length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal check after Create First Ticket click:', modalCheck);
|
||||||
|
await page.screenshot({ path: 'after-create-first-authenticated.png', fullPage: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the TicketTypeModal component is actually loaded
|
||||||
|
const componentCheck = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
astroIslands: document.querySelectorAll('astro-island').length,
|
||||||
|
ticketComponents: document.querySelectorAll('[class*="Ticket"]').length,
|
||||||
|
reactComponents: document.querySelectorAll('[data-reactroot]').length,
|
||||||
|
ticketTypeModalElements: document.querySelectorAll('[class*="TicketTypeModal"], [id*="ticket-modal"]').length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Component analysis:', componentCheck);
|
||||||
|
});
|
||||||
|
});
|
||||||
173
test-ticket-buttons.cjs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
test.describe('Ticket Pricing Buttons Test', () => {
|
||||||
|
test('should test ticket pricing buttons functionality', async ({ page }) => {
|
||||||
|
console.log('Testing ticket pricing buttons...');
|
||||||
|
|
||||||
|
// Try to navigate to a manage page
|
||||||
|
await page.goto('http://localhost:3001/events/d89dd7ec-01b3-4b89-a52e-b08afaf7661d/manage');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const currentUrl = page.url();
|
||||||
|
console.log(`Current URL: ${currentUrl}`);
|
||||||
|
|
||||||
|
if (currentUrl.includes('/login')) {
|
||||||
|
console.log('Authentication required - trying to login first or bypass');
|
||||||
|
|
||||||
|
// Let's try to access the home page and see if we can find any accessible pages
|
||||||
|
await page.goto('http://localhost:3001/');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Take screenshot of homepage
|
||||||
|
await page.screenshot({ path: 'homepage-access.png', fullPage: true });
|
||||||
|
|
||||||
|
// Try to find any manage links or test events
|
||||||
|
const links = await page.locator('a[href*="/events/"]').count();
|
||||||
|
console.log(`Found ${links} event links on homepage`);
|
||||||
|
|
||||||
|
if (links > 0) {
|
||||||
|
const eventLinks = await page.locator('a[href*="/events/"]').all();
|
||||||
|
for (let i = 0; i < Math.min(3, eventLinks.length); i++) {
|
||||||
|
const href = await eventLinks[i].getAttribute('href');
|
||||||
|
console.log(`Found event link: ${href}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // Skip the rest if we can't access manage page
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made it here, we have access to the manage page
|
||||||
|
await page.screenshot({ path: 'manage-page-accessible.png', fullPage: true });
|
||||||
|
|
||||||
|
// Wait for React components to load
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
// Check for ticket-related buttons specifically
|
||||||
|
console.log('Looking for ticket pricing buttons...');
|
||||||
|
|
||||||
|
// Check if the tickets tab is visible/active
|
||||||
|
const ticketsTab = page.locator('button, [role="tab"]').filter({ hasText: /tickets/i });
|
||||||
|
const ticketsTabCount = await ticketsTab.count();
|
||||||
|
console.log(`Tickets tab found: ${ticketsTabCount > 0}`);
|
||||||
|
|
||||||
|
if (ticketsTabCount > 0) {
|
||||||
|
console.log('Clicking tickets tab...');
|
||||||
|
await ticketsTab.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for "Add Ticket Type" button
|
||||||
|
const addTicketButton = page.locator('button').filter({ hasText: /Add Ticket Type/i });
|
||||||
|
const addTicketCount = await addTicketButton.count();
|
||||||
|
console.log(`"Add Ticket Type" buttons found: ${addTicketCount}`);
|
||||||
|
|
||||||
|
// Look for "Create Your First Ticket Type" button
|
||||||
|
const createFirstTicketButton = page.locator('button').filter({ hasText: /Create.*First.*Ticket/i });
|
||||||
|
const createFirstCount = await createFirstTicketButton.count();
|
||||||
|
console.log(`"Create Your First Ticket Type" buttons found: ${createFirstCount}`);
|
||||||
|
|
||||||
|
// Get all button texts to see what's available
|
||||||
|
const allButtons = await page.locator('button').all();
|
||||||
|
const buttonTexts = [];
|
||||||
|
for (const button of allButtons) {
|
||||||
|
const text = await button.textContent();
|
||||||
|
if (text && text.trim()) {
|
||||||
|
buttonTexts.push(text.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('All button texts found:', buttonTexts);
|
||||||
|
|
||||||
|
// Check for React component mounting
|
||||||
|
const reactInfo = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
astroIslands: document.querySelectorAll('astro-island').length,
|
||||||
|
reactElements: document.querySelectorAll('[data-reactroot], [data-react-class]').length,
|
||||||
|
ticketComponents: document.querySelectorAll('[class*="Ticket"], [id*="ticket"]').length,
|
||||||
|
modalElements: document.querySelectorAll('[role="dialog"], .modal, [data-testid*="modal"]').length,
|
||||||
|
hasWindowOpenEmbedModal: typeof window.openEmbedModal === 'function'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('React component info:', reactInfo);
|
||||||
|
|
||||||
|
// Test the "Add Ticket Type" button if it exists
|
||||||
|
if (addTicketCount > 0) {
|
||||||
|
console.log('Testing "Add Ticket Type" button...');
|
||||||
|
|
||||||
|
// Set up console monitoring
|
||||||
|
const consoleMessages = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
consoleMessages.push(`${msg.type()}: ${msg.text()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
await addTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check for modal after click
|
||||||
|
const modalAfterAdd = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('[role="dialog"], .modal, [class*="Modal"]');
|
||||||
|
return {
|
||||||
|
modalCount: modals.length,
|
||||||
|
modalsVisible: Array.from(modals).filter(m =>
|
||||||
|
!m.hidden &&
|
||||||
|
m.style.display !== 'none' &&
|
||||||
|
m.offsetParent !== null
|
||||||
|
).length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal status after "Add Ticket Type" click:', modalAfterAdd);
|
||||||
|
console.log('Console messages during click:', consoleMessages);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-add-ticket-click.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the "Create Your First Ticket Type" button if it exists
|
||||||
|
if (createFirstCount > 0) {
|
||||||
|
console.log('Testing "Create Your First Ticket Type" button...');
|
||||||
|
|
||||||
|
await createFirstTicketButton.first().click();
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const modalAfterCreate = await page.evaluate(() => {
|
||||||
|
const modals = document.querySelectorAll('[role="dialog"], .modal, [class*="Modal"]');
|
||||||
|
return {
|
||||||
|
modalCount: modals.length,
|
||||||
|
modalsVisible: Array.from(modals).filter(m =>
|
||||||
|
!m.hidden &&
|
||||||
|
m.style.display !== 'none' &&
|
||||||
|
m.offsetParent !== null
|
||||||
|
).length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Modal status after "Create Your First Ticket Type" click:', modalAfterCreate);
|
||||||
|
|
||||||
|
await page.screenshot({ path: 'after-create-first-ticket-click.png', fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any JavaScript errors
|
||||||
|
const pageErrors = [];
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
pageErrors.push(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Page errors found:', pageErrors);
|
||||||
|
|
||||||
|
// Final check - try to find any ticket-related React components
|
||||||
|
const ticketComponentCheck = await page.evaluate(() => {
|
||||||
|
// Look for specific ticket component indicators
|
||||||
|
const indicators = {
|
||||||
|
ticketTabsComponent: !!document.querySelector('[class*="TicketsTab"]'),
|
||||||
|
ticketTypeModal: !!document.querySelector('[class*="TicketTypeModal"]'),
|
||||||
|
ticketManagement: !!document.querySelector('[class*="ticket"], [class*="Ticket"]'),
|
||||||
|
hasTicketTypes: !!document.querySelector('[data-testid*="ticket"], [id*="ticket"]')
|
||||||
|
};
|
||||||
|
|
||||||
|
return indicators;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Ticket component indicators:', ticketComponentCheck);
|
||||||
|
});
|
||||||
|
});
|
||||||
BIN
ticket-types-tab-active.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
tickets-tab-active.png
Normal file
|
After Width: | Height: | Size: 171 KiB |