diff --git a/BUGS_AND_FEATURES.md b/BUGS_AND_FEATURES.md new file mode 100644 index 00000000..76696a34 --- /dev/null +++ b/BUGS_AND_FEATURES.md @@ -0,0 +1,1309 @@ +# PicPeak Bug Report & Feature Requests + +> **Generated:** January 2, 2026 +> **Version:** Frontend v1.1.15 / Backend v1.1.15 +> **Priority Levels:** P0 (Critical), P1 (High), P2 (Medium), P3 (Low) + +--- + +## Table of Contents + +1. [BUG-001: Logo Customization Section Formatting Issues](#bug-001-logo-customization-section-formatting-issues) +2. [BUG-002: Favicon Upload Section Missing Preview](#bug-002-favicon-upload-section-missing-preview) +3. [FEATURE-003: Custom CSS Instructions Panel](#feature-003-custom-css-instructions-panel) +4. [FEATURE-004: Custom CSS Integration with Theme Presets & Gallery Layouts](#feature-004-custom-css-integration-with-theme-presets--gallery-layouts) +5. [BUG-005: Custom Layouts Not Showing in Event Creation](#bug-005-custom-layouts-not-showing-in-event-creation) +6. [FEATURE-006: Apple Liquid Glass Design Custom CSS Templates](#feature-006-apple-liquid-glass-design-custom-css-templates) +7. [BUG-007: Typography & Style Section Row Formatting](#bug-007-typography--style-section-row-formatting) + +--- + +## BUG-001: Logo Customization Section Formatting Issues + +### Priority: P1 (High) + +### Location +- **Page:** `/admin/branding` +- **Section:** Company Information → Logo Customization + +### Current Behavior +1. The "Upload Logo" button appears as a plain text link instead of a proper button +2. No preview thumbnail is displayed when a logo has been uploaded +3. The layout doesn't show the currently uploaded logo image +4. Button styling is inconsistent with other upload buttons (e.g., Favicon) + +### Expected Behavior +1. "Upload Logo" should be a styled button matching the "Upload Favicon" button style +2. When a logo is uploaded, a preview thumbnail should be displayed (similar to how other image uploads work) +3. A "Remove Logo" or "Change Logo" option should appear when a logo exists +4. The section should show: + - Current logo preview (if uploaded) + - Upload/Change button + - Remove button (if logo exists) + - File size/dimension info + +### Technical Analysis + +**Affected Component:** `frontend/src/pages/admin/BrandingPage.tsx` or similar + +**Proposed Solution:** +```tsx +// Logo preview section structure +
+ {currentLogo ? ( +
+ Current Logo +
+ + +
+
+ ) : ( + + )} +

Recommended size: 200x60px, PNG or JPEG

+
+``` + +### Screenshots +- See: `.playwright-mcp/branding-logo-section.png` + +--- + +## BUG-002: Favicon Upload Section Missing Preview + +### Priority: P2 (Medium) + +### Location +- **Page:** `/admin/branding` +- **Section:** Company Information → Favicon + +### Current Behavior +1. Only shows "Upload Favicon" button +2. No preview of currently uploaded favicon +3. No way to see or remove existing favicon + +### Expected Behavior +1. Display current favicon preview (32x32px thumbnail) +2. Show "Change Favicon" when one exists +3. Provide "Remove Favicon" option +4. Display file info (name, size) when uploaded + +### Technical Analysis + +**Proposed Solution:** +```tsx +
+ +
+ {currentFavicon ? ( + <> + Current Favicon + + + + ) : ( + + )} +
+

PNG or ICO format, recommended size: 32x32px

+
+``` + +--- + +## FEATURE-003: Custom CSS Instructions Panel + +### Priority: P2 (Medium) + +### Location +- **Page:** `/admin/settings` → Custom CSS tab + +### Current Behavior +- Simple textarea with minimal guidance +- Only shows: "Advanced: Add custom CSS to further customize the appearance" +- CSS variables are documented in comments within the template + +### Required Enhancement +Add a collapsible instruction panel with comprehensive documentation: + +### Proposed Implementation + +```tsx + + + + CSS Documentation & Guide + + + + {/* Documentation content */} + + +``` + +### Documentation Content Structure + +#### 1. Available CSS Custom Properties +```css +/* Color Variables */ +--gallery-bg: #ffffff; /* Main background color */ +--gallery-bg-secondary: #f5f5f5; /* Secondary/card background */ +--gallery-text: #171717; /* Primary text color */ +--gallery-text-muted: #737373; /* Muted/secondary text */ +--gallery-accent: #22c55e; /* Accent/highlight color */ +--gallery-accent-hover: #16a34a; /* Accent hover state */ +--gallery-border: #e5e5e5; /* Border color */ + +/* Spacing & Layout */ +--gallery-spacing: 16px; /* Base spacing unit */ +--gallery-radius: 8px; /* Border radius */ +--gallery-shadow: 0 1px 3px rgba(0,0,0,0.1); /* Box shadow */ + +/* Typography */ +--gallery-font-body: 'Inter', sans-serif; +--gallery-font-heading: 'Inter', sans-serif; +--gallery-font-size-base: 16px; +``` + +#### 2. Targetable CSS Classes +```css +/* Page Structure */ +.gallery-page { } /* Root gallery container */ +.gallery-header { } /* Header section */ +.gallery-content { } /* Main content area */ +.gallery-footer { } /* Footer section */ + +/* Photo Grid */ +.photo-grid { } /* Grid container */ +.photo-card { } /* Individual photo card */ +.photo-card img { } /* Photo image */ +.photo-card-overlay { } /* Hover overlay */ +.photo-card-info { } /* Photo metadata */ + +/* Lightbox */ +.lightbox-overlay { } /* Lightbox background */ +.lightbox-content { } /* Lightbox container */ +.lightbox-image { } /* Lightbox image */ +.lightbox-controls { } /* Navigation controls */ + +/* Buttons & Interactions */ +.gallery-btn { } /* Primary buttons */ +.gallery-btn-secondary { } /* Secondary buttons */ +.gallery-link { } /* Links */ + +/* Categories */ +.category-filter { } /* Category filter bar */ +.category-pill { } /* Category pill/tag */ + +/* Masonry Layout */ +.masonry-grid { } /* Masonry container */ +.masonry-item { } /* Masonry item */ + +/* Carousel Layout */ +.carousel-container { } /* Carousel wrapper */ +.carousel-slide { } /* Individual slide */ +.carousel-nav { } /* Navigation arrows */ +.carousel-dots { } /* Pagination dots */ + +/* Timeline Layout */ +.timeline-container { } /* Timeline wrapper */ +.timeline-item { } /* Timeline entry */ +.timeline-date { } /* Date marker */ + +/* Hero Layout */ +.hero-section { } /* Hero image area */ +.hero-image { } /* Featured image */ +.hero-content { } /* Hero text content */ +``` + +#### 3. Layout-Specific Customization +```css +/* Grid Layout Customization */ +.gallery-page[data-layout="grid"] .photo-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--gallery-spacing); +} + +/* Masonry Layout Customization */ +.gallery-page[data-layout="masonry"] .masonry-grid { + column-count: 4; + column-gap: var(--gallery-spacing); +} + +/* Carousel Layout Customization */ +.gallery-page[data-layout="carousel"] .carousel-container { + height: 80vh; +} + +/* Timeline Layout Customization */ +.gallery-page[data-layout="timeline"] .timeline-container { + max-width: 1200px; + margin: 0 auto; +} +``` + +#### 4. Responsive Breakpoints +```css +/* Mobile: < 640px */ +@media (max-width: 639px) { } + +/* Tablet: 640px - 1023px */ +@media (min-width: 640px) and (max-width: 1023px) { } + +/* Desktop: >= 1024px */ +@media (min-width: 1024px) { } +``` + +#### 5. Animation Classes +```css +/* Available animations */ +.animate-fade { } /* Fade in */ +.animate-scale { } /* Scale up */ +.animate-slide { } /* Slide in */ +.animate-none { } /* No animation */ +``` + +--- + +## FEATURE-004: Custom CSS Integration with Theme Presets & Gallery Layouts + +### Priority: P0 (Critical) + +### Location +- **Page:** `/admin/settings` → Custom CSS tab +- **Page:** `/admin/branding` → Gallery Theme section +- **Page:** `/admin/events/new` → Theme & Style section + +### Current Behavior +- Custom CSS templates exist but are isolated +- Theme presets don't integrate with custom CSS +- Users cannot define custom theme presets +- Users cannot define custom gallery layouts +- No way to use CSS variables from theme presets + +### Required Features + +#### 4.1 Custom Theme Preset Creation + +Users should be able to create custom theme presets that appear alongside built-in presets: + +```typescript +interface CustomThemePreset { + id: string; + name: string; + description: string; + thumbnail?: string; + // Inherits from base preset or standalone + basePreset?: 'classic-grid' | 'elegant-wedding' | 'modern-masonry' | null; + // CSS template reference + cssTemplateId: 1 | 2 | 3; + // Override variables + variables: { + primaryColor: string; + accentColor: string; + backgroundColor: string; + textColor: string; + fontBody: string; + fontHeading: string; + borderRadius: string; + spacing: string; + }; + // Layout settings + layout: 'grid' | 'masonry' | 'carousel' | 'timeline' | 'hero' | 'mosaic' | 'custom'; + layoutSettings: { + columns: { mobile: number; tablet: number; desktop: number }; + spacing: 'tight' | 'normal' | 'relaxed'; + animation: 'none' | 'fade' | 'scale' | 'slide'; + }; +} +``` + +#### 4.2 Custom Gallery Layout Definition + +Allow users to define custom layouts via CSS: + +```css +/* Custom Layout 1: Magazine Style */ +.gallery-page[data-layout="custom-1"] .photo-grid { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + grid-template-rows: auto; + gap: 8px; +} + +.gallery-page[data-layout="custom-1"] .photo-card:first-child { + grid-row: span 2; +} + +/* Custom Layout 2: Polaroid Style */ +.gallery-page[data-layout="custom-2"] .photo-card { + background: white; + padding: 12px 12px 40px 12px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + transform: rotate(var(--rotation, 0deg)); +} + +/* Custom Layout 3: Filmstrip */ +.gallery-page[data-layout="custom-3"] .photo-grid { + display: flex; + overflow-x: auto; + gap: 4px; + padding: 20px; + background: #1a1a1a; +} +``` + +#### 4.3 Variable Inheritance System + +Custom CSS should be able to reference theme preset variables: + +```css +/* Access theme preset variables */ +.gallery-page { + /* These come from the selected theme preset */ + background: var(--theme-bg, var(--gallery-bg)); + color: var(--theme-text, var(--gallery-text)); + + /* Override specific elements */ + --gallery-accent: var(--theme-accent); +} + +/* Conditional styling based on preset */ +.gallery-page[data-preset="elegant-wedding"] { + /* Wedding-specific overrides */ +} + +.gallery-page[data-preset="custom"] { + /* Full custom styling */ +} +``` + +### Database Schema Addition + +```sql +-- Custom theme presets table +CREATE TABLE custom_theme_presets ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + thumbnail_url VARCHAR(500), + base_preset VARCHAR(50), + css_template_id INTEGER REFERENCES css_templates(id), + variables JSONB NOT NULL DEFAULT '{}', + layout VARCHAR(50) NOT NULL DEFAULT 'grid', + layout_settings JSONB NOT NULL DEFAULT '{}', + is_enabled BOOLEAN DEFAULT true, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Link events to custom presets +ALTER TABLE events ADD COLUMN custom_preset_id INTEGER REFERENCES custom_theme_presets(id); +``` + +### UI Components Required + +1. **Theme Preset Builder** - Visual editor for creating custom presets +2. **Layout Designer** - CSS grid/flexbox visual editor for custom layouts +3. **Variable Picker** - Color/font/spacing picker that outputs CSS variables +4. **Preview Synchronization** - Real-time preview of custom themes + +--- + +## BUG-005: Custom Layouts Not Showing in Event Creation + +### Priority: P1 (High) + +### Location +- **Page:** `/admin/events/new` → Theme & Style → Gallery Layout + +### Current Behavior +- Only shows 6 default layouts: grid, masonry, carousel, timeline, hero, mosaic +- Custom CSS templates with enabled custom layouts don't appear +- No "Custom 1", "Custom 2", "Custom 3" options visible + +### Expected Behavior +When Custom CSS templates are enabled in Settings: +1. Additional layout options should appear: "Custom 1", "Custom 2", "Custom 3" +2. Only show custom layouts that have content (non-empty CSS) +3. Display the template name as the layout name +4. Show a preview thumbnail if available + +### Technical Analysis + +**Current Gallery Layout Options:** +```typescript +const GALLERY_LAYOUTS = [ + { value: 'grid', label: 'Grid', description: 'Classic grid layout...' }, + { value: 'masonry', label: 'Masonry', description: 'Pinterest-style...' }, + { value: 'carousel', label: 'Carousel', description: 'Full-screen slideshow...' }, + { value: 'timeline', label: 'Timeline', description: 'Photos organized by date' }, + { value: 'hero', label: 'Hero', description: 'Featured image with grid...' }, + { value: 'mosaic', label: 'Mosaic', description: 'Artistic layout...' }, +]; +``` + +**Required Addition:** +```typescript +// Fetch enabled custom templates +const { data: customTemplates } = useQuery({ + queryKey: ['customCssTemplates'], + queryFn: () => settingsService.getCustomCssTemplates(), +}); + +// Filter to only enabled templates with content +const enabledCustomLayouts = customTemplates + ?.filter(t => t.enabled && t.content?.trim()) + .map((t, index) => ({ + value: `custom-${index + 1}`, + label: t.name || `Custom ${index + 1}`, + description: 'Custom CSS layout template', + isCustom: true, + })); + +const allLayouts = [...GALLERY_LAYOUTS, ...(enabledCustomLayouts || [])]; +``` + +### API Endpoint Required + +```typescript +// GET /api/admin/settings/custom-css-templates +interface CustomCssTemplate { + id: number; + name: string; + enabled: boolean; + content: string; + hasLayoutDefinition: boolean; // true if contains custom layout CSS +} +``` + +--- + +## FEATURE-006: Apple Liquid Glass Design Custom CSS Templates + +### Priority: P2 (Medium) + +### Overview +Create 2 premium custom CSS templates implementing Apple's "Liquid Glass" design language introduced at WWDC 2025. This design features translucent surfaces, dynamic light refraction effects, and sophisticated blur treatments. + +### References +- [Apple's Liquid Glass UI design + CSS guide](https://dev.to/gruszdev/apples-liquid-glass-revolution-how-glassmorphism-is-shaping-ui-design-in-2025-with-css-code-1221) +- [Recreating Apple's Liquid Glass Effect with Pure CSS](https://dev.to/kevinbism/recreating-apples-liquid-glass-effect-with-pure-css-3gpl) +- [CSS-Tricks: Getting Clarity on Apple's Liquid Glass](https://css-tricks.com/getting-clarity-on-apples-liquid-glass/) +- [Liquid Glass CSS Generator](https://liquidglassgen.com/) + +### Template 1: "Liquid Glass Light" + +A light-themed glassmorphism design with subtle transparency and soft shadows. + +```css +/* + * PicPeak Custom CSS Template: Liquid Glass Light + * Inspired by Apple's iOS 26 Liquid Glass Design Language + * + * Features: + * - Translucent frosted glass surfaces + * - Dynamic light refraction effects + * - Subtle specular highlights + * - Soft depth shadows + */ + +/* ===== Base Theme Variables ===== */ +.gallery-page { + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-bg-elevated: rgba(255, 255, 255, 0.85); + --glass-border: rgba(255, 255, 255, 0.5); + --glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.15); + --glass-blur: 20px; + --glass-saturation: 180%; + + --gallery-bg: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --gallery-text: #1a1a2e; + --gallery-text-muted: rgba(26, 26, 46, 0.7); + --gallery-accent: #667eea; + --gallery-accent-hover: #764ba2; + --gallery-radius: 24px; + --gallery-spacing: 20px; +} + +/* ===== Page Background ===== */ +.gallery-page { + background: var(--gallery-bg); + min-height: 100vh; + position: relative; +} + +/* Animated gradient background */ +.gallery-page::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 40%); + pointer-events: none; + z-index: 0; +} + +/* ===== Glass Card Base ===== */ +.glass-surface { + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + box-shadow: + var(--glass-shadow), + inset 0 1px 1px rgba(255, 255, 255, 0.8), + inset 0 -1px 1px rgba(0, 0, 0, 0.05); +} + +/* Liquid shine effect */ +.glass-surface::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.4) 0%, + rgba(255, 255, 255, 0.1) 50%, + transparent 100% + ); + border-radius: var(--gallery-radius) var(--gallery-radius) 0 0; + pointer-events: none; +} + +/* ===== Gallery Header ===== */ +.gallery-header { + background: var(--glass-bg-elevated); + backdrop-filter: blur(30px) saturate(200%); + -webkit-backdrop-filter: blur(30px) saturate(200%); + border-bottom: 1px solid var(--glass-border); + padding: calc(var(--gallery-spacing) * 1.5); + position: sticky; + top: 0; + z-index: 100; +} + +.gallery-title { + color: var(--gallery-text); + font-weight: 700; + font-size: 1.75rem; + letter-spacing: -0.02em; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5); +} + +/* ===== Photo Grid ===== */ +.photo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--gallery-spacing); + padding: calc(var(--gallery-spacing) * 2); + position: relative; + z-index: 1; +} + +/* ===== Photo Cards - Glass Style ===== */ +.photo-card { + position: relative; + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.1), + inset 0 1px 1px rgba(255, 255, 255, 0.6); +} + +.photo-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 40%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.3) 0%, + transparent 100% + ); + pointer-events: none; + z-index: 1; + border-radius: var(--gallery-radius) var(--gallery-radius) 0 0; +} + +.photo-card:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: + 0 20px 40px rgba(102, 126, 234, 0.3), + 0 8px 16px rgba(0, 0, 0, 0.1), + inset 0 1px 1px rgba(255, 255, 255, 0.8); +} + +.photo-card img { + width: 100%; + height: 240px; + object-fit: cover; + transition: transform 0.4s ease; +} + +.photo-card:hover img { + transform: scale(1.05); +} + +.photo-card-info { + padding: var(--gallery-spacing); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0.3) 100% + ); +} + +/* ===== Buttons - Glass Style ===== */ +.gallery-btn { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: calc(var(--gallery-radius) / 2); + padding: 12px 24px; + color: var(--gallery-text); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.gallery-btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.4) 0%, + transparent 100% + ); +} + +.gallery-btn:hover { + background: var(--glass-bg-elevated); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); +} + +.gallery-btn-primary { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-hover) 100%); + color: white; + border: none; +} + +/* ===== Lightbox - Glass Style ===== */ +.lightbox-overlay { + background: rgba(26, 26, 46, 0.8); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); +} + +.lightbox-content { + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); +} + +/* ===== Category Pills ===== */ +.category-pill { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 9999px; + padding: 8px 20px; + font-size: 0.875rem; + font-weight: 500; + color: var(--gallery-text); + transition: all 0.3s ease; +} + +.category-pill:hover, +.category-pill.active { + background: var(--gallery-accent); + color: white; + border-color: var(--gallery-accent); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .gallery-page { + --gallery-radius: 16px; + --gallery-spacing: 12px; + --glass-blur: 16px; + } + + .photo-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + .photo-card img { + height: 180px; + } +} + +/* ===== Accessibility: Reduce Motion ===== */ +@media (prefers-reduced-motion: reduce) { + .photo-card, + .gallery-btn { + transition: none; + } + + .photo-card:hover { + transform: none; + } +} + +/* ===== Accessibility: Reduce Transparency ===== */ +@media (prefers-reduced-transparency: reduce) { + .glass-surface, + .photo-card, + .gallery-btn { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.95); + } +} +``` + +### Template 2: "Liquid Glass Dark" + +A dark-themed glassmorphism design with deep translucency and neon accents. + +```css +/* + * PicPeak Custom CSS Template: Liquid Glass Dark + * Inspired by Apple's iOS 26 Liquid Glass Design Language + * + * Features: + * - Deep translucent dark surfaces + * - Neon accent highlights + * - Dramatic glass reflections + * - Subtle animated gradients + */ + +/* ===== Base Theme Variables ===== */ +.gallery-page { + --glass-bg: rgba(15, 15, 35, 0.7); + --glass-bg-elevated: rgba(25, 25, 55, 0.85); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-border-highlight: rgba(255, 255, 255, 0.2); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --glass-blur: 24px; + --glass-saturation: 150%; + + --gallery-bg: #0a0a1a; + --gallery-text: #f0f0f5; + --gallery-text-muted: rgba(240, 240, 245, 0.6); + --gallery-accent: #00d4ff; + --gallery-accent-secondary: #ff00e5; + --gallery-accent-hover: #00ffea; + --gallery-radius: 20px; + --gallery-spacing: 20px; + + /* Neon glow variables */ + --neon-glow: 0 0 20px rgba(0, 212, 255, 0.5), 0 0 40px rgba(0, 212, 255, 0.2); + --neon-glow-secondary: 0 0 20px rgba(255, 0, 229, 0.5), 0 0 40px rgba(255, 0, 229, 0.2); +} + +/* ===== Page Background ===== */ +.gallery-page { + background: var(--gallery-bg); + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +/* Animated mesh gradient background */ +.gallery-page::before { + content: ''; + position: fixed; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: + radial-gradient(circle at 30% 20%, rgba(0, 212, 255, 0.15) 0%, transparent 40%), + radial-gradient(circle at 70% 80%, rgba(255, 0, 229, 0.1) 0%, transparent 40%), + radial-gradient(circle at 50% 50%, rgba(100, 100, 255, 0.05) 0%, transparent 60%); + animation: gradientShift 20s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +@keyframes gradientShift { + 0%, 100% { transform: translate(0, 0) rotate(0deg); } + 25% { transform: translate(2%, 2%) rotate(1deg); } + 50% { transform: translate(-1%, 3%) rotate(-1deg); } + 75% { transform: translate(3%, -2%) rotate(2deg); } +} + +/* ===== Gallery Header ===== */ +.gallery-header { + background: var(--glass-bg-elevated); + backdrop-filter: blur(30px) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturation)); + border-bottom: 1px solid var(--glass-border-highlight); + padding: calc(var(--gallery-spacing) * 1.5); + position: sticky; + top: 0; + z-index: 100; + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.gallery-title { + color: var(--gallery-text); + font-weight: 700; + font-size: 1.75rem; + letter-spacing: -0.02em; + background: linear-gradient(135deg, var(--gallery-text) 0%, var(--gallery-accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ===== Photo Grid ===== */ +.photo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--gallery-spacing); + padding: calc(var(--gallery-spacing) * 2); + position: relative; + z-index: 1; +} + +/* ===== Photo Cards - Dark Glass Style ===== */ +.photo-card { + position: relative; + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +/* Top highlight reflection */ +.photo-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.3) 50%, + transparent 100% + ); + z-index: 2; +} + +/* Inner glow effect */ +.photo-card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--gallery-radius); + padding: 1px; + background: linear-gradient( + 135deg, + rgba(0, 212, 255, 0) 0%, + rgba(0, 212, 255, 0) 40%, + rgba(0, 212, 255, 0.1) 100% + ); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0; + transition: opacity 0.4s ease; +} + +.photo-card:hover { + transform: translateY(-8px) scale(1.02); + border-color: var(--glass-border-highlight); + box-shadow: + 0 24px 48px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(0, 212, 255, 0.2), + var(--neon-glow); +} + +.photo-card:hover::after { + opacity: 1; +} + +.photo-card img { + width: 100%; + height: 240px; + object-fit: cover; + transition: transform 0.4s ease, filter 0.4s ease; + filter: brightness(0.9); +} + +.photo-card:hover img { + transform: scale(1.05); + filter: brightness(1); +} + +.photo-card-info { + padding: var(--gallery-spacing); + background: linear-gradient( + 180deg, + rgba(0, 0, 0, 0.2) 0%, + rgba(0, 0, 0, 0.4) 100% + ); + color: var(--gallery-text); +} + +.photo-card-info p { + color: var(--gallery-text-muted); + font-size: 0.875rem; +} + +/* ===== Buttons - Neon Glass Style ===== */ +.gallery-btn { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: calc(var(--gallery-radius) / 2); + padding: 12px 24px; + color: var(--gallery-text); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.gallery-btn:hover { + border-color: var(--gallery-accent); + box-shadow: var(--neon-glow); + color: var(--gallery-accent); +} + +.gallery-btn-primary { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + color: white; + border: none; + box-shadow: var(--neon-glow); +} + +.gallery-btn-primary:hover { + box-shadow: + 0 0 30px rgba(0, 212, 255, 0.6), + 0 0 60px rgba(0, 212, 255, 0.3), + 0 0 90px rgba(255, 0, 229, 0.2); + transform: translateY(-2px); +} + +/* ===== Lightbox - Dark Glass ===== */ +.lightbox-overlay { + background: rgba(5, 5, 15, 0.9); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); +} + +.lightbox-content { + background: var(--glass-bg-elevated); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid var(--glass-border-highlight); + border-radius: var(--gallery-radius); + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.5), + var(--neon-glow); +} + +/* ===== Category Pills ===== */ +.category-pill { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 9999px; + padding: 8px 20px; + font-size: 0.875rem; + font-weight: 500; + color: var(--gallery-text-muted); + transition: all 0.3s ease; +} + +.category-pill:hover { + border-color: var(--gallery-accent); + color: var(--gallery-accent); + box-shadow: var(--neon-glow); +} + +.category-pill.active { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + color: white; + border-color: transparent; + box-shadow: var(--neon-glow); +} + +/* ===== Scrollbar Styling ===== */ +.gallery-page ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.gallery-page ::-webkit-scrollbar-track { + background: var(--glass-bg); + border-radius: 4px; +} + +.gallery-page ::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + border-radius: 4px; +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .gallery-page { + --gallery-radius: 16px; + --gallery-spacing: 12px; + --glass-blur: 16px; + } + + .photo-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + .photo-card img { + height: 180px; + } + + /* Reduce animation complexity on mobile */ + .gallery-page::before { + animation: none; + } +} + +/* ===== Accessibility: Reduce Motion ===== */ +@media (prefers-reduced-motion: reduce) { + .gallery-page::before { + animation: none; + } + + .photo-card, + .gallery-btn { + transition: none; + } + + .photo-card:hover { + transform: none; + } +} + +/* ===== Accessibility: Reduce Transparency ===== */ +@media (prefers-reduced-transparency: reduce) { + .photo-card, + .gallery-btn, + .gallery-header { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .gallery-page { + --glass-bg: rgba(20, 20, 40, 0.98); + --glass-bg-elevated: rgba(30, 30, 60, 0.98); + } +} +``` + +--- + +## BUG-007: Typography & Style Section Row Formatting + +### Priority: P2 (Medium) + +### Location +- **Page:** `/admin/events/new` → Theme & Style → Typography & Style section +- **Page:** `/admin/branding` → Gallery Theme → Typography & Style section + +### Current Behavior +The second row of Typography & Style section contains 4 dropdown fields crammed into one row: +- Font Size +- Border Radius +- Shadow Style +- Background + +These fields are truncated and show abbreviated text (e.g., "Nor", "Lar", "Sub") because they don't have enough horizontal space. + +### Expected Behavior +Split the 4 fields into 2 rows of 2 fields each: +- **Row 1:** Font Size, Border Radius +- **Row 2:** Shadow Style, Background + +### Screenshots +- See: `.playwright-mcp/typography-style-visible.png` + +### Technical Analysis + +**Current Code Structure (likely):** +```tsx +
+ + + + +
+``` + +**Proposed Fix:** +```tsx +{/* Row 1: Font Size & Border Radius */} +
+ + + + + + +
+ +{/* Row 2: Shadow Style & Background */} +
+ + + + + + +
+``` + +### Files to Modify +1. `frontend/src/pages/admin/EventCreatePage.tsx` (or similar) +2. `frontend/src/pages/admin/BrandingPage.tsx` +3. `frontend/src/components/admin/ThemeCustomizer.tsx` (if shared component) + +--- + +## Implementation Priority Order + +| Priority | Issue | Effort | Impact | +|----------|-------|--------|--------| +| P0 | FEATURE-004: Custom CSS + Theme Integration | High | Critical for customization | +| P1 | BUG-001: Logo Upload Formatting | Medium | User experience | +| P1 | BUG-005: Custom Layouts in Events | Medium | Feature completeness | +| P2 | BUG-002: Favicon Preview | Low | User experience | +| P2 | BUG-007: Typography Row Layout | Low | UI polish | +| P2 | FEATURE-003: CSS Instructions | Medium | User guidance | +| P2 | FEATURE-006: Liquid Glass Templates | Medium | Premium feature | + +--- + +## Testing Checklist + +### BUG-001 & BUG-002 +- [ ] Logo upload displays preview after upload +- [ ] Logo can be changed/removed +- [ ] Favicon upload displays preview +- [ ] Favicon can be changed/removed +- [ ] Button styling matches design system + +### FEATURE-003 +- [ ] Collapsible instructions panel works +- [ ] All CSS variables documented +- [ ] All CSS classes documented +- [ ] Code examples copy correctly + +### FEATURE-004 +- [ ] Custom theme presets can be created +- [ ] Custom presets appear in event creation +- [ ] CSS variables from presets work in custom CSS +- [ ] Custom layouts render correctly + +### BUG-005 +- [ ] Enabled custom templates show in layout selector +- [ ] Only templates with content appear +- [ ] Custom layouts apply correctly to galleries + +### FEATURE-006 +- [ ] Liquid Glass Light template renders correctly +- [ ] Liquid Glass Dark template renders correctly +- [ ] Animations respect prefers-reduced-motion +- [ ] Transparency respects prefers-reduced-transparency +- [ ] Mobile responsiveness works + +### BUG-007 +- [ ] Typography section shows 2 items per row +- [ ] Dropdown labels fully visible +- [ ] Responsive behavior maintained + +--- + +*Document generated for PicPeak development team* diff --git a/backend/data/photo_sharing.db b/backend/data/photo_sharing.db index bd8a5bbd..2a0b5003 100644 Binary files a/backend/data/photo_sharing.db and b/backend/data/photo_sharing.db differ diff --git a/backend/migrations/core/053_add_liquid_glass_templates.js b/backend/migrations/core/053_add_liquid_glass_templates.js new file mode 100644 index 00000000..2900eb0f --- /dev/null +++ b/backend/migrations/core/053_add_liquid_glass_templates.js @@ -0,0 +1,693 @@ +/** + * Migration: Add Liquid Glass CSS Templates + * Updates template slots 2 and 3 with Apple-inspired Liquid Glass designs + */ + +const LIQUID_GLASS_LIGHT = `/* + * PicPeak Custom CSS Template: Liquid Glass Light + * Inspired by Apple's iOS 26 Liquid Glass Design Language + * + * Features: + * - Translucent frosted glass surfaces + * - Dynamic light refraction effects + * - Subtle specular highlights + * - Soft depth shadows + */ + +/* ===== Base Theme Variables ===== */ +.gallery-page { + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-bg-elevated: rgba(255, 255, 255, 0.85); + --glass-border: rgba(255, 255, 255, 0.5); + --glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.15); + --glass-blur: 20px; + --glass-saturation: 180%; + + --gallery-bg: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); + --gallery-text: #1a1a2e; + --gallery-text-muted: rgba(26, 26, 46, 0.7); + --gallery-accent: #667eea; + --gallery-accent-hover: #764ba2; + --gallery-radius: 24px; + --gallery-spacing: 20px; +} + +/* ===== Page Background ===== */ +.gallery-page { + background: var(--gallery-bg); + min-height: 100vh; + position: relative; +} + +/* Animated gradient background */ +.gallery-page::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.2) 0%, transparent 40%); + pointer-events: none; + z-index: 0; +} + +/* ===== Glass Card Base ===== */ +.glass-surface { + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + box-shadow: + var(--glass-shadow), + inset 0 1px 1px rgba(255, 255, 255, 0.8), + inset 0 -1px 1px rgba(0, 0, 0, 0.05); +} + +/* Liquid shine effect */ +.glass-surface::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.4) 0%, + rgba(255, 255, 255, 0.1) 50%, + transparent 100% + ); + border-radius: var(--gallery-radius) var(--gallery-radius) 0 0; + pointer-events: none; +} + +/* ===== Gallery Header ===== */ +.gallery-header { + background: var(--glass-bg-elevated); + backdrop-filter: blur(30px) saturate(200%); + -webkit-backdrop-filter: blur(30px) saturate(200%); + border-bottom: 1px solid var(--glass-border); + padding: calc(var(--gallery-spacing) * 1.5); + position: sticky; + top: 0; + z-index: 100; +} + +.gallery-title { + color: var(--gallery-text); + font-weight: 700; + font-size: 1.75rem; + letter-spacing: -0.02em; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5); +} + +/* ===== Photo Grid ===== */ +.photo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--gallery-spacing); + padding: calc(var(--gallery-spacing) * 2); + position: relative; + z-index: 1; +} + +/* ===== Photo Cards - Glass Style ===== */ +.photo-card { + position: relative; + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.1), + inset 0 1px 1px rgba(255, 255, 255, 0.6); +} + +.photo-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 40%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.3) 0%, + transparent 100% + ); + pointer-events: none; + z-index: 1; + border-radius: var(--gallery-radius) var(--gallery-radius) 0 0; +} + +.photo-card:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: + 0 20px 40px rgba(102, 126, 234, 0.3), + 0 8px 16px rgba(0, 0, 0, 0.1), + inset 0 1px 1px rgba(255, 255, 255, 0.8); +} + +.photo-card img { + width: 100%; + height: 240px; + object-fit: cover; + transition: transform 0.4s ease; +} + +.photo-card:hover img { + transform: scale(1.05); +} + +.photo-card-info { + padding: var(--gallery-spacing); + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0.3) 100% + ); +} + +/* ===== Buttons - Glass Style ===== */ +.gallery-btn { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: calc(var(--gallery-radius) / 2); + padding: 12px 24px; + color: var(--gallery-text); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.gallery-btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.4) 0%, + transparent 100% + ); +} + +.gallery-btn:hover { + background: var(--glass-bg-elevated); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); +} + +.gallery-btn-primary { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-hover) 100%); + color: white; + border: none; +} + +/* ===== Lightbox - Glass Style ===== */ +.lightbox-overlay { + background: rgba(26, 26, 46, 0.8); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); +} + +.lightbox-content { + background: var(--glass-bg); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); +} + +/* ===== Category Pills ===== */ +.category-pill { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 9999px; + padding: 8px 20px; + font-size: 0.875rem; + font-weight: 500; + color: var(--gallery-text); + transition: all 0.3s ease; +} + +.category-pill:hover, +.category-pill.active { + background: var(--gallery-accent); + color: white; + border-color: var(--gallery-accent); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .gallery-page { + --gallery-radius: 16px; + --gallery-spacing: 12px; + --glass-blur: 16px; + } + + .photo-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + .photo-card img { + height: 180px; + } +} + +/* ===== Accessibility: Reduce Motion ===== */ +@media (prefers-reduced-motion: reduce) { + .photo-card, + .gallery-btn { + transition: none; + } + + .photo-card:hover { + transform: none; + } +} + +/* ===== Accessibility: Reduce Transparency ===== */ +@media (prefers-reduced-transparency: reduce) { + .glass-surface, + .photo-card, + .gallery-btn { + backdrop-filter: none; + -webkit-backdrop-filter: none; + background: rgba(255, 255, 255, 0.95); + } +}`; + +const LIQUID_GLASS_DARK = `/* + * PicPeak Custom CSS Template: Liquid Glass Dark + * Inspired by Apple's iOS 26 Liquid Glass Design Language + * + * Features: + * - Deep translucent dark surfaces + * - Neon accent highlights + * - Dramatic glass reflections + * - Subtle animated gradients + */ + +/* ===== Base Theme Variables ===== */ +.gallery-page { + --glass-bg: rgba(15, 15, 35, 0.7); + --glass-bg-elevated: rgba(25, 25, 55, 0.85); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-border-highlight: rgba(255, 255, 255, 0.2); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --glass-blur: 24px; + --glass-saturation: 150%; + + --gallery-bg: #0a0a1a; + --gallery-text: #f0f0f5; + --gallery-text-muted: rgba(240, 240, 245, 0.6); + --gallery-accent: #00d4ff; + --gallery-accent-secondary: #ff00e5; + --gallery-accent-hover: #00ffea; + --gallery-radius: 20px; + --gallery-spacing: 20px; + + /* Neon glow variables */ + --neon-glow: 0 0 20px rgba(0, 212, 255, 0.5), 0 0 40px rgba(0, 212, 255, 0.2); + --neon-glow-secondary: 0 0 20px rgba(255, 0, 229, 0.5), 0 0 40px rgba(255, 0, 229, 0.2); +} + +/* ===== Page Background ===== */ +.gallery-page { + background: var(--gallery-bg); + min-height: 100vh; + position: relative; + overflow-x: hidden; +} + +/* Animated mesh gradient background */ +.gallery-page::before { + content: ''; + position: fixed; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: + radial-gradient(circle at 30% 20%, rgba(0, 212, 255, 0.15) 0%, transparent 40%), + radial-gradient(circle at 70% 80%, rgba(255, 0, 229, 0.1) 0%, transparent 40%), + radial-gradient(circle at 50% 50%, rgba(100, 100, 255, 0.05) 0%, transparent 60%); + animation: gradientShift 20s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +@keyframes gradientShift { + 0%, 100% { transform: translate(0, 0) rotate(0deg); } + 25% { transform: translate(2%, 2%) rotate(1deg); } + 50% { transform: translate(-1%, 3%) rotate(-1deg); } + 75% { transform: translate(3%, -2%) rotate(2deg); } +} + +/* ===== Gallery Header ===== */ +.gallery-header { + background: var(--glass-bg-elevated); + backdrop-filter: blur(30px) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturation)); + border-bottom: 1px solid var(--glass-border-highlight); + padding: calc(var(--gallery-spacing) * 1.5); + position: sticky; + top: 0; + z-index: 100; + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.gallery-title { + color: var(--gallery-text); + font-weight: 700; + font-size: 1.75rem; + letter-spacing: -0.02em; + background: linear-gradient(135deg, var(--gallery-text) 0%, var(--gallery-accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ===== Photo Grid ===== */ +.photo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--gallery-spacing); + padding: calc(var(--gallery-spacing) * 2); + position: relative; + z-index: 1; +} + +/* ===== Photo Cards - Dark Glass Style ===== */ +.photo-card { + position: relative; + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturation)); + border: 1px solid var(--glass-border); + border-radius: var(--gallery-radius); + overflow: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 24px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.05); +} + +/* Top highlight reflection */ +.photo-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.3) 50%, + transparent 100% + ); + z-index: 2; +} + +/* Inner glow effect */ +.photo-card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--gallery-radius); + padding: 1px; + background: linear-gradient( + 135deg, + rgba(0, 212, 255, 0) 0%, + rgba(0, 212, 255, 0) 40%, + rgba(0, 212, 255, 0.1) 100% + ); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0; + transition: opacity 0.4s ease; +} + +.photo-card:hover { + transform: translateY(-8px) scale(1.02); + border-color: var(--glass-border-highlight); + box-shadow: + 0 24px 48px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(0, 212, 255, 0.2), + var(--neon-glow); +} + +.photo-card:hover::after { + opacity: 1; +} + +.photo-card img { + width: 100%; + height: 240px; + object-fit: cover; + transition: transform 0.4s ease, filter 0.4s ease; + filter: brightness(0.9); +} + +.photo-card:hover img { + transform: scale(1.05); + filter: brightness(1); +} + +.photo-card-info { + padding: var(--gallery-spacing); + background: linear-gradient( + 180deg, + rgba(0, 0, 0, 0.2) 0%, + rgba(0, 0, 0, 0.4) 100% + ); + color: var(--gallery-text); +} + +.photo-card-info p { + color: var(--gallery-text-muted); + font-size: 0.875rem; +} + +/* ===== Buttons - Neon Glass Style ===== */ +.gallery-btn { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: calc(var(--gallery-radius) / 2); + padding: 12px 24px; + color: var(--gallery-text); + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.gallery-btn:hover { + border-color: var(--gallery-accent); + box-shadow: var(--neon-glow); + color: var(--gallery-accent); +} + +.gallery-btn-primary { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + color: white; + border: none; + box-shadow: var(--neon-glow); +} + +.gallery-btn-primary:hover { + box-shadow: + 0 0 30px rgba(0, 212, 255, 0.6), + 0 0 60px rgba(0, 212, 255, 0.3), + 0 0 90px rgba(255, 0, 229, 0.2); + transform: translateY(-2px); +} + +/* ===== Lightbox - Dark Glass ===== */ +.lightbox-overlay { + background: rgba(5, 5, 15, 0.9); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); +} + +.lightbox-content { + background: var(--glass-bg-elevated); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid var(--glass-border-highlight); + border-radius: var(--gallery-radius); + box-shadow: + 0 24px 80px rgba(0, 0, 0, 0.5), + var(--neon-glow); +} + +/* ===== Category Pills ===== */ +.category-pill { + background: var(--glass-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + border-radius: 9999px; + padding: 8px 20px; + font-size: 0.875rem; + font-weight: 500; + color: var(--gallery-text-muted); + transition: all 0.3s ease; +} + +.category-pill:hover { + border-color: var(--gallery-accent); + color: var(--gallery-accent); + box-shadow: var(--neon-glow); +} + +.category-pill.active { + background: linear-gradient(135deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + color: white; + border-color: transparent; + box-shadow: var(--neon-glow); +} + +/* ===== Scrollbar Styling ===== */ +.gallery-page ::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.gallery-page ::-webkit-scrollbar-track { + background: var(--glass-bg); + border-radius: 4px; +} + +.gallery-page ::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--gallery-accent) 0%, var(--gallery-accent-secondary) 100%); + border-radius: 4px; +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .gallery-page { + --gallery-radius: 16px; + --gallery-spacing: 12px; + --glass-blur: 16px; + } + + .photo-grid { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + .photo-card img { + height: 180px; + } + + /* Reduce animation complexity on mobile */ + .gallery-page::before { + animation: none; + } +} + +/* ===== Accessibility: Reduce Motion ===== */ +@media (prefers-reduced-motion: reduce) { + .gallery-page::before { + animation: none; + } + + .photo-card, + .gallery-btn { + transition: none; + } + + .photo-card:hover { + transform: none; + } +} + +/* ===== Accessibility: Reduce Transparency ===== */ +@media (prefers-reduced-transparency: reduce) { + .photo-card, + .gallery-btn, + .gallery-header { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .gallery-page { + --glass-bg: rgba(20, 20, 40, 0.98); + --glass-bg-elevated: rgba(30, 30, 60, 0.98); + } +}`; + +exports.up = async function(knex) { + // Update template slot 2 with Liquid Glass Light + await knex('css_templates') + .where({ slot_number: 2 }) + .update({ + name: 'Liquid Glass Light', + css_content: LIQUID_GLASS_LIGHT, + is_enabled: true, + is_default: false, + updated_at: knex.fn.now() + }); + + // Update template slot 3 with Liquid Glass Dark + await knex('css_templates') + .where({ slot_number: 3 }) + .update({ + name: 'Liquid Glass Dark', + css_content: LIQUID_GLASS_DARK, + is_enabled: true, + is_default: false, + updated_at: knex.fn.now() + }); +}; + +exports.down = async function(knex) { + // Revert to empty templates + await knex('css_templates') + .where({ slot_number: 2 }) + .update({ + name: 'Untitled', + css_content: '', + is_enabled: false, + is_default: false, + updated_at: knex.fn.now() + }); + + await knex('css_templates') + .where({ slot_number: 3 }) + .update({ + name: 'Untitled', + css_content: '', + is_enabled: false, + is_default: false, + updated_at: knex.fn.now() + }); +}; + +// Export templates for use elsewhere +module.exports.LIQUID_GLASS_LIGHT = LIQUID_GLASS_LIGHT; +module.exports.LIQUID_GLASS_DARK = LIQUID_GLASS_DARK; diff --git a/backend/server.js b/backend/server.js index fb9f9e6c..d49556a5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -25,6 +25,7 @@ const { startBackupService } = require('./src/services/backupService'); const { startScheduledBackups } = require('./src/services/databaseBackup'); const { maintenanceMiddleware } = require('./src/middleware/maintenance'); const { sessionTimeoutMiddleware } = require('./src/middleware/sessionTimeout'); +const { errorHandler, notFoundHandler } = require('./src/middleware/errorHandler'); const { createRateLimiter, createAuthRateLimiter } = require('./src/services/rateLimitService'); const { getPublicSitePayload } = require('./src/services/publicSiteService'); const cookieParser = require('cookie-parser'); @@ -471,20 +472,11 @@ try { logger.warn('Failed to enable frontend static serving', { error: e.message }); } -// Error handling middleware -app.use((err, req, res, next) => { - console.error('EXPRESS ERROR HANDLER:', err); - console.error('Error stack:', err.stack); - console.error('Request URL:', req.url); - console.error('Request method:', req.method); - logger.error('Express error handler:', { - message: err.message, - stack: err.stack, - url: req.url, - method: req.method - }); - res.status(500).json({ error: 'Something went wrong!', details: err.message }); -}); +// 404 handler for undefined API routes +app.use('/api', notFoundHandler); + +// Global error handler (must be last) +app.use(errorHandler); // Initialize services async function startServer() { diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js new file mode 100644 index 00000000..782a2370 --- /dev/null +++ b/backend/src/middleware/errorHandler.js @@ -0,0 +1,183 @@ +/** + * Global error handler middleware. + * Catches all errors and returns standardized responses. + * Distinguishes between operational errors (expected) and programming errors (bugs). + */ + +const logger = require('../utils/logger'); +const { AppError } = require('../utils/errors'); + +/** + * Determines if an error is operational (expected) or a programming error (bug). + * Operational errors are expected failures like validation errors, not found, etc. + * Programming errors are bugs that should be logged and investigated. + * + * @param {Error} err - The error to check + * @returns {boolean} True if operational error + */ +const isOperationalError = (err) => { + return err instanceof AppError && err.isOperational; +}; + +/** + * Formats error for development environment (includes stack trace). + * + * @param {Error} err - The error object + * @returns {Object} Formatted error response + */ +const formatDevError = (err) => { + return { + error: err.message, + code: err.code || 'INTERNAL_ERROR', + stack: err.stack, + ...(err.details && { details: err.details }), + ...(err.field && { field: err.field }) + }; +}; + +/** + * Formats error for production environment (hides sensitive details). + * + * @param {Error} err - The error object + * @param {boolean} isOperational - Whether this is an operational error + * @returns {Object} Formatted error response + */ +const formatProdError = (err, isOperational) => { + // For operational errors, show the message + if (isOperational) { + return { + error: err.message, + code: err.code || 'ERROR', + ...(err.details && { details: err.details }), + ...(err.field && { field: err.field }) + }; + } + + // For programming errors, hide details + return { + error: 'An unexpected error occurred', + code: 'INTERNAL_ERROR' + }; +}; + +/** + * Handles specific error types and converts them to AppError format. + * + * @param {Error} err - The error to handle + * @returns {Error} Converted error or original error + */ +const handleKnownErrors = (err) => { + // Handle Knex/Database errors + if (err.code === 'SQLITE_CONSTRAINT' || err.code === '23505') { + const { AppError } = require('../utils/errors'); + const error = new AppError('A record with this value already exists', 409, 'DUPLICATE_ENTRY'); + error.isOperational = true; + return error; + } + + // Handle JSON parsing errors + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + const { ValidationError } = require('../utils/errors'); + return new ValidationError('Invalid JSON in request body'); + } + + // Handle multer file upload errors + if (err.code === 'LIMIT_FILE_SIZE') { + const { ValidationError } = require('../utils/errors'); + return new ValidationError('File size exceeds the maximum allowed limit'); + } + + if (err.code === 'LIMIT_UNEXPECTED_FILE') { + const { ValidationError } = require('../utils/errors'); + return new ValidationError('Unexpected file field'); + } + + return err; +}; + +/** + * Global error handler middleware. + * Must be registered last, after all routes. + * + * @param {Error} err - The error object + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @param {Function} next - Express next function + */ +const errorHandler = (err, req, res, next) => { + // If headers already sent, delegate to Express default handler + if (res.headersSent) { + return next(err); + } + + // Convert known error types + const error = handleKnownErrors(err); + + // Determine error status code + const statusCode = error.statusCode || error.status || 500; + const operational = isOperationalError(error); + + // Log the error + const logContext = { + url: req.originalUrl, + method: req.method, + ip: req.ip, + statusCode, + errorCode: error.code, + operational, + ...(req.admin && { adminId: req.admin.id }), + ...(req.gallerySlug && { gallerySlug: req.gallerySlug }) + }; + + if (operational) { + // Operational errors are expected, log at warn level + logger.warn('Operational error', { + ...logContext, + message: error.message + }); + } else { + // Programming errors are bugs, log at error level with stack + logger.error('Unhandled error', { + ...logContext, + message: error.message, + stack: error.stack + }); + } + + // Format and send response + const isDev = process.env.NODE_ENV === 'development'; + const response = isDev ? formatDevError(error) : formatProdError(error, operational); + + res.status(statusCode).json(response); +}; + +/** + * 404 handler for undefined routes. + * Should be registered after all routes but before errorHandler. + * + * @param {Request} req - Express request object + * @param {Response} res - Express response object + * @param {Function} next - Express next function + */ +const notFoundHandler = (req, res, next) => { + const { NotFoundError } = require('../utils/errors'); + next(new NotFoundError('Route', req.originalUrl)); +}; + +/** + * Async handler that catches unhandled promise rejections. + * Use this to wrap async route handlers. + * + * @param {Function} fn - Async function to wrap + * @returns {Function} Wrapped function + */ +const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +module.exports = { + errorHandler, + notFoundHandler, + asyncHandler, + isOperationalError +}; diff --git a/backend/src/routes/adminAuth.js b/backend/src/routes/adminAuth.js index 0b31888c..dfc5c183 100644 --- a/backend/src/routes/adminAuth.js +++ b/backend/src/routes/adminAuth.js @@ -1,31 +1,29 @@ const express = require('express'); const bcrypt = require('bcrypt'); -const { body, validationResult } = require('express-validator'); +const { body } = require('express-validator'); const { db, logActivity } = require('../database/db'); const { adminAuth } = require('../middleware/auth'); const { endSession } = require('../middleware/sessionTimeout'); const { validatePasswordStrength } = require('../utils/passwordGenerator'); +const { handleAsync, validateRequest, successResponse } = require('../utils/routeHelpers'); +const { NotFoundError, ConflictError, ValidationError } = require('../utils/errors'); const router = express.Router(); -// Change password -router.get('/profile', adminAuth, async (req, res) => { - try { - const admin = await db('admin_users') - .where('id', req.admin.id) - .select('id', 'username', 'email', 'last_login', 'last_login_ip', 'created_at', 'updated_at', 'must_change_password as mustChangePassword') - .first(); +// Get admin profile +router.get('/profile', adminAuth, handleAsync(async (req, res) => { + const admin = await db('admin_users') + .where('id', req.admin.id) + .select('id', 'username', 'email', 'last_login', 'last_login_ip', 'created_at', 'updated_at', 'must_change_password as mustChangePassword') + .first(); - if (!admin) { - return res.status(404).json({ error: 'Admin user not found' }); - } - - res.json(admin); - } catch (error) { - console.error('Admin profile fetch error:', error); - res.status(500).json({ error: 'Failed to fetch admin profile' }); + if (!admin) { + throw new NotFoundError('Admin user'); } -}); + res.json(admin); +})); + +// Update admin profile router.put('/profile', [ adminAuth, body('username') @@ -37,212 +35,129 @@ router.put('/profile', [ .isEmail() .withMessage('A valid email address is required') .normalizeEmail() -], async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } +], handleAsync(async (req, res) => { + validateRequest(req); - const username = req.body.username.trim(); - const email = req.body.email.trim().toLowerCase(); - const adminId = req.admin.id; + const username = req.body.username.trim(); + const email = req.body.email.trim().toLowerCase(); + const adminId = req.admin.id; - const existingUsername = await db('admin_users') - .where('username', username) - .whereNot('id', adminId) - .first(); + // Check for username conflict + const existingUsername = await db('admin_users') + .where('username', username) + .whereNot('id', adminId) + .first(); - if (existingUsername) { - return res.status(409).json({ error: 'Username is already in use' }); - } - - const existingEmail = await db('admin_users') - .where('email', email) - .whereNot('id', adminId) - .first(); - - if (existingEmail) { - return res.status(409).json({ error: 'Email address is already in use' }); - } - - await db('admin_users') - .where('id', adminId) - .update({ - username, - email, - updated_at: new Date() - }); - - await logActivity('admin_profile_updated', - { username, email }, - null, - { type: 'admin', id: adminId, name: req.admin.username } - ); - - const updatedAdmin = await db('admin_users') - .where('id', adminId) - .select('id', 'username', 'email', 'must_change_password as mustChangePassword') - .first(); - - res.json({ - message: 'Admin profile updated successfully', - user: updatedAdmin - }); - } catch (error) { - console.error('Admin profile update error:', error); - res.status(500).json({ error: 'Failed to update admin profile' }); + if (existingUsername) { + throw new ConflictError('Username is already in use', 'username'); } -}); + // Check for email conflict + const existingEmail = await db('admin_users') + .where('email', email) + .whereNot('id', adminId) + .first(); + + if (existingEmail) { + throw new ConflictError('Email address is already in use', 'email'); + } + + await db('admin_users') + .where('id', adminId) + .update({ + username, + email, + updated_at: new Date() + }); + + await logActivity('admin_profile_updated', + { username, email }, + null, + { type: 'admin', id: adminId, name: req.admin.username } + ); + + const updatedAdmin = await db('admin_users') + .where('id', adminId) + .select('id', 'username', 'email', 'must_change_password as mustChangePassword') + .first(); + + successResponse(res, { + message: 'Admin profile updated successfully', + user: updatedAdmin + }); +})); + +// Change password router.post('/change-password', [ adminAuth, body('currentPassword').notEmpty().withMessage('Current password is required'), body('newPassword').isLength({ min: 12 }).withMessage('New password must be at least 12 characters') -], async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } +], handleAsync(async (req, res) => { + validateRequest(req); - const { currentPassword, newPassword } = req.body; - const userId = req.admin.id; // Changed from req.user.id to req.admin.id + const { currentPassword, newPassword } = req.body; + const userId = req.admin.id; - // Validate new password strength - const passwordValidation = validatePasswordStrength(newPassword); - if (!passwordValidation.isValid) { - return res.status(400).json({ - error: 'Password does not meet security requirements', - details: passwordValidation.messages - }); - } - - // Get user from database - const user = await db('admin_users') - .where('id', userId) - .first(); - - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - - // Verify current password - const validPassword = await bcrypt.compare(currentPassword, user.password_hash); - if (!validPassword) { - return res.status(400).json({ error: 'Current password is incorrect' }); - } - - // Hash new password with more rounds - const newPasswordHash = await bcrypt.hash(newPassword, 12); - - // Update password and clear must_change_password flag - await db('admin_users') - .where('id', userId) - .update({ - password_hash: newPasswordHash, - must_change_password: false, - updated_at: new Date() - }); - - // Log activity - await logActivity('password_changed', - { admin_id: userId }, - null, - { type: 'admin', id: userId, name: user.username } - ); - - res.json({ message: 'Password changed successfully' }); - } catch (error) { - console.error('Password change error:', error); - res.status(500).json({ error: 'Failed to change password' }); + // Validate new password strength + const passwordValidation = validatePasswordStrength(newPassword); + if (!passwordValidation.isValid) { + throw new ValidationError('Password does not meet security requirements', passwordValidation.messages); } -}); -// Update admin profile -router.put('/profile', [ - adminAuth, - body('username').trim().notEmpty().withMessage('Username is required'), - body('email').trim().isEmail().withMessage('Valid email is required') -], async (req, res) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } + // Get user from database + const user = await db('admin_users') + .where('id', userId) + .first(); - const { username, email } = req.body; - const userId = req.admin.id; - - // Check for email conflicts - const existingEmail = await db('admin_users') - .where('email', email) - .whereNot('id', userId) - .first(); - - if (existingEmail) { - return res.status(409).json({ error: 'Email is already in use by another admin' }); - } - - // Check username conflict (if multiple admins are supported) - const existingUsername = await db('admin_users') - .where('username', username) - .whereNot('id', userId) - .first(); - - if (existingUsername) { - return res.status(409).json({ error: 'Username is already in use by another admin' }); - } - - await db('admin_users') - .where('id', userId) - .update({ - username, - email, - updated_at: new Date() - }); - - const updatedUser = await db('admin_users') - .select('id', 'username', 'email', 'must_change_password') - .where('id', userId) - .first(); - - await logActivity( - 'admin_profile_updated', - { admin_id: userId, updated_fields: ['username', 'email'] }, - null, - { type: 'admin', id: userId, name: username } - ); - - res.json({ user: updatedUser }); - } catch (error) { - console.error('Admin profile update error:', error); - res.status(500).json({ error: 'Failed to update admin profile' }); + if (!user) { + throw new NotFoundError('User'); } -}); + + // Verify current password + const validPassword = await bcrypt.compare(currentPassword, user.password_hash); + if (!validPassword) { + throw new ValidationError('Current password is incorrect'); + } + + // Hash new password with more rounds + const newPasswordHash = await bcrypt.hash(newPassword, 12); + + // Update password and clear must_change_password flag + await db('admin_users') + .where('id', userId) + .update({ + password_hash: newPasswordHash, + must_change_password: false, + updated_at: new Date() + }); + + // Log activity + await logActivity('password_changed', + { admin_id: userId }, + null, + { type: 'admin', id: userId, name: user.username } + ); + + successResponse(res, { message: 'Password changed successfully' }); +})); // Logout -router.post('/logout', adminAuth, async (req, res) => { - try { - // Get token from header - const token = req.headers.authorization?.split(' ')[1]; - if (token) { - // End the session - endSession(token); - } - - // Log activity - await logActivity('admin_logout', - { admin_id: req.admin.id }, - null, - { type: 'admin', id: req.admin.id, name: req.admin.username } - ); - - res.json({ message: 'Logged out successfully' }); - } catch (error) { - console.error('Logout error:', error); - res.status(500).json({ error: 'Failed to logout' }); +router.post('/logout', adminAuth, handleAsync(async (req, res) => { + // Get token from header + const token = req.headers.authorization?.split(' ')[1]; + if (token) { + // End the session + endSession(token); } -}); + + // Log activity + await logActivity('admin_logout', + { admin_id: req.admin.id }, + null, + { type: 'admin', id: req.admin.id, name: req.admin.username } + ); + + successResponse(res, { message: 'Logged out successfully' }); +})); module.exports = router; diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 2b0a61b3..0b136d10 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -141,7 +141,8 @@ router.post('/', adminAuth, [ body('allow_downloads').optional().isBoolean(), body('disable_right_click').optional().isBoolean(), body('watermark_downloads').optional().isBoolean(), - body('watermark_text').optional().trim() + body('watermark_text').optional().trim(), + body('css_template_id').optional({ nullable: true, checkFalsy: true }).isInt() ], async (req, res) => { try { logger.debug('Create event request body', { body: req.body }); @@ -178,7 +179,9 @@ router.post('/', adminAuth, [ allow_favorites = true, require_name_email = false, moderate_comments = true, - show_feedback_to_guests = true + show_feedback_to_guests = true, + // CSS Template + css_template_id = null } = req.body; const customerName = getCustomerNameFromPayload(req.body); @@ -299,7 +302,8 @@ router.post('/', adminAuth, [ disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false), watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false), watermark_text, - require_password: formatBoolean(requirePassword) + require_password: formatBoolean(requirePassword), + css_template_id: css_template_id || null }).returning('id'); // Handle both PostgreSQL (returns array of objects) and SQLite (returns array of IDs) diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index eb72bd46..08e98c7e 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -10,6 +10,8 @@ const secureImageService = require('../services/secureImageService'); const logger = require('../utils/logger'); const { resolvePhotoFilePath } = require('../services/photoResolver'); const { getEventShareToken, resolveShareIdentifier, buildShareLinkVariants } = require('../services/shareLinkService'); +const { handleAsync } = require('../utils/routeHelpers'); +const { NotFoundError } = require('../utils/errors'); // Get storage path from environment or default const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../storage'); @@ -32,69 +34,59 @@ async function checkSlugRedirect(slug) { } // Resolve gallery identifier (slug or token) to canonical data -router.get('/resolve/:identifier', async (req, res) => { - try { - const { identifier } = req.params; - let result = await resolveShareIdentifier(identifier); +router.get('/resolve/:identifier', handleAsync(async (req, res) => { + const { identifier } = req.params; + let result = await resolveShareIdentifier(identifier); - // If not found, check for redirect - if (!result) { - const newSlug = await checkSlugRedirect(identifier); - if (newSlug) { - return res.status(301).json({ - redirect: true, - newSlug, - message: 'Gallery has been renamed' - }); - } - return res.status(404).json({ error: 'Gallery not found' }); + // If not found, check for redirect + if (!result) { + const newSlug = await checkSlugRedirect(identifier); + if (newSlug) { + return res.status(301).json({ + redirect: true, + newSlug, + message: 'Gallery has been renamed' + }); } - - const { event, matchType, shareToken } = result; - const linkVariants = await buildShareLinkVariants({ slug: event.slug, shareToken }); - const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0'); - - res.json({ - slug: event.slug, - token: shareToken, - matchType, - share_link: event.share_link, - share_path: linkVariants.sharePath, - share_url: linkVariants.shareUrl, - short_enabled: linkVariants.shortEnabled, - requires_password: requiresPassword - }); - } catch (error) { - logger.error('Error resolving gallery identifier:', error); - res.status(500).json({ error: 'Failed to resolve gallery link' }); + throw new NotFoundError('Gallery'); } -}); + + const { event, matchType, shareToken } = result; + const linkVariants = await buildShareLinkVariants({ slug: event.slug, shareToken }); + const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0'); + + res.json({ + slug: event.slug, + token: shareToken, + matchType, + share_link: event.share_link, + share_path: linkVariants.sharePath, + share_url: linkVariants.shareUrl, + short_enabled: linkVariants.shortEnabled, + requires_password: requiresPassword + }); +})); // Verify share token -router.get('/:slug/verify-token/:token', async (req, res) => { - try { - const { slug, token } = req.params; - - const event = await db('events') - .where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) - .select('id', 'share_link', 'share_token') - .first(); - - if (!event) { - return res.status(404).json({ error: 'Gallery not found' }); - } - - const expectedToken = getEventShareToken(event); - if (token !== expectedToken) { - return res.status(404).json({ error: 'Invalid gallery link' }); - } - - res.json({ valid: true }); - } catch (error) { - console.error('Error verifying token:', error); - res.status(500).json({ error: 'Failed to verify token' }); +router.get('/:slug/verify-token/:token', handleAsync(async (req, res) => { + const { slug, token } = req.params; + + const event = await db('events') + .where({ slug, is_active: formatBoolean(true), is_archived: formatBoolean(false) }) + .select('id', 'share_link', 'share_token') + .first(); + + if (!event) { + throw new NotFoundError('Gallery'); } -}); + + const expectedToken = getEventShareToken(event); + if (token !== expectedToken) { + throw new NotFoundError('Gallery', 'Invalid gallery link'); + } + + res.json({ valid: true }); +})); // Get gallery info (with optional token verification) router.get('/:slug/info', async (req, res) => { diff --git a/backend/src/utils/errors.js b/backend/src/utils/errors.js new file mode 100644 index 00000000..ff676d29 --- /dev/null +++ b/backend/src/utils/errors.js @@ -0,0 +1,121 @@ +/** + * Custom error classes for standardized error handling across the application. + * These errors are caught by the global error handler and converted to appropriate HTTP responses. + */ + +/** + * Base class for operational errors (expected errors that can occur during normal operation) + */ +class AppError extends Error { + constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') { + super(message); + this.statusCode = statusCode; + this.code = code; + this.isOperational = true; + Error.captureStackTrace(this, this.constructor); + } + + toJSON() { + return { + error: this.message, + code: this.code, + ...(process.env.NODE_ENV === 'development' && { stack: this.stack }) + }; + } +} + +/** + * Validation error - for invalid input data (400 Bad Request) + */ +class ValidationError extends AppError { + constructor(message = 'Validation failed', details = null) { + super(message, 400, 'VALIDATION_ERROR'); + this.details = details; + } + + toJSON() { + return { + ...super.toJSON(), + ...(this.details && { details: this.details }) + }; + } +} + +/** + * Not found error - for resources that don't exist (404 Not Found) + */ +class NotFoundError extends AppError { + constructor(resource = 'Resource', identifier = null) { + const message = identifier + ? `${resource} with identifier '${identifier}' not found` + : `${resource} not found`; + super(message, 404, 'NOT_FOUND'); + this.resource = resource; + this.identifier = identifier; + } +} + +/** + * Unauthorized error - for missing or invalid authentication (401 Unauthorized) + */ +class UnauthorizedError extends AppError { + constructor(message = 'Authentication required') { + super(message, 401, 'UNAUTHORIZED'); + } +} + +/** + * Forbidden error - for insufficient permissions (403 Forbidden) + */ +class ForbiddenError extends AppError { + constructor(message = 'Access denied') { + super(message, 403, 'FORBIDDEN'); + } +} + +/** + * Conflict error - for resource conflicts (409 Conflict) + */ +class ConflictError extends AppError { + constructor(message = 'Resource conflict', field = null) { + super(message, 409, 'CONFLICT'); + this.field = field; + } + + toJSON() { + return { + ...super.toJSON(), + ...(this.field && { field: this.field }) + }; + } +} + +/** + * Rate limit error - for too many requests (429 Too Many Requests) + */ +class RateLimitError extends AppError { + constructor(message = 'Too many requests', retryAfter = null) { + super(message, 429, 'RATE_LIMIT_EXCEEDED'); + this.retryAfter = retryAfter; + } +} + +/** + * Service unavailable error - for maintenance mode or service issues (503 Service Unavailable) + */ +class ServiceUnavailableError extends AppError { + constructor(message = 'Service temporarily unavailable') { + super(message, 503, 'SERVICE_UNAVAILABLE'); + } +} + +module.exports = { + AppError, + ValidationError, + NotFoundError, + UnauthorizedError, + ForbiddenError, + ConflictError, + RateLimitError, + ServiceUnavailableError +}; diff --git a/backend/src/utils/routeHelpers.js b/backend/src/utils/routeHelpers.js new file mode 100644 index 00000000..d29e9913 --- /dev/null +++ b/backend/src/utils/routeHelpers.js @@ -0,0 +1,180 @@ +/** + * Route helper utilities for standardized request handling. + * Provides async error wrapping, validation, and response formatting. + */ + +const { validationResult } = require('express-validator'); +const { ValidationError } = require('./errors'); + +/** + * Wraps an async route handler to catch errors and pass them to the error handler. + * Eliminates the need for try/catch blocks in every route. + * + * @param {Function} fn - Async route handler function + * @returns {Function} Express middleware function + * + * @example + * router.get('/events', handleAsync(async (req, res) => { + * const events = await eventService.getAll(); + * res.json(events); + * })); + */ +const handleAsync = (fn) => { + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; + +/** + * Validates the request using express-validator and throws ValidationError if invalid. + * Should be called at the beginning of route handlers after validation middleware. + * + * @param {Request} req - Express request object + * @throws {ValidationError} If validation fails + * + * @example + * router.post('/events', [ + * body('name').notEmpty(), + * body('date').isDate() + * ], handleAsync(async (req, res) => { + * validateRequest(req); + * // ... rest of handler + * })); + */ +const validateRequest = (req) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + const errorDetails = errors.array().map(err => ({ + field: err.path || err.param, + message: err.msg + })); + throw new ValidationError('Validation failed', errorDetails); + } +}; + +/** + * Sends a standardized success response. + * + * @param {Response} res - Express response object + * @param {*} data - Data to send in the response + * @param {number} [statusCode=200] - HTTP status code + * @param {string} [message] - Optional success message + * + * @example + * successResponse(res, { event }, 201, 'Event created successfully'); + */ +const successResponse = (res, data, statusCode = 200, message = null) => { + const response = message ? { message, ...data } : data; + res.status(statusCode).json(response); +}; + +/** + * Sends a standardized error response. + * Note: Prefer throwing custom errors and letting the error handler format the response. + * + * @param {Response} res - Express response object + * @param {string} message - Error message + * @param {number} [statusCode=500] - HTTP status code + * @param {string} [code] - Optional error code + * @param {*} [details] - Optional additional error details + * + * @example + * errorResponse(res, 'Invalid input', 400, 'VALIDATION_ERROR', { field: 'email' }); + */ +const errorResponse = (res, message, statusCode = 500, code = null, details = null) => { + const response = { + error: message, + ...(code && { code }), + ...(details && { details }) + }; + res.status(statusCode).json(response); +}; + +/** + * Creates a route handler with built-in validation. + * Combines handleAsync and validateRequest for cleaner route definitions. + * + * @param {Function} fn - Async route handler function + * @returns {Function} Express middleware function + * + * @example + * router.post('/events', [ + * body('name').notEmpty() + * ], withValidation(async (req, res) => { + * const event = await eventService.create(req.body); + * successResponse(res, { event }, 201); + * })); + */ +const withValidation = (fn) => { + return handleAsync(async (req, res, next) => { + validateRequest(req); + return fn(req, res, next); + }); +}; + +/** + * Extracts pagination parameters from query string with defaults. + * + * @param {Request} req - Express request object + * @param {Object} [defaults] - Default values + * @param {number} [defaults.page=1] - Default page number + * @param {number} [defaults.limit=20] - Default items per page + * @param {number} [defaults.maxLimit=100] - Maximum allowed limit + * @returns {{ page: number, limit: number, offset: number }} + * + * @example + * const { page, limit, offset } = getPagination(req); + * const events = await db('events').limit(limit).offset(offset); + */ +const getPagination = (req, defaults = {}) => { + const { page: defaultPage = 1, limit: defaultLimit = 20, maxLimit = 100 } = defaults; + + let page = parseInt(req.query.page, 10) || defaultPage; + let limit = parseInt(req.query.limit, 10) || defaultLimit; + + // Ensure valid values + page = Math.max(1, page); + limit = Math.min(Math.max(1, limit), maxLimit); + + const offset = (page - 1) * limit; + + return { page, limit, offset }; +}; + +/** + * Creates a paginated response with metadata. + * + * @param {*} data - Data array + * @param {number} total - Total count of items + * @param {number} page - Current page + * @param {number} limit - Items per page + * @returns {Object} Paginated response object + * + * @example + * const events = await db('events').limit(limit).offset(offset); + * const total = await db('events').count('* as count').first(); + * res.json(paginatedResponse(events, total.count, page, limit)); + */ +const paginatedResponse = (data, total, page, limit) => { + const totalPages = Math.ceil(total / limit); + return { + data, + pagination: { + page, + limit, + total, + totalPages, + hasMore: page < totalPages + } + }; +}; + +module.exports = { + handleAsync, + validateRequest, + successResponse, + errorResponse, + withValidation, + getPagination, + paginatedResponse +}; diff --git a/frontend/TEST_PLAN.md b/frontend/TEST_PLAN.md new file mode 100644 index 00000000..863443ae --- /dev/null +++ b/frontend/TEST_PLAN.md @@ -0,0 +1,1022 @@ +# PicPeak E2E Test Plan + +## Overview + +This document provides a comprehensive end-to-end test plan for the PicPeak photo sharing platform. All tests are designed to be executed using **Playwright MCP Browser** or **Chrome DevTools MCP** tools. + +## Prerequisites + +### Environment Setup +- [ ] Frontend dev server running on `http://localhost:5173` +- [ ] Backend server running on `http://localhost:3001` +- [ ] Database seeded with test data +- [ ] Admin credentials available (default: admin/admin) + +### MCP Tools Required +- `mcp__playwright__browser_navigate` - Navigate to URLs +- `mcp__playwright__browser_snapshot` - Capture page state +- `mcp__playwright__browser_click` - Click elements +- `mcp__playwright__browser_fill_form` - Fill form fields +- `mcp__playwright__browser_type` - Type text +- `mcp__playwright__browser_console_messages` - Check console errors +- `mcp__chrome-devtools__list_console_messages` - Alternative console check +- `mcp__chrome-devtools__list_network_requests` - Monitor API calls + +### Test Data Requirements +- At least 2 active events with photos +- At least 1 expired event +- At least 1 archived event (optional) +- Categories configured +- Email templates configured + +--- + +## Test Execution Methodology + +### Before Each Test Section +``` +1. Navigate to the target page +2. Take a snapshot to verify page loaded +3. Check console for errors: browser_console_messages(level: "error") +4. Verify no network request failures +``` + +### After Each Test Section +``` +1. Take final snapshot +2. Check console for new errors +3. Document any issues found +``` + +--- + +## 1. Authentication Tests + +### 1.1 Admin Login +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| AUTH-001 | Valid login | Navigate to `/admin/login`, enter valid credentials, click Login | Redirect to dashboard, session created | +| AUTH-002 | Invalid password | Enter wrong password | Error message displayed, no redirect | +| AUTH-003 | Empty fields | Submit with empty fields | Validation errors shown | +| AUTH-004 | Session persistence | Login, refresh page | Stay logged in | +| AUTH-005 | Logout | Click logout button | Redirect to login, session cleared | + +### 1.2 Gallery Authentication +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAUTH-001 | Valid gallery password | Navigate to gallery, enter correct password | Gallery content displayed | +| GAUTH-002 | Invalid gallery password | Enter wrong password | Error message, access denied | +| GAUTH-003 | Token-based access | Access gallery with token in URL | Direct access without password | +| GAUTH-004 | Expired gallery access | Access expired gallery | Appropriate expiration message | + +--- + +## 2. Dashboard Tests + +### 2.1 Dashboard Display +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DASH-001 | Stats display | Navigate to `/admin/dashboard` | All stat cards show correct values | +| DASH-002 | Active Events count | Check "Active Events" card | Matches actual active events | +| DASH-003 | Expiring Soon count | Check "Expiring Soon" card | Shows events expiring in 7 days | +| DASH-004 | Total Photos count | Check "Total Photos" card | Matches sum of all event photos | +| DASH-005 | Storage Used | Check "Storage Used" card | Shows correct storage value | +| DASH-006 | Total Views | Check "Total Views" card | Shows accumulated views | +| DASH-007 | Downloads count | Check "Downloads" card | Shows accumulated downloads | +| DASH-008 | Archived Events | Check "Archived Events" card | Matches archived count | +| DASH-009 | System Health | Check "System Health" card | Shows "Healthy" or appropriate status | + +### 2.2 Dashboard Widgets +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DASH-010 | Expiring events list | Check "Events Expiring Soon" section | Lists events expiring within 7 days | +| DASH-011 | Recent activity | Check "Recent Activity" section | Shows recent actions with timestamps | +| DASH-012 | Create Event button | Click "Create Event" | Navigate to event creation | + +### 2.3 Translation Check - Dashboard +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DASH-T01 | English translation | Set language to English | All text in English, no translation keys visible | +| DASH-T02 | German translation | Set language to German | All text in German, no translation keys visible | +| DASH-T03 | Translation completeness | Check all labels and buttons | No `t('...')` keys or undefined text | + +--- + +## 3. Events Management Tests + +### 3.1 Events List Page +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-001 | Events table display | Navigate to `/admin/events` | Table shows all events | +| EVT-002 | Stats cards | Check stat cards | Total, Active, Photos, Expiring counts correct | +| EVT-003 | Search functionality | Type in search box | Events filtered by name | +| EVT-004 | Filter - All | Click "All" filter | All events shown | +| EVT-005 | Filter - Active | Click "Active" filter | Only active events shown | +| EVT-006 | Filter - Expiring | Click "Expiring" filter | Only expiring events shown | +| EVT-007 | Filter - Archived | Click "Archived" filter | Only archived events shown | +| EVT-008 | Select all checkbox | Click header checkbox | All events selected | +| EVT-009 | Bulk actions | Select multiple, check actions | Bulk action options available | + +### 3.2 Event Creation +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-010 | Open create form | Click "Create Event" | Modal/page opens | +| EVT-011 | Required fields validation | Submit empty form | Validation errors for required fields | +| EVT-012 | Event name required | Leave name empty | Error shown | +| EVT-013 | Event date required | Leave date empty | Error shown | +| EVT-014 | Event type selection | Select different types | Type saved correctly | +| EVT-015 | Password generation | Click generate password | Random password generated | +| EVT-016 | Custom password | Enter custom password | Password accepted | +| EVT-017 | Expiry date setting | Set expiry date | Date saved correctly | +| EVT-018 | Customer email optional | Leave email empty (if not required) | Event created without email | +| EVT-019 | Customer email required | Leave email empty (when required) | Validation error shown | +| EVT-020 | Successful creation | Fill all required, submit | Event created, redirect to details | + +### 3.3 Event Creation - CSS Template Selection +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-CSS-001 | CSS templates visible | Check Theme & Style section | "Custom CSS Template" section shown (if templates exist) | +| EVT-CSS-002 | No template option | Check "No Template" card | Selectable, selected by default | +| EVT-CSS-003 | Template cards display | Check enabled templates | Templates show name and slot number | +| EVT-CSS-004 | Select template | Click template card | Card highlighted, template selected | +| EVT-CSS-005 | Template persistence | Create event with template | Template saved with event | +| EVT-CSS-006 | Hidden when no templates | Disable all CSS templates | Section not visible | + +### 3.4 Event Creation - Required Fields Settings +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-REQ-001 | Email required - enabled | Enable in settings, create event without email | Validation error for email | +| EVT-REQ-002 | Email required - disabled | Disable in settings, create event without email | Event created successfully | +| EVT-REQ-003 | Customer name required - enabled | Enable in settings, create event without name | Validation error for customer name | +| EVT-REQ-004 | Customer name required - disabled | Disable in settings, create event without name | Event created successfully | +| EVT-REQ-005 | Admin email required - enabled | Enable in settings, create event without admin email | Validation error | +| EVT-REQ-006 | Admin email required - disabled | Disable in settings, create event without admin email | Event created successfully | +| EVT-REQ-007 | Welcome message required - enabled | Enable in settings, create without message | Validation error | +| EVT-REQ-008 | Welcome message required - disabled | Disable in settings, create without message | Event created successfully | + +### 3.4 Event Actions +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-020 | View details | Click actions → View Details | Navigate to event details | +| EVT-021 | View gallery | Click actions → View Gallery | Opens gallery in new tab | +| EVT-022 | Archive event | Click actions → Archive | Confirmation dialog, event archived | +| EVT-023 | Delete event | Click actions → Delete | Confirmation dialog, event deleted | + +### 3.5 Translation Check - Events +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EVT-T01 | Events list - English | Check all labels | No translation keys visible | +| EVT-T02 | Events list - German | Switch to German | All text translated | +| EVT-T03 | Create event modal - both languages | Check form labels | All properly translated | + +--- + +## 4. Event Details Tests + +### 4.1 Overview Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DET-001 | Event header | Check header section | Title, date, type, status displayed | +| DET-002 | Event information | Check info section | All fields displayed correctly | +| DET-003 | Source mode display | Check source mode | Shows "Managed" or "Watch folder" | +| DET-004 | Customer info | Check customer details | Name, email displayed | +| DET-005 | Dates display | Check created/expires | Both dates shown correctly | +| DET-006 | Share link | Check share link section | Link displayed with copy button | +| DET-007 | Copy link | Click copy button | Link copied to clipboard | +| DET-008 | Reset password | Click "Reset Gallery Password" | Confirmation, new password generated | +| DET-009 | Resend email | Click "Resend Creation Email" | Email sent notification | +| DET-010 | Photo statistics | Check stats section | Views, downloads, visitors shown | +| DET-011 | Theme preview | Check theme section | Current theme settings displayed | + +### 4.2 Photos Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DET-020 | Photos grid | Click Photos tab | Photo grid displayed | +| DET-021 | Search photos | Type in search box | Photos filtered by filename | +| DET-022 | Category filter | Select category | Photos filtered by category | +| DET-023 | Sort by date | Select "Sort by Date" | Photos sorted chronologically | +| DET-024 | Sort by name | Select "Sort by Name" | Photos sorted alphabetically | +| DET-025 | Sort by size | Select "Sort by Size" | Photos sorted by file size | +| DET-026 | Sort by rating | Select "Sort by Rating" | Photos sorted by rating | +| DET-027 | Sort direction | Click sort direction button | Order reversed | +| DET-028 | Rating filter | Select rating filter | Photos filtered by minimum rating | +| DET-029 | Has likes filter | Check "Has likes" | Only liked photos shown | +| DET-030 | Has favorites filter | Check "Has favorites" | Only favorited photos shown | +| DET-031 | Has comments filter | Check "Has comments" | Only commented photos shown | +| DET-032 | Upload photos | Click "Upload Photos" | Upload modal opens | +| DET-033 | Photo upload | Select and upload files | Photos uploaded, appear in grid | +| DET-034 | Select photos | Click "Select Photos" | Selection mode enabled | +| DET-035 | Bulk select | Select multiple photos | Selection count updates | +| DET-036 | Export selected | Select photos, click Export | Export options shown | +| DET-037 | Delete photos | Select photos, delete | Confirmation, photos removed | + +### 4.3 Categories Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DET-040 | Categories display | Click Categories tab | Global and event categories shown | +| DET-041 | Add category | Click "Add", enter name | New category created | +| DET-042 | Edit category | Edit category name | Name updated | +| DET-043 | Delete category | Delete event category | Category removed | +| DET-044 | Global categories | Check global list | All global categories displayed | + +### 4.4 Event Edit +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DET-050 | Open edit | Click "Edit" button | Edit modal/page opens | +| DET-051 | Edit name | Change event name | Name updated | +| DET-052 | Edit date | Change event date | Date updated | +| DET-053 | Edit expiry | Change expiry date | Expiry updated | +| DET-054 | Edit customer info | Change customer details | Info updated | +| DET-055 | Edit theme | Change theme settings | Theme updated | +| DET-056 | Save changes | Click save | Changes persisted | +| DET-057 | Cancel edit | Click cancel | Changes discarded | + +### 4.5 Translation Check - Event Details +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| DET-T01 | Overview tab | Check all labels | No translation keys | +| DET-T02 | Photos tab | Check all labels | No translation keys | +| DET-T03 | Categories tab | Check all labels | No translation keys | +| DET-T04 | Both languages | Switch languages | All text properly translated | + +--- + +## 5. Gallery (Public View) Tests + +### 5.1 Gallery Access +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-001 | Password prompt | Navigate to gallery URL | Password form displayed | +| GAL-002 | Enter password | Enter correct password | Gallery content shown | +| GAL-003 | Token access | Use URL with token | Direct access granted | +| GAL-004 | Invalid password | Enter wrong password | Error message shown | +| GAL-005 | Session persistence | Enter password, navigate away, return | Still authenticated | + +### 5.2 Gallery Display +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-010 | Header display | Check gallery header | Event name, branding shown | +| GAL-011 | Photo grid | Check main content | Photos displayed in configured layout | +| GAL-012 | Grid layout | Set grid layout | Photos in uniform grid | +| GAL-013 | Masonry layout | Set masonry layout | Pinterest-style layout | +| GAL-014 | Hero layout | Set hero layout | Featured image with grid below | +| GAL-015 | Carousel layout | Set carousel layout | Slideshow navigation | +| GAL-016 | Timeline layout | Set timeline layout | Photos by date | +| GAL-017 | Mosaic layout | Set mosaic layout | Varied sizes | +| GAL-018 | Footer display | Check footer | Legal links, copyright | + +### 5.3 Gallery Features +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-020 | Search photos | Type in search | Photos filtered | +| GAL-021 | Sort by date | Click sort by date | Photos reordered | +| GAL-022 | Sort by name | Click sort by name | Photos reordered | +| GAL-023 | Sort by size | Click sort by size | Photos reordered | +| GAL-024 | Sort by rating | Click sort by rating | Photos reordered | +| GAL-025 | Category filter | Select category | Photos filtered | + +### 5.4 Photo Interactions +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-030 | Open lightbox | Click photo | Lightbox opens | +| GAL-031 | Navigate lightbox | Use arrows/keys | Navigate between photos | +| GAL-032 | Close lightbox | Click close/escape | Lightbox closes | +| GAL-033 | Download single | Click download in lightbox | Photo downloaded | +| GAL-034 | Like photo | Click like button | Like registered, count updates | +| GAL-035 | Favorite photo | Click favorite button | Favorite registered | +| GAL-036 | Rate photo | Click rating stars | Rating saved | +| GAL-037 | Add comment | Type and submit comment | Comment added | + +### 5.5 Bulk Download +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-040 | Download all | Click "Download All" | ZIP download starts | +| GAL-041 | Select photos | Click "Select Photos" | Selection mode enabled | +| GAL-042 | Download selected | Select photos, download | Selected photos in ZIP | + +### 5.6 Translation Check - Gallery +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| GAL-T01 | Password page | Check all labels | No translation keys | +| GAL-T02 | Gallery view | Check all buttons/labels | No translation keys | +| GAL-T03 | Lightbox | Check all controls | No translation keys | +| GAL-T04 | German locale | Switch to German | All text translated | + +--- + +## 6. Archives Tests + +### 6.1 Archives List +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ARC-001 | Archives page | Navigate to `/admin/archives` | Archives list displayed | +| ARC-002 | Stats cards | Check stat cards | Total, storage, photos, avg size shown | +| ARC-003 | Search archives | Type in search | Archives filtered | +| ARC-004 | Type filter | Select event type | Archives filtered | +| ARC-005 | Sort by date | Select sort by date | Archives sorted | +| ARC-006 | Sort by name | Select sort by name | Archives sorted | +| ARC-007 | Sort by size | Select sort by size | Archives sorted | +| ARC-008 | Empty state | No archives | "No archives found" message | + +### 6.2 Archive Actions +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ARC-010 | Download archive | Click download action | ZIP file downloads | +| ARC-011 | Restore archive | Click restore action | Confirmation, event restored | +| ARC-012 | Delete archive | Click delete action | Confirmation, archive removed | +| ARC-013 | View archive info | Click archive row | Archive details shown | + +### 6.3 Translation Check - Archives +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ARC-T01 | Archives page - English | Check all labels | No translation keys | +| ARC-T02 | Archives page - German | Switch to German | All text translated | + +--- + +## 7. Settings Tests + +### 7.1 General Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-001 | General tab display | Navigate to Settings, General tab | All sections visible | +| SET-002 | Account section | Check account form | Username, password fields | +| SET-003 | Update password | Change password, save | Password updated | +| SET-004 | Site name | Change site name, save | Name updated | +| SET-005 | Timezone | Change timezone, save | Timezone updated | +| SET-006 | Date format | Change format, save | Format updated | +| SET-007 | Language setting | Change default language | Language updated | +| SET-008 | Feature toggles | Toggle features on/off | Settings saved | +| SET-009 | Max uploads setting | Change max files per upload | Setting saved | + +### 7.2 Event Creation Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-010 | Events tab display | Click "Event Creation" tab | Required fields options shown | +| SET-011 | Customer email required | Toggle on/off | Setting saved | +| SET-012 | Customer name required | Toggle on/off | Setting saved | +| SET-013 | Admin email required | Toggle on/off | Setting saved | +| SET-014 | Welcome message required | Toggle on/off | Setting saved | +| SET-015 | Settings persistence | Change settings, refresh | Settings persisted | + +### 7.3 System Status Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-020 | Status tab display | Click "System Status" tab | All status sections shown | +| SET-021 | Storage overview | Check storage section | Used, available, limit shown | +| SET-022 | Soft limit setting | Set soft limit, save | Limit saved | +| SET-023 | Capacity override | Set override values | Values saved | +| SET-024 | System info | Check system section | Version, uptime, memory shown | +| SET-025 | Database info | Check database section | Type, size, connections shown | +| SET-026 | Background services | Check services list | Service status indicators | + +### 7.4 Security Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-030 | Security tab display | Click "Security" tab | All security options shown | +| SET-031 | Password requirements | Change min length | Setting saved | +| SET-032 | Session timeout | Change timeout value | Setting saved | +| SET-033 | Max login attempts | Change attempts value | Setting saved | +| SET-034 | reCAPTCHA settings | Configure reCAPTCHA | Settings saved | +| SET-035 | reCAPTCHA site key | Enter site key | Key saved | +| SET-036 | reCAPTCHA secret | Enter secret key | Key saved | + +### 7.5 Categories Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-040 | Categories tab display | Click "Categories" tab | Category manager shown | +| SET-041 | List categories | Check categories list | All global categories shown | +| SET-042 | Add category | Add new category | Category created | +| SET-043 | Edit category | Edit category name | Name updated | +| SET-044 | Delete category | Delete category | Category removed | +| SET-045 | Reorder categories | Drag to reorder | Order saved | + +### 7.6 Analytics Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-050 | Analytics tab display | Click "Analytics" tab | Analytics settings shown | +| SET-051 | Umami website ID | Enter website ID | ID saved | +| SET-052 | Umami host URL | Enter host URL | URL saved | +| SET-053 | Backend analytics info | Check backend section | Connection status shown | + +### 7.7 Moderation Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-060 | Moderation tab display | Click "Moderation" tab | Word filter manager shown | +| SET-061 | Add filter word | Add blocked word | Word added to list | +| SET-062 | Remove filter word | Remove word | Word removed | +| SET-063 | Filter action | Change filter action | Action saved | + +### 7.8 Custom CSS Tab +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-070 | Styling tab display | Click "Custom CSS" tab | CSS editor shown | +| SET-071 | Template selection | Select template 1/2/3 | Template loaded | +| SET-072 | Template name | Change template name | Name saved | +| SET-073 | Enable template | Toggle enable checkbox | State saved | +| SET-074 | Edit CSS | Modify CSS content | CSS saved | +| SET-075 | Reset to default | Click reset button | Default CSS restored | +| SET-076 | Character count | Check character counter | Shows correct count | + +### 7.9 Liquid Glass CSS Templates (FEATURE-006) +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-080 | Liquid Glass Light | Check template slot 2 | "Liquid Glass Light" template present | +| SET-081 | Liquid Glass Dark | Check template slot 3 | "Liquid Glass Dark" template present | +| SET-082 | Glass variables | Check Light template CSS | Contains --glass-bg, --glass-blur variables | +| SET-083 | Neon glow variables | Check Dark template CSS | Contains --neon-glow variable | +| SET-084 | Backdrop filter | Check templates | Contains backdrop-filter: blur() | +| SET-085 | Reduced motion | Check templates | Contains @media (prefers-reduced-motion) | +| SET-086 | Reduced transparency | Check templates | Contains @media (prefers-reduced-transparency) | +| SET-087 | Responsive styles | Check templates | Contains @media (max-width: 768px) | +| SET-088 | Template enabled | Toggle enable for Liquid Glass | Template becomes available in event creation | + +### 7.10 Translation Check - Settings +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| SET-T01 | All tabs - English | Check every tab | No translation keys visible | +| SET-T02 | All tabs - German | Switch to German, check tabs | All text translated | +| SET-T03 | Form labels | Check all form labels | Properly translated | +| SET-T04 | Button text | Check all buttons | Properly translated | +| SET-T05 | Error messages | Trigger validation errors | Errors translated | + +--- + +## 8. Analytics Page Tests + +### 8.1 Analytics Display +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ANA-001 | Analytics page | Navigate to `/admin/analytics` | Dashboard displayed | +| ANA-002 | Page views card | Check page views | Count and chart shown | +| ANA-003 | Unique visitors | Check visitors card | Count and chart shown | +| ANA-004 | Downloads card | Check downloads | Count shown | +| ANA-005 | Time range filter | Select 7/30/90 days | Data updates | +| ANA-006 | Top pages list | Check top pages section | Pages with view counts | +| ANA-007 | Device breakdown | Check device section | Desktop/mobile/tablet % | +| ANA-008 | Storage usage | Check storage section | Used/available/photos | +| ANA-009 | Refresh data | Click refresh button | Data reloads | + +### 8.2 Translation Check - Analytics +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ANA-T01 | Analytics - English | Check all labels | No translation keys | +| ANA-T02 | Analytics - German | Switch to German | All text translated | + +--- + +## 9. Email Settings Tests + +### 9.1 SMTP Configuration +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EML-001 | Email page | Navigate to `/admin/email` | SMTP settings shown | +| EML-002 | SMTP host | Enter host value | Field accepts input | +| EML-003 | SMTP port | Enter port number | Field accepts number | +| EML-004 | Security type | Select TLS/SSL | Selection saved | +| EML-005 | Username | Enter username | Field accepts input | +| EML-006 | Password | Enter password | Field masked | +| EML-007 | Show password | Click show button | Password visible | +| EML-008 | From email | Enter from address | Field accepts email | +| EML-009 | From name | Enter from name | Field accepts input | +| EML-010 | Save settings | Click save | Settings persisted | +| EML-011 | Ignore SSL toggle | Toggle certificate check | Setting saved | + +### 9.2 Test Email +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EML-020 | Test email input | Enter test email address | Field accepts email | +| EML-021 | Send test email | Click send button | Email sent, success message | +| EML-022 | Test email failure | Wrong SMTP config | Error message shown | + +### 9.3 Email Templates +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EML-030 | Templates tab | Click "Email Templates" | Template list shown | +| EML-031 | Template list | Check all templates | 10 templates listed | +| EML-032 | Select template | Click template | Editor loads template | +| EML-033 | Edit subject | Modify subject line | Subject updated | +| EML-034 | Edit body | Modify HTML body | Body updated | +| EML-035 | Language switch | Click English/German | Language version loaded | +| EML-036 | Preview template | Click preview | Preview rendered | +| EML-037 | Save template | Click save | Template saved | +| EML-038 | Variables display | Check variables section | Available variables listed | + +### 9.4 Translation Check - Email +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| EML-T01 | SMTP tab - English | Check all labels | No translation keys | +| EML-T02 | Templates tab - English | Check all labels | No translation keys | +| EML-T03 | Both tabs - German | Switch to German | All text translated | + +--- + +## 10. Branding Tests + +### 10.1 Company Information +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-001 | Branding page | Navigate to `/admin/branding` | All sections shown | +| BRD-002 | Company name | Enter company name | Field accepts input | +| BRD-003 | Company tagline | Enter tagline | Field accepts input | +| BRD-004 | Support email | Enter email | Field accepts email | +| BRD-005 | Footer text | Enter footer text | Field accepts input | +| BRD-006 | Upload favicon | Upload favicon file | Favicon saved | +| BRD-007 | Upload logo | Upload logo file | Logo displayed | +| BRD-008 | Remove logo | Click remove button | Logo removed | +| BRD-009 | Logo size | Select size option | Size applied | +| BRD-010 | Logo position | Select left/center/right | Position saved | +| BRD-011 | Display mode | Select logo/name/both | Mode applied | +| BRD-012 | Header logo toggle | Toggle show in header | Setting saved | +| BRD-013 | Hero logo toggle | Toggle show in hero | Setting saved | +| BRD-014 | White label | Toggle hide PicPeak | Branding hidden | +| BRD-015 | Watermarks | Toggle watermarks | Setting saved | + +### 10.2 Theme Presets +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-020 | Classic Grid preset | Select Classic Grid | Theme applied | +| BRD-021 | Elegant Wedding preset | Select Elegant Wedding | Theme applied | +| BRD-022 | Modern Masonry preset | Select Modern Masonry | Theme applied | +| BRD-023 | Birthday preset | Select Birthday | Theme applied | +| BRD-024 | Corporate preset | Select Corporate | Theme applied | +| BRD-025 | Artistic preset | Select Artistic | Theme applied | + +### 10.3 Gallery Layout +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-030 | Grid layout | Select grid | Layout applied | +| BRD-031 | Masonry layout | Select masonry | Layout applied | +| BRD-032 | Carousel layout | Select carousel | Layout applied | +| BRD-033 | Timeline layout | Select timeline | Layout applied | +| BRD-034 | Hero layout | Select hero | Layout applied | +| BRD-035 | Mosaic layout | Select mosaic | Layout applied | +| BRD-036 | Photo spacing | Change spacing | Setting applied | +| BRD-037 | Photo animation | Change animation | Setting applied | + +### 10.4 Colors and Typography +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-040 | Primary color | Change primary color | Color picker works, color applied | +| BRD-041 | Accent color | Change accent color | Color applied | +| BRD-042 | Background color | Change background | Color applied | +| BRD-043 | Text color | Change text color | Color applied | +| BRD-044 | Body font | Select body font | Font applied | +| BRD-045 | Heading font | Select heading font | Font applied | +| BRD-046 | Font size | Select font size | Size applied | +| BRD-047 | Border radius | Select radius | Radius applied | +| BRD-048 | Shadow style | Select shadow | Shadow applied | +| BRD-049 | Background pattern | Select pattern | Pattern applied | + +### 10.5 Live Preview +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-050 | Preview display | Check live preview section | Preview shown | +| BRD-051 | Preview updates | Change settings | Preview updates | +| BRD-052 | Live preview toggle | Toggle live preview | Immediate updates | +| BRD-053 | Save changes | Click save | All settings persisted | +| BRD-054 | Preview button | Click preview button | Full preview opens | + +### 10.6 Custom CSS Instructions Panel +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-060 | Instructions toggle | Click "How to use Custom CSS" | Panel expands/collapses | +| BRD-061 | CSS variables section | Check variables section | Theme variables displayed with current values | +| BRD-062 | Gallery layouts section | Check layouts section | CSS selectors documented | +| BRD-063 | Glassmorphism example | Check glass effect section | Example CSS shown | +| BRD-064 | Tip section | Check tip box | Links to CSS Templates | +| BRD-065 | Panel closed by default | Load page | Instructions panel collapsed | + +### 10.7 Typography Row Layout +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-070 | Font size & border radius | Check first row | 2 items per row, fully visible | +| BRD-071 | Shadow & background | Check second row | 2 items per row, fully visible | +| BRD-072 | Dropdown values visible | Check all dropdowns | Full text visible (not truncated) | +| BRD-073 | Mobile responsive | Check on mobile viewport | Stacks to 1 column properly | + +### 10.8 Translation Check - Branding +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BRD-T01 | Branding - English | Check all labels | No translation keys | +| BRD-T02 | Branding - German | Switch to German | All text translated | + +--- + +## 11. Backup & Restore Tests + +### 11.1 Backup Dashboard +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BAK-001 | Backup page | Navigate to `/admin/backup` | Dashboard shown | +| BAK-002 | Backup health | Check health indicator | Status displayed | +| BAK-003 | Stats cards | Check total/size/duration | Values displayed | +| BAK-004 | Backup coverage | Check coverage section | DB/Photos/Archives status | +| BAK-005 | Storage destination | Check destination info | Path and retention shown | +| BAK-006 | Run backup now | Click run backup | Backup process starts | + +### 11.2 Backup Configuration +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BAK-010 | Configuration tab | Click Configuration | Settings form shown | +| BAK-011 | Enable backups | Toggle enable | Setting saved | +| BAK-012 | Backup schedule | Set schedule | Schedule saved | +| BAK-013 | Include database | Toggle database | Setting saved | +| BAK-014 | Include photos | Toggle photos | Setting saved | +| BAK-015 | Retention period | Set retention days | Setting saved | +| BAK-016 | Storage path | Set backup path | Path saved | +| BAK-017 | S3 configuration | Configure S3 | Settings saved | + +### 11.3 Backup History +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BAK-020 | History tab | Click Backup History | History list shown | +| BAK-021 | Backup list | Check backup entries | Date, size, status shown | +| BAK-022 | Download backup | Click download | Backup file downloads | +| BAK-023 | Delete backup | Click delete | Confirmation, backup removed | + +### 11.4 Restore +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BAK-030 | Restore tab | Click Restore | Restore options shown | +| BAK-031 | Select backup | Choose backup to restore | Backup selected | +| BAK-032 | Upload backup | Upload backup file | File accepted | +| BAK-033 | Restore backup | Click restore | Confirmation, restore starts | + +### 11.5 Translation Check - Backup +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| BAK-T01 | All tabs - English | Check all tabs | No translation keys | +| BAK-T02 | All tabs - German | Switch to German | All text translated | + +--- + +## 12. CMS Pages Tests + +### 12.1 Public Landing Page +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| CMS-001 | CMS page | Navigate to `/admin/cms` | Page displayed | +| CMS-002 | Landing toggle | Toggle public landing | Setting saved | +| CMS-003 | Landing HTML | Edit HTML content | Content saved | +| CMS-004 | Landing CSS | Edit CSS content | CSS saved | +| CMS-005 | Save public site | Click save | Content published | +| CMS-006 | Reset to default | Click reset | Default content restored | +| CMS-007 | Live preview | Check preview (when enabled) | Preview shows content | + +### 12.2 Legal Pages +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| CMS-010 | Privacy Policy | Select Privacy Policy | Editor loads content | +| CMS-011 | Legal Notice | Select Legal Notice | Editor loads content | +| CMS-012 | Edit title | Change page title | Title updated | +| CMS-013 | Edit content | Use markdown editor | Content updated | +| CMS-014 | English version | Click English tab | English content loaded | +| CMS-015 | German version | Click German tab | German content loaded | +| CMS-016 | Save page | Click save | Content saved | +| CMS-017 | Preview links | Click preview link | Page opens in new tab | + +### 12.3 Rich Text Editor +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| CMS-020 | Heading buttons | Click H1-H6 buttons | Headings applied | +| CMS-021 | Bold text | Click bold | Text bolded | +| CMS-022 | Italic text | Click italic | Text italicized | +| CMS-023 | Code formatting | Click code | Code formatted | +| CMS-024 | Lists | Click bullet/number list | Lists created | +| CMS-025 | Blockquote | Click quote | Quote formatted | +| CMS-026 | Link | Click link button | Link added | +| CMS-027 | Alignment | Click align buttons | Text aligned | +| CMS-028 | Undo/Redo | Click undo/redo | Changes undone/redone | +| CMS-029 | Fullscreen | Click fullscreen | Editor expands | + +### 12.4 Translation Check - CMS +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| CMS-T01 | CMS page - English | Check all labels | No translation keys | +| CMS-T02 | CMS page - German | Switch to German | All text translated | + +--- + +## 13. Cross-Cutting Tests + +### 13.1 Translation Button (All Pages) +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| TRANS-001 | Language button visibility | Check header on all pages | Language button always visible | +| TRANS-002 | Language dropdown | Click language button | Dropdown shows EN/DE options | +| TRANS-003 | Switch to German | Select German | UI switches to German | +| TRANS-004 | Switch to English | Select English | UI switches to English | +| TRANS-005 | Language persistence | Change language, refresh | Language persists | + +### 13.2 Translation Completeness Check +**Execute on every page:** + +| Page | Test Steps | Check Points | +|------|------------|--------------| +| Login | Load page in EN and DE | All labels, buttons, errors translated | +| Dashboard | Load page in EN and DE | All cards, labels, activity items | +| Events List | Load page in EN and DE | Headers, filters, table columns, actions | +| Event Details | All tabs in EN and DE | All sections, buttons, labels | +| Event Create/Edit | Form in EN and DE | All field labels, placeholders, errors | +| Gallery (Public) | Load in EN and DE | All buttons, filters, messages | +| Archives | Load in EN and DE | Table, filters, messages | +| Settings (all tabs) | Each tab in EN and DE | All form labels, descriptions, buttons | +| Analytics | Load in EN and DE | All cards, sections, labels | +| Email Settings | Both tabs in EN and DE | All form fields, template names | +| Branding | Load in EN and DE | All sections, options, labels | +| Backup | All tabs in EN and DE | All cards, buttons, status messages | +| CMS | Load in EN and DE | All editor labels, page names | + +### 13.3 Responsive Design +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| RESP-001 | Desktop view | Set viewport 1920x1080 | Full layout displayed | +| RESP-002 | Tablet view | Set viewport 768x1024 | Responsive layout | +| RESP-003 | Mobile view | Set viewport 375x667 | Mobile layout, hamburger menu | +| RESP-004 | Sidebar collapse | Resize window | Sidebar collapses appropriately | +| RESP-005 | Table responsiveness | Check tables on mobile | Tables scroll or stack | + +### 13.4 Error Handling +| Test ID | Test Case | Steps | Expected Result | +|---------|-----------|-------|-----------------| +| ERR-001 | Network error | Disable network, perform action | Error message shown | +| ERR-002 | 404 page | Navigate to invalid route | 404 page displayed | +| ERR-003 | API error | Trigger API error | User-friendly error message | +| ERR-004 | Form validation | Submit invalid forms | Field-level errors shown | +| ERR-005 | Session expired | Let session expire | Redirect to login | + +--- + +## 14. Console Error Monitoring + +### 14.1 Error Check Procedure +Execute after each major test section: + +```javascript +// Using Playwright MCP +browser_console_messages({ level: "error" }) + +// Using Chrome DevTools MCP +list_console_messages({ types: ["error"] }) +``` + +### 14.2 Acceptable vs Critical Errors + +**Acceptable (informational):** +- React DevTools suggestions +- React Router future flag warnings +- Third-party library deprecation warnings + +**Critical (must fix):** +- Uncaught exceptions +- Network request failures (4xx, 5xx) +- React rendering errors +- TypeScript/JavaScript runtime errors +- Failed to fetch errors +- CORS errors + +### 14.3 Error Documentation Template +``` +| Page | Error Message | Severity | Status | +|------|---------------|----------|--------| +| [page] | [error text] | Critical/Warning | Open/Fixed | +``` + +--- + +## 15. Network Request Monitoring + +### 15.1 API Health Check +```javascript +// Using Chrome DevTools MCP +list_network_requests({ resourceTypes: ["fetch", "xhr"] }) +``` + +### 15.2 Expected API Endpoints + +| Endpoint Pattern | Method | Expected Status | +|------------------|--------|-----------------| +| `/api/admin/auth/*` | POST | 200/401 | +| `/api/admin/events` | GET | 200 | +| `/api/admin/events/*` | GET/PUT/DELETE | 200/404 | +| `/api/admin/settings` | GET/PUT | 200 | +| `/api/admin/photos/*` | GET/POST/DELETE | 200 | +| `/api/gallery/*` | GET | 200/401 | +| `/api/admin/backup/*` | GET/POST | 200 | + +### 15.3 Failed Request Documentation +``` +| Endpoint | Method | Status | Error | Impact | +|----------|--------|--------|-------|--------| +| [url] | [method] | [status] | [error] | [impact] | +``` + +--- + +## 16. Backend Service Log Monitoring + +### 16.1 Log Check Command +```bash +# Check backend logs for errors +docker logs picpeak-backend 2>&1 | grep -i error + +# Or if running locally +tail -f backend/logs/error.log +``` + +### 16.2 Log Patterns to Watch + +| Pattern | Severity | Action | +|---------|----------|--------| +| `ERROR` | High | Investigate immediately | +| `WARN` | Medium | Document and monitor | +| `UnhandledPromiseRejection` | Critical | Fix immediately | +| `ECONNREFUSED` | High | Check service connectivity | +| `JWT` errors | High | Check authentication | +| `Database` errors | Critical | Check DB connection | + +### 16.3 Service Health Checks +```bash +# Check all services +curl http://localhost:3001/api/health + +# Expected response +{ "status": "healthy", "services": { "database": "up", "redis": "up" } } +``` + +--- + +## 17. State-Based Testing + +### 17.1 Event States +| State | Conditions | Expected Behavior | +|-------|------------|-------------------| +| Active | Created, not expired | Full access, all features | +| Expiring | Within 7 days of expiry | Warning indicators shown | +| Expired | Past expiry date | Limited access, archive prompt | +| Archived | Manually archived | ZIP available, no gallery access | + +### 17.2 Settings State Combinations + +#### Required Fields Matrix +| Customer Email | Customer Name | Admin Email | Welcome Msg | Test Scenario | +|----------------|---------------|-------------|-------------|---------------| +| Required | Required | Required | Required | All fields mandatory | +| Required | Optional | Optional | Optional | Only email required | +| Optional | Required | Optional | Optional | Only name required | +| Optional | Optional | Optional | Optional | All fields optional | + +**Test each combination:** +1. Configure settings +2. Attempt to create event with missing field +3. Verify validation works correctly + +### 17.3 Theme State Testing +| Theme Setting | Layout | Test Points | +|---------------|--------|-------------| +| Classic Grid | grid | Uniform photo sizes | +| Elegant Wedding | hero | Hero image prominent | +| Modern Masonry | masonry | Varied photo heights | +| Birthday | carousel | Slideshow works | +| Corporate | timeline | Date grouping | +| Artistic | mosaic | Mixed sizes | + +--- + +## 18. Test Execution Checklist + +### Pre-Test Setup +- [ ] Start frontend dev server +- [ ] Start backend server +- [ ] Verify database connection +- [ ] Clear browser cache/cookies +- [ ] Set up MCP browser connection + +### Test Execution Order +1. [ ] Authentication Tests (Section 1) +2. [ ] Dashboard Tests (Section 2) +3. [ ] Events Management Tests (Section 3) +4. [ ] Event Details Tests (Section 4) +5. [ ] Gallery Tests (Section 5) +6. [ ] Archives Tests (Section 6) +7. [ ] Settings Tests (Section 7) +8. [ ] Analytics Tests (Section 8) +9. [ ] Email Settings Tests (Section 9) +10. [ ] Branding Tests (Section 10) +11. [ ] Backup Tests (Section 11) +12. [ ] CMS Tests (Section 12) +13. [ ] Cross-Cutting Tests (Section 13) +14. [ ] Console Error Review (Section 14) +15. [ ] Network Request Review (Section 15) +16. [ ] Backend Log Review (Section 16) +17. [ ] State-Based Tests (Section 17) + +### Post-Test Actions +- [ ] Document all failures +- [ ] Capture screenshots of issues +- [ ] Log console errors +- [ ] Note network failures +- [ ] Update test status + +--- + +## 19. Test Result Summary Template + +```markdown +## Test Results - [Date] + +### Environment +- Frontend Version: x.x.x +- Backend Version: x.x.x +- Browser: Chrome/Firefox/Safari +- Tester: [Name] + +### Summary +| Category | Total | Passed | Failed | Blocked | +|----------|-------|--------|--------|---------| +| Auth | X | X | X | X | +| Dashboard | X | X | X | X | +| ... | ... | ... | ... | ... | + +### Critical Issues +1. [Issue description] +2. [Issue description] + +### Translation Gaps +| Page | Missing Translation | Language | +|------|---------------------|----------| +| ... | ... | ... | + +### Console Errors +| Page | Error | Severity | +|------|-------|----------| +| ... | ... | ... | + +### Recommendations +1. [Recommendation] +2. [Recommendation] +``` + +--- + +## Appendix A: MCP Tool Reference + +### Navigation +```javascript +mcp__playwright__browser_navigate({ url: "http://localhost:5173/admin/dashboard" }) +``` + +### Take Snapshot +```javascript +mcp__playwright__browser_snapshot() +``` + +### Click Element +```javascript +mcp__playwright__browser_click({ element: "Description", ref: "e123" }) +``` + +### Fill Form +```javascript +mcp__playwright__browser_type({ element: "Input field", ref: "e456", text: "value" }) +``` + +### Check Console +```javascript +mcp__playwright__browser_console_messages({ level: "error" }) +``` + +### Check Network +```javascript +mcp__chrome-devtools__list_network_requests({ resourceTypes: ["fetch", "xhr"] }) +``` + +--- + +## Appendix B: Common Test Data + +### Admin Credentials +- Username: `admin` +- Password: `admin` (or configured password) + +### Test Event Data +```json +{ + "name": "Test Event", + "date": "2026-01-15", + "type": "wedding", + "password": "test123", + "customerName": "John Doe", + "customerEmail": "john@example.com", + "expiryDate": "2026-02-15" +} +``` + +### Test Gallery Password +- Default: `test123` or as configured per event + +--- + +*Last Updated: January 2026* +*Version: 1.0* diff --git a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx index 3ea4b9a9..ba14e9f0 100644 --- a/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx +++ b/frontend/src/components/admin/ThemeCustomizerEnhanced.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid } from 'lucide-react'; +import { Palette, RotateCcw, Check, Layout, Type, Sparkles, Grid3X3, Layers, Play, Clock, Image, LayoutGrid, ChevronDown, Code, Info } from 'lucide-react'; import { Button, Card, Input } from '../common'; import { ThemeConfig, GALLERY_THEME_PRESETS, GalleryLayoutType } from '../../types/theme.types'; // import { settingsService } from '../../services/settings.service'; @@ -44,6 +44,7 @@ export const ThemeCustomizerEnhanced: React.FC = ( const [localTheme, setLocalTheme] = useState(value); const [selectedPreset, setSelectedPreset] = useState(presetName); const [customCss, setCustomCss] = useState(value.customCss || ''); + const [showCssInstructions, setShowCssInstructions] = useState(false); // const logoInputRef = useRef(null); useEffect(() => { @@ -494,7 +495,8 @@ export const ThemeCustomizerEnhanced: React.FC = ( -
+ {/* Row 1: Font Size & Border Radius */} +
+
+ {/* Row 2: Shadow Style & Background Pattern */} +