1773ed5f95
Mirror to GitHub / mirror (push) Successful in 26s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m28s
Version and Release / version-bump (push) Successful in 32s
Version and Release / trigger-drone (push) Has been skipped
Original: feat: enhance security logging and ensure rate limit blocks are properly tracked - Add comprehensive logging for rate limit blocks with full request details - IP address (with proper proxy detection), user agent, headers, timestamps - Rate limit info (current count, limit, remaining, reset time) - Separate tracking for auth vs general endpoints - Enhance authentication failure logging - JWT validation failures with detailed error info - Admin auth attempts without token - Failed token validation with user context - All events include IP, path, method, user agent - Improve Winston logger configuration for production - Add automatic log rotation (10MB errors, 50MB combined) - Create separate security.log for auth/rate limit events - Ensure logs directory exists automatically - Add structured JSON format for log aggregation - Support container logging with LOG_TO_CONSOLE env var - Create comprehensive documentation - Security logging guide with examples - Monitoring recommendations - Configuration reference - Add test script to verify logging functionality All rate limit settings remain configurable via admin panel: - Window duration, max requests, auth limits - Skip authenticated requests option - Public endpoints only option 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
184 lines
6.4 KiB
TypeScript
184 lines
6.4 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { Camera } from 'lucide-react';
|
|
import { ThemeConfig, GalleryLayoutType } from '../../types/theme.types';
|
|
|
|
interface GalleryPreviewProps {
|
|
theme: ThemeConfig;
|
|
layoutType?: GalleryLayoutType;
|
|
className?: string;
|
|
}
|
|
|
|
// Mock photo data for preview
|
|
const generateMockPhotos = (count: number) => {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: i + 1,
|
|
filename: `photo-${i + 1}.jpg`,
|
|
url: '',
|
|
thumbnail_url: '',
|
|
type: i % 3 === 0 ? 'collage' : 'individual',
|
|
category_id: (i % 4) + 1,
|
|
category_name: ['Ceremony', 'Reception', 'Portraits', 'Party'][i % 4],
|
|
category_slug: ['ceremony', 'reception', 'portraits', 'party'][i % 4],
|
|
size: Math.floor(Math.random() * 5000000) + 1000000,
|
|
uploaded_at: new Date().toISOString(),
|
|
}));
|
|
};
|
|
|
|
// Preview photo component
|
|
const PreviewPhoto: React.FC<{
|
|
photo: any;
|
|
className?: string;
|
|
aspectRatio?: string;
|
|
}> = ({
|
|
photo,
|
|
className = '',
|
|
aspectRatio = 'aspect-square'
|
|
}) => (
|
|
<div className={`relative overflow-hidden rounded-lg bg-gradient-to-br from-neutral-200 to-neutral-300 ${aspectRatio} ${className}`}>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<Camera className="w-8 h-8 text-neutral-400" />
|
|
</div>
|
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-2">
|
|
<p className="text-white text-xs truncate">{photo.filename}</p>
|
|
{photo.category_name && (
|
|
<p className="text-white/70 text-[10px]">{photo.category_name}</p>
|
|
)}
|
|
</div>
|
|
{photo.type === 'collage' && (
|
|
<div className="absolute top-1 right-1">
|
|
<span className="px-1.5 py-0.5 bg-black/60 text-white text-[10px] rounded">
|
|
Collage
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
export const GalleryPreview: React.FC<GalleryPreviewProps> = ({
|
|
theme,
|
|
layoutType,
|
|
className = ''
|
|
}) => {
|
|
const mockPhotos = useMemo(() => generateMockPhotos(12), []);
|
|
|
|
// Use the provided layoutType or fallback to theme's gallery layout
|
|
const activeLayout = layoutType || theme.galleryLayout || 'grid';
|
|
|
|
const renderLayout = () => {
|
|
const spacing = theme.gallerySettings?.spacing || 'normal';
|
|
const gapClass = spacing === 'tight' ? 'gap-1' : spacing === 'relaxed' ? 'gap-4' : 'gap-2';
|
|
|
|
switch (activeLayout) {
|
|
case 'grid': {
|
|
return (
|
|
<div className={`grid grid-cols-3 md:grid-cols-4 ${gapClass}`}>
|
|
{mockPhotos.slice(0, 8).map((photo) => (
|
|
<PreviewPhoto key={photo.id} photo={photo} />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
case 'masonry':
|
|
return (
|
|
<div className={`columns-3 md:columns-4 ${gapClass}`}>
|
|
{mockPhotos.slice(0, 10).map((photo, idx) => (
|
|
<div key={photo.id} className={`break-inside-avoid mb-${spacing === 'tight' ? '1' : spacing === 'relaxed' ? '4' : '2'}`}>
|
|
<PreviewPhoto
|
|
photo={photo}
|
|
aspectRatio={idx % 3 === 0 ? 'aspect-[4/5]' : idx % 3 === 1 ? 'aspect-[4/3]' : 'aspect-square'}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
case 'carousel':
|
|
return (
|
|
<div className="relative">
|
|
<div className="flex items-center gap-2 overflow-hidden">
|
|
<PreviewPhoto photo={mockPhotos[0]} className="w-full max-w-md mx-auto" aspectRatio="aspect-[4/3]" />
|
|
</div>
|
|
<div className="flex justify-center gap-1 mt-3">
|
|
{[0, 1, 2, 3].map((idx) => (
|
|
<div key={idx} className={`w-2 h-2 rounded-full ${idx === 0 ? 'bg-primary-600' : 'bg-neutral-300'}`} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'timeline':
|
|
return (
|
|
<div className="space-y-6">
|
|
{['Today', 'Yesterday'].map((date, dateIdx) => (
|
|
<div key={date}>
|
|
<h4 className="text-sm font-medium text-neutral-700 mb-2">{date}</h4>
|
|
<div className={`grid grid-cols-3 ${gapClass}`}>
|
|
{mockPhotos.slice(dateIdx * 3, (dateIdx * 3) + 3).map((photo) => (
|
|
<PreviewPhoto key={photo.id} photo={photo} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
case 'hero':
|
|
return (
|
|
<div className="space-y-3">
|
|
<PreviewPhoto photo={mockPhotos[0]} aspectRatio="aspect-[16/9]" className="w-full" />
|
|
<div className={`grid grid-cols-4 ${gapClass}`}>
|
|
{mockPhotos.slice(1, 5).map((photo) => (
|
|
<PreviewPhoto key={photo.id} photo={photo} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 'mosaic':
|
|
return (
|
|
<div className={`grid grid-cols-4 grid-rows-3 ${gapClass} h-64`}>
|
|
<PreviewPhoto photo={mockPhotos[0]} className="col-span-2 row-span-2" aspectRatio="aspect-auto h-full" />
|
|
<PreviewPhoto photo={mockPhotos[1]} className="col-span-1 row-span-1" aspectRatio="aspect-auto h-full" />
|
|
<PreviewPhoto photo={mockPhotos[2]} className="col-span-1 row-span-1" aspectRatio="aspect-auto h-full" />
|
|
<PreviewPhoto photo={mockPhotos[3]} className="col-span-1 row-span-1" aspectRatio="aspect-auto h-full" />
|
|
<PreviewPhoto photo={mockPhotos[4]} className="col-span-1 row-span-1" aspectRatio="aspect-auto h-full" />
|
|
<PreviewPhoto photo={mockPhotos[5]} className="col-span-2 row-span-1" aspectRatio="aspect-auto h-full" />
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`bg-white rounded-lg shadow-sm overflow-hidden ${className}`}
|
|
style={{
|
|
backgroundColor: theme.backgroundColor || '#ffffff',
|
|
color: theme.textColor || '#171717',
|
|
fontFamily: theme.fontFamily || 'Inter, sans-serif',
|
|
}}
|
|
>
|
|
{/* Preview Header */}
|
|
<div
|
|
className="px-4 py-3 border-b"
|
|
style={{
|
|
borderColor: theme.primaryColor ? `${theme.primaryColor}20` : '#e5e7eb',
|
|
}}
|
|
>
|
|
<h3 className="text-sm font-medium">
|
|
Gallery Preview - <span className="capitalize">{activeLayout}</span> Layout
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Preview Content */}
|
|
<div className="p-4" style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
|
{renderLayout()}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
GalleryPreview.displayName = 'GalleryPreview'; |