Compare commits
3 Commits
6746fc72b7
...
a049472a13
| Author | SHA1 | Date | |
|---|---|---|---|
| a049472a13 | |||
| 92ab9406be | |||
| 988294a55d |
49
.mcp.json
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"context": {
|
||||
"modes": ["sequential-thinking"]
|
||||
},
|
||||
"mcpServers": {
|
||||
"supabase": {
|
||||
"command": "npx",
|
||||
@@ -10,6 +13,52 @@
|
||||
"env": {
|
||||
"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
|
||||
|
||||
# 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 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
|
||||
node setup-schema.js # Initialize database schema (run once)
|
||||
|
||||
# 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 down && docker-compose up -d # Clean restart containers
|
||||
|
||||
# Stripe MCP (Model Context Protocol)
|
||||
npm run mcp:stripe # Start Stripe MCP server for AI integration
|
||||
@@ -197,8 +211,16 @@ const formattedDate = api.formatDate(dateString);
|
||||
|
||||
## 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
|
||||
- **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
|
||||
- **Performance**: Sentry performance monitoring enabled
|
||||
|
||||
@@ -228,6 +250,13 @@ SENTRY_DSN=https://...
|
||||
2. **API Endpoints**: Create in `/src/pages/api/` with proper validation
|
||||
3. **UI Components**: Follow glassmorphism design system patterns
|
||||
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
|
||||
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
|
||||
|
||||
**⚠️ 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:logs": "docker-compose -f docker-compose.astro.yml logs -f",
|
||||
"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": {
|
||||
"@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
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
module.exports = defineConfig({
|
||||
export default defineConfig({
|
||||
testDir: './',
|
||||
testMatch: 'test-calendar-theme.cjs',
|
||||
testMatch: 'test-*.cjs',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
@@ -20,9 +20,9 @@ module.exports = defineConfig({
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
}
|
||||
],
|
||||
webServer: {
|
||||
command: 'echo "Server assumed to be running"',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
// webServer: {
|
||||
// command: 'echo "Server assumed to be running"',
|
||||
// url: 'http://192.168.0.46:3000',
|
||||
// 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 {
|
||||
eventId: string;
|
||||
}
|
||||
@@ -118,6 +120,12 @@ const { eventId } = Astro.props;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Simple Embed Test -->
|
||||
<SimpleEmbedTest
|
||||
client:load
|
||||
eventId={eventId}
|
||||
/>
|
||||
|
||||
<script define:vars={{ eventId }}>
|
||||
// Initialize event header when page loads
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@@ -220,82 +228,7 @@ const { eventId } = Astro.props;
|
||||
|
||||
// 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 showEditEventModal(event) {
|
||||
@@ -540,22 +473,31 @@ const { eventId } = Astro.props;
|
||||
if (embedBtn) {
|
||||
embedBtn.addEventListener('click', () => {
|
||||
// Get event details for the embed modal
|
||||
const eventTitle = document.getElementById('event-title').textContent;
|
||||
const eventSlug = document.getElementById('preview-link').href.split('/e/')[1];
|
||||
const previewLink = document.getElementById('preview-link').href;
|
||||
const eventSlug = previewLink ? previewLink.split('/e/')[1] : 'loading';
|
||||
|
||||
// Get eventId from the URL
|
||||
const urlParts = window.location.pathname.split('/');
|
||||
const currentEventId = urlParts[urlParts.indexOf('events') + 1];
|
||||
|
||||
// Create and show embed modal
|
||||
showEmbedModal(currentEventId, eventSlug, eventTitle);
|
||||
// Show embed modal using React component
|
||||
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
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', addEventListeners);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
addEventListeners();
|
||||
});
|
||||
} else {
|
||||
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 { loadSalesData, calculateSalesMetrics } from '../../lib/sales-analytics';
|
||||
import { formatCurrency } from '../../lib/event-management';
|
||||
import TicketTypeModal from '../modals/TicketTypeModal';
|
||||
import TicketTypeModal from '../modals/TicketTypeModal.tsx';
|
||||
import type { TicketType } from '../../lib/ticket-management';
|
||||
|
||||
interface TicketsTabProps {
|
||||
@@ -39,8 +39,10 @@ export default function TicketsTab({ eventId }: TicketsTabProps) {
|
||||
};
|
||||
|
||||
const handleCreateTicketType = () => {
|
||||
console.log('handleCreateTicketType called');
|
||||
setEditingTicketType(undefined);
|
||||
setShowModal(true);
|
||||
console.log('showModal set to true');
|
||||
};
|
||||
|
||||
const handleEditTicketType = (ticketType: TicketType) => {
|
||||
@@ -378,12 +380,14 @@ export default function TicketsTab({ eventId }: TicketsTabProps) {
|
||||
|
||||
{showModal && (
|
||||
<TicketTypeModal
|
||||
isOpen={showModal}
|
||||
eventId={eventId}
|
||||
ticketType={editingTicketType}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSave={loadData}
|
||||
onSave={() => loadData()}
|
||||
/>
|
||||
)}
|
||||
{console.log('TicketsTab render - showModal:', showModal)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -67,14 +67,27 @@ export default function EmbedCodeModal({
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||
<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="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="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="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
|
||||
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"
|
||||
>
|
||||
<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 */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-white mb-4">Direct Link</h3>
|
||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Direct Link</h3>
|
||||
<div className="rounded-lg p-4" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||
<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 className="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={directLink}
|
||||
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
|
||||
onClick={() => handleCopy(directLink, 'link')}
|
||||
@@ -114,11 +132,11 @@ export default function EmbedCodeModal({
|
||||
</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>
|
||||
<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">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -126,9 +144,10 @@ export default function EmbedCodeModal({
|
||||
value="basic"
|
||||
checked={embedType === 'basic'}
|
||||
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 className="flex items-center">
|
||||
<input
|
||||
@@ -136,32 +155,43 @@ export default function EmbedCodeModal({
|
||||
value="custom"
|
||||
checked={embedType === '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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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
|
||||
type="number"
|
||||
value={width}
|
||||
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"
|
||||
max="800"
|
||||
/>
|
||||
</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
|
||||
type="number"
|
||||
value={height}
|
||||
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"
|
||||
max="1000"
|
||||
/>
|
||||
@@ -171,11 +201,16 @@ export default function EmbedCodeModal({
|
||||
{embedType === 'custom' && (
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
value={theme}
|
||||
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="dark">Dark</option>
|
||||
@@ -183,12 +218,16 @@ export default function EmbedCodeModal({
|
||||
</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
|
||||
type="color"
|
||||
value={primaryColor}
|
||||
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>
|
||||
|
||||
@@ -198,18 +237,20 @@ export default function EmbedCodeModal({
|
||||
type="checkbox"
|
||||
checked={showHeader}
|
||||
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 className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDescription}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,13 +259,14 @@ export default function EmbedCodeModal({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-white mb-4">Embed Code</h3>
|
||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Embed Code</h3>
|
||||
<div className="rounded-lg p-4" style={{ background: 'var(--glass-bg-lg)', border: '1px solid var(--glass-border)' }}>
|
||||
<textarea
|
||||
value={generateEmbedCode()}
|
||||
readOnly
|
||||
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">
|
||||
<button
|
||||
@@ -244,8 +286,8 @@ export default function EmbedCodeModal({
|
||||
|
||||
{/* Preview Panel */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-white mb-4">Preview</h3>
|
||||
<div className="bg-white/5 border border-white/20 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-4" style={{ color: 'var(--glass-text-primary)' }}>Preview</h3>
|
||||
<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">
|
||||
<iframe
|
||||
src={previewUrl}
|
||||
@@ -264,7 +306,10 @@ export default function EmbedCodeModal({
|
||||
<div className="flex justify-end mt-6">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -20,8 +20,8 @@ export default function TicketTypeModal({
|
||||
const [formData, setFormData] = useState<TicketTypeFormData>({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: 0,
|
||||
quantity: 100,
|
||||
price: 0,
|
||||
quantity_available: 100,
|
||||
is_active: true
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -32,16 +32,16 @@ export default function TicketTypeModal({
|
||||
setFormData({
|
||||
name: ticketType.name,
|
||||
description: ticketType.description,
|
||||
price_cents: ticketType.price_cents,
|
||||
quantity: ticketType.quantity,
|
||||
price: ticketType.price,
|
||||
quantity_available: ticketType.quantity_available,
|
||||
is_active: ticketType.is_active
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
price_cents: 0,
|
||||
quantity: 100,
|
||||
price: 0,
|
||||
quantity_available: 100,
|
||||
is_active: true
|
||||
});
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export default function TicketTypeModal({
|
||||
if (!isOpen) return null;
|
||||
|
||||
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="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
@@ -136,13 +136,18 @@ export default function TicketTypeModal({
|
||||
required
|
||||
value={formData.name}
|
||||
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"
|
||||
/>
|
||||
</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
|
||||
</label>
|
||||
<textarea
|
||||
@@ -151,49 +156,64 @@ export default function TicketTypeModal({
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
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..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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 ($) *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="price_cents"
|
||||
name="price_cents"
|
||||
id="price"
|
||||
name="price"
|
||||
required
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={formData.price_cents / 100}
|
||||
value={formData.price}
|
||||
onChange={(e) => {
|
||||
const dollars = parseFloat(e.target.value) || 0;
|
||||
const price = parseFloat(e.target.value) || 0;
|
||||
setFormData(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"
|
||||
/>
|
||||
</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 *
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
id="quantity_available"
|
||||
name="quantity_available"
|
||||
required
|
||||
min="1"
|
||||
value={formData.quantity}
|
||||
value={formData.quantity_available}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -206,9 +226,10 @@ export default function TicketTypeModal({
|
||||
name="is_active"
|
||||
checked={formData.is_active}
|
||||
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)
|
||||
</label>
|
||||
</div>
|
||||
@@ -217,14 +238,16 @@ export default function TicketTypeModal({
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { supabase } from './supabase';
|
||||
|
||||
/**
|
||||
* Admin API Router for centralized admin dashboard API calls
|
||||
* 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 {
|
||||
private session: any = null;
|
||||
@@ -64,46 +63,28 @@ export class AdminApiRouter {
|
||||
}
|
||||
}
|
||||
|
||||
const [organizationsResult, eventsResult, ticketsResult] = await Promise.all([
|
||||
supabase.from('organizations').select('id'),
|
||||
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', {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch('/api/admin/stats', {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (usersResponse.ok) {
|
||||
const usersResult = await usersResponse.json();
|
||||
if (usersResult.success) {
|
||||
users = usersResult.data?.length || 0;
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch platform stats:', response.status, response.statusText);
|
||||
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) {
|
||||
console.error('Error fetching user count for stats:', 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) {
|
||||
|
||||
console.error('Platform stats error:', error);
|
||||
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([
|
||||
supabase.from('events').select('*, organizations(name)').order('created_at', { ascending: false }).limit(5),
|
||||
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', {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch('/api/admin/activity', {
|
||||
method: 'GET',
|
||||
credentials: 'include'
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (usersResponse.ok) {
|
||||
const result = await usersResponse.json();
|
||||
if (result.success) {
|
||||
// Limit to 5 most recent users
|
||||
usersResult.data = result.data.slice(0, 5);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch recent activity:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Recent activity API error:', result.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching users for recent activity:', 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) {
|
||||
|
||||
console.error('Recent activity error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -205,20 +139,33 @@ export class AdminApiRouter {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: org, error } = await supabase
|
||||
.from('organizations')
|
||||
.select('*')
|
||||
.eq('id', orgId)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch(`/api/admin/organizations?id=${orgId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch organization:', response.status, response.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return org;
|
||||
} catch (error) {
|
||||
const result = await response.json();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -235,30 +182,28 @@ export class AdminApiRouter {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: orgs, error } = await supabase
|
||||
.from('organizations')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch('/api/admin/organizations', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch organizations:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get user counts for each organization
|
||||
if (orgs) {
|
||||
for (const org of orgs) {
|
||||
const { data: users } = await supabase
|
||||
.from('users')
|
||||
.select('id')
|
||||
.eq('organization_id', org.id);
|
||||
org.user_count = users ? users.length : 0;
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Organizations API error:', result.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return orgs || [];
|
||||
return result.data || [];
|
||||
} catch (error) {
|
||||
|
||||
console.error('Organizations error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -315,35 +260,28 @@ export class AdminApiRouter {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: events, error } = await supabase
|
||||
.from('events')
|
||||
.select(`
|
||||
*,
|
||||
organizations(name),
|
||||
users(name, email),
|
||||
venues(name)
|
||||
`)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch('/api/admin/admin-events', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch events:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get ticket type counts for each event
|
||||
if (events) {
|
||||
for (const event of events) {
|
||||
const { data: ticketTypes } = await supabase
|
||||
.from('ticket_types')
|
||||
.select('id')
|
||||
.eq('event_id', event.id);
|
||||
event.ticket_type_count = ticketTypes ? ticketTypes.length : 0;
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Events API error:', result.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return events || [];
|
||||
return result.data || [];
|
||||
} catch (error) {
|
||||
|
||||
console.error('Events error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -360,34 +298,28 @@ export class AdminApiRouter {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: tickets, error } = await supabase
|
||||
.from('tickets')
|
||||
.select(`
|
||||
*,
|
||||
ticket_types (
|
||||
name,
|
||||
price
|
||||
),
|
||||
events (
|
||||
title,
|
||||
venue,
|
||||
start_time,
|
||||
organizations (
|
||||
name
|
||||
)
|
||||
)
|
||||
`)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
if (error) {
|
||||
// Use server-side API endpoint to avoid CORS issues
|
||||
const response = await fetch('/api/admin/admin-tickets', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch tickets:', response.status, response.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
return tickets || [];
|
||||
} catch (error) {
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Tickets API error:', result.error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return result.data || [];
|
||||
} catch (error) {
|
||||
console.error('Tickets error:', error);
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,8 @@ export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||
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
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { supabase } from './supabase';
|
||||
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 {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -66,5 +66,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
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;
|
||||
});
|
||||
@@ -1,44 +1,62 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import PublicHeader from '../components/PublicHeader.astro';
|
||||
import ThemeToggle from '../components/ThemeToggle.tsx';
|
||||
---
|
||||
|
||||
<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 />
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<div class="fixed top-20 right-4 z-50">
|
||||
<ThemeToggle client:load />
|
||||
</div>
|
||||
|
||||
<!-- 404 Hero Section -->
|
||||
<section class="relative overflow-hidden min-h-screen flex items-center justify-center">
|
||||
<!-- Animated Background -->
|
||||
<div class="absolute inset-0 opacity-30">
|
||||
<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 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 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>
|
||||
<!-- Animated Background Elements -->
|
||||
<div class="absolute inset-0 opacity-20">
|
||||
<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-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 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>
|
||||
|
||||
<!-- Floating Elements -->
|
||||
<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-40 right-32 w-6 h-6 bg-purple-200 rounded-full animate-float opacity-50" style="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-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 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 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 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 rounded-full animate-float opacity-70" style="background: var(--glass-text-accent); animation-delay: 1.5s;"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<!-- 404 Illustration -->
|
||||
<div class="mb-12">
|
||||
<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">
|
||||
<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
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- 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="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">
|
||||
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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" 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>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -48,26 +66,28 @@ import PublicHeader from '../components/PublicHeader.astro';
|
||||
|
||||
<!-- Error Message -->
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Looking for something specific?</h3>
|
||||
<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 mb-4" style="color: var(--glass-text-primary);">Looking for something specific?</h3>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
id="error-search"
|
||||
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
|
||||
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">
|
||||
<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">
|
||||
<a
|
||||
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">
|
||||
<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
|
||||
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">
|
||||
<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>
|
||||
<span>Go Home</span>
|
||||
</a>
|
||||
@@ -102,38 +124,42 @@ import PublicHeader from '../components/PublicHeader.astro';
|
||||
|
||||
<!-- Popular Suggestions -->
|
||||
<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">
|
||||
<a
|
||||
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-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
|
||||
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-sm font-medium text-gray-700">Music</div>
|
||||
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Music</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
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-sm font-medium text-gray-700">Arts</div>
|
||||
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Arts</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
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-sm font-medium text-gray-700">Community</div>
|
||||
<div class="text-sm font-medium" style="color: var(--glass-text-secondary);">Community</div>
|
||||
</a>
|
||||
</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 {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
@@ -176,10 +193,6 @@ import PublicHeader from '../components/PublicHeader.astro';
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Interactive hover effects */
|
||||
.hover-lift {
|
||||
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 {
|
||||
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>
|
||||
|
||||
<script>
|
||||
import { initializeTheme } from '../lib/theme';
|
||||
|
||||
// Initialize theme
|
||||
initializeTheme();
|
||||
|
||||
// Search functionality from 404 page
|
||||
const errorSearch = document.getElementById('error-search');
|
||||
const errorSearchBtn = document.getElementById('error-search-btn');
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import PublicHeader from '../components/PublicHeader.astro';
|
||||
import ThemeToggle from '../components/ThemeToggle.tsx';
|
||||
---
|
||||
|
||||
<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 />
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<div class="fixed top-20 right-4 z-50">
|
||||
<ThemeToggle client:load />
|
||||
</div>
|
||||
|
||||
<!-- 500 Hero Section -->
|
||||
<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 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 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 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-20 left-20 w-64 h-64 rounded-full blur-3xl animate-pulse" style="background: var(--bg-orb-1);"></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 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 class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<!-- Error Illustration -->
|
||||
<div class="mb-12">
|
||||
<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">
|
||||
<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
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<!-- 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="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">
|
||||
<svg class="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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" 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>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -40,24 +58,24 @@ import PublicHeader from '../components/PublicHeader.astro';
|
||||
|
||||
<!-- Error Message -->
|
||||
<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
|
||||
</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.
|
||||
</p>
|
||||
|
||||
<!-- 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="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-lg font-semibold text-gray-900">Server Status</span>
|
||||
<div class="w-3 h-3 rounded-full animate-pulse" style="background: var(--error-color, #ef4444);"></div>
|
||||
<span class="text-lg font-semibold" style="color: var(--glass-text-primary);">Server Status</span>
|
||||
</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.
|
||||
</p>
|
||||
<div class="text-sm text-gray-500">
|
||||
Error Code: <span class="font-mono bg-gray-100 px-2 py-1 rounded">TEMP_500</span>
|
||||
<div class="text-sm" style="color: var(--glass-text-tertiary);">
|
||||
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>
|
||||
@@ -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">
|
||||
<button
|
||||
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">
|
||||
<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
|
||||
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">
|
||||
<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>
|
||||
<span>Go Home</span>
|
||||
</a>
|
||||
@@ -87,14 +107,15 @@ import PublicHeader from '../components/PublicHeader.astro';
|
||||
|
||||
<!-- Support Contact -->
|
||||
<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">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-3">Need Immediate Help?</h3>
|
||||
<p class="text-gray-600 mb-4 text-sm">
|
||||
<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 mb-3" style="color: var(--glass-text-primary);">Need Immediate Help?</h3>
|
||||
<p class="mb-4 text-sm" style="color: var(--glass-text-secondary);">
|
||||
If this error persists, please reach out to our support team.
|
||||
</p>
|
||||
<a
|
||||
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">
|
||||
<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 {
|
||||
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>
|
||||
|
||||
<script>
|
||||
import { initializeTheme } from '../lib/theme';
|
||||
|
||||
// Initialize theme
|
||||
initializeTheme();
|
||||
|
||||
// Auto-retry functionality
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Navigation from '../../components/Navigation.astro';
|
||||
import { createSupabaseServerClient } from '../../lib/supabase-ssr';
|
||||
|
||||
// Enable server-side rendering for auth checks
|
||||
@@ -61,46 +62,35 @@ const auth = {
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Navigation -->
|
||||
<nav class="sticky top-0 z-50 bg-white/10 backdrop-blur-xl shadow-xl border-b border-white/20">
|
||||
<!-- Modern Navigation Component -->
|
||||
<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="flex justify-between h-20">
|
||||
<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 justify-between items-center h-12">
|
||||
<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
|
||||
id="super-admin-link"
|
||||
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
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
</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>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
@@ -281,8 +271,6 @@ const auth = {
|
||||
<script>
|
||||
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');
|
||||
|
||||
let currentTab = 'overview';
|
||||
@@ -296,11 +284,6 @@ const auth = {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userInfo = adminApi.getUserInfo();
|
||||
if (userInfo && userNameSpan) {
|
||||
userNameSpan.textContent = userInfo.name || userInfo.email;
|
||||
}
|
||||
|
||||
// Check if user has super admin privileges
|
||||
try {
|
||||
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>
|
||||
</td>
|
||||
<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'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -970,7 +953,7 @@ const auth = {
|
||||
<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>
|
||||
</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'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1250,12 +1233,6 @@ const auth = {
|
||||
});
|
||||
});
|
||||
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
await adminApi.signOut();
|
||||
window.location.href = '/';
|
||||
});
|
||||
}
|
||||
|
||||
// Fee management functions
|
||||
function setupFeeFormListeners() {
|
||||
@@ -1506,21 +1483,20 @@ const auth = {
|
||||
`;
|
||||
button.disabled = true;
|
||||
|
||||
// Get platform data
|
||||
const supabase = adminApi.getSupabaseClient();
|
||||
// Get platform data using server-side API to avoid CORS issues
|
||||
const [eventsResult, usersResult, ticketsResult, orgsResult] = await Promise.all([
|
||||
supabase.from('events').select('*'),
|
||||
supabase.from('users').select('*'),
|
||||
supabase.from('tickets').select('*'),
|
||||
supabase.from('organizations').select('*')
|
||||
adminApi.getEvents(),
|
||||
adminApi.getUsers(),
|
||||
adminApi.getTickets(),
|
||||
adminApi.getOrganizations()
|
||||
]);
|
||||
|
||||
// Create CSV content
|
||||
const csvData = {
|
||||
events: eventsResult.data || [],
|
||||
users: usersResult.data || [],
|
||||
tickets: ticketsResult.data || [],
|
||||
organizations: orgsResult.data || []
|
||||
events: eventsResult || [],
|
||||
users: usersResult || [],
|
||||
tickets: ticketsResult || [],
|
||||
organizations: orgsResult || []
|
||||
};
|
||||
|
||||
// Create summary report
|
||||
|
||||
@@ -308,31 +308,11 @@ const search = url.searchParams.get('search');
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Force dark mode for this page - no theme toggle allowed
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
|
||||
// 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);
|
||||
// Default to dark theme for better glassmorphism design
|
||||
const preferredTheme = localStorage.getItem('theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', preferredTheme);
|
||||
document.documentElement.classList.add(preferredTheme);
|
||||
document.documentElement.classList.remove(preferredTheme === 'dark' ? 'light' : 'dark');
|
||||
</script>
|
||||
|
||||
<script>
|
||||
@@ -634,6 +614,11 @@ const search = url.searchParams.get('search');
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeLocation();
|
||||
|
||||
// Load components immediately with empty data if no location
|
||||
if (!userLocation) {
|
||||
loadComponents();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</Layout>
|
||||
@@ -1,17 +1,10 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.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;
|
||||
|
||||
// Required authentication check for calendar access
|
||||
const auth = await verifyAuth(Astro.request);
|
||||
if (!auth) {
|
||||
return Astro.redirect('/login-new');
|
||||
}
|
||||
|
||||
// Get query parameters for filtering
|
||||
const url = new URL(Astro.request.url);
|
||||
const featured = url.searchParams.get('featured');
|
||||
@@ -147,7 +140,7 @@ const search = url.searchParams.get('search');
|
||||
</section>
|
||||
|
||||
<!-- 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="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
|
||||
<!-- View Toggle - Premium Design -->
|
||||
@@ -512,7 +505,8 @@ const search = url.searchParams.get('search');
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
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) {
|
||||
@@ -1623,7 +1617,7 @@ const search = url.searchParams.get('search');
|
||||
|
||||
// Old theme toggle code removed - using simpler onclick approach
|
||||
|
||||
// Smooth sticky header behavior
|
||||
// Simplified sticky header - just keep hero visible
|
||||
window.initStickyHeader = function initStickyHeader() {
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
const filterControls = document.querySelector('[data-filter-controls]');
|
||||
@@ -1634,63 +1628,21 @@ const search = url.searchParams.get('search');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add smooth transition styles
|
||||
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;
|
||||
// Calculate hero section height
|
||||
const heroHeight = heroSection.offsetHeight;
|
||||
const filterControlsOffsetTop = filterControls.offsetTop;
|
||||
|
||||
// Calculate transition point - when filter controls should take over
|
||||
const transitionThreshold = filterControlsOffsetTop - heroHeight;
|
||||
// Set CSS variable for filter controls positioning
|
||||
document.documentElement.style.setProperty('--hero-height', `${heroHeight}px`);
|
||||
|
||||
if (currentScrollY >= transitionThreshold) {
|
||||
// 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;
|
||||
// Ensure hero section stays visible with proper styling
|
||||
heroSection.style.position = 'sticky';
|
||||
heroSection.style.top = '0px';
|
||||
heroSection.style.zIndex = '40';
|
||||
heroSection.style.transform = 'translateY(0)';
|
||||
heroSection.style.opacity = '1';
|
||||
heroSection.style.zIndex = '40'; // Above content but below filter controls
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
}
|
||||
|
||||
// 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();
|
||||
console.log('Hero section initialized and kept visible. Height:', heroHeight);
|
||||
console.log('Filter controls positioned below hero at:', `${heroHeight}px`);
|
||||
}
|
||||
|
||||
// Initialize sticky header
|
||||
|
||||
@@ -11,7 +11,9 @@ import { verifyAuth } from '../../../lib/auth';
|
||||
// Server-side authentication check using cookies
|
||||
const auth = await verifyAuth(Astro.cookies);
|
||||
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
|
||||
|
||||
@@ -6,8 +6,8 @@ import { verifyAuth } from '../../lib/auth';
|
||||
// Enable server-side rendering for auth checks
|
||||
export const prerender = false;
|
||||
|
||||
// Server-side authentication check
|
||||
const auth = await verifyAuth(Astro.request);
|
||||
// Server-side auth check using cookies for better SSR compatibility
|
||||
const auth = await verifyAuth(Astro.cookies);
|
||||
if (!auth) {
|
||||
return Astro.redirect('/login-new');
|
||||
}
|
||||
@@ -324,7 +324,15 @@ if (!auth) {
|
||||
|
||||
// Load user data (auth already verified server-side)
|
||||
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) {
|
||||
// Silently handle client-side auth failure - user might be logged out
|
||||
@@ -344,6 +352,11 @@ if (!auth) {
|
||||
}
|
||||
|
||||
return authUser;
|
||||
} catch (error) {
|
||||
console.error('Auth error:', error);
|
||||
window.location.href = '/login-new';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug from title
|
||||
@@ -505,7 +518,15 @@ if (!auth) {
|
||||
.select()
|
||||
.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
|
||||
|
||||
@@ -513,8 +534,22 @@ if (!auth) {
|
||||
window.location.href = `/events/${event.id}/manage`;
|
||||
|
||||
} catch (error) {
|
||||
// Handle errors gracefully without exposing details
|
||||
errorMessage.textContent = 'An error occurred creating the event. Please try again.';
|
||||
console.error('Event creation error:', error);
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -130,8 +130,10 @@ import LoginLayout from '../layouts/LoginLayout.astro';
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Login successful, redirect to dashboard
|
||||
window.location.href = data.redirectTo || '/dashboard';
|
||||
// Login successful, redirect to intended destination or dashboard
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const redirectTo = urlParams.get('redirect') || data.redirectTo || '/dashboard';
|
||||
window.location.href = redirectTo;
|
||||
} else {
|
||||
// Show error message
|
||||
errorMessage.textContent = data.error || 'Login failed. Please try again.';
|
||||
|
||||
@@ -1176,6 +1176,31 @@ nav a:hover {
|
||||
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 */
|
||||
.slider-thumb {
|
||||
-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",
|
||||
"failedTests": []
|
||||
"status": "failed",
|
||||
"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 |