fix: resolve multiple UI issues in admin panel
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m13s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 3s
- Fixed dropdown menu visibility in events table by using fixed positioning - Fixed double /api prefix in settings upload endpoints (favicon, watermark) - Fixed thumbnail display in hero image selection by properly handling API paths - Removed unnecessary console.log statements - Added proper cleanup for dropdown on scroll/resize events 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
# Storage Management Scripts
|
||||
|
||||
## check-storage.js
|
||||
|
||||
Checks the storage directory structure and verifies that photo files exist.
|
||||
|
||||
### Usage:
|
||||
```bash
|
||||
# Check overall storage structure
|
||||
node scripts/check-storage.js
|
||||
|
||||
# Check specific event
|
||||
node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||
```
|
||||
|
||||
### What it checks:
|
||||
- Storage directory structure and permissions
|
||||
- Event directories
|
||||
- Photo files existence
|
||||
- Thumbnail files existence
|
||||
- Database vs filesystem consistency
|
||||
|
||||
## regenerate-thumbnails.js
|
||||
|
||||
Regenerates missing thumbnails for photos in the database.
|
||||
|
||||
### Usage:
|
||||
```bash
|
||||
# Regenerate all missing thumbnails
|
||||
node scripts/regenerate-thumbnails.js
|
||||
|
||||
# Regenerate thumbnails for specific event (by ID)
|
||||
node scripts/regenerate-thumbnails.js 2
|
||||
```
|
||||
|
||||
### What it does:
|
||||
- Scans photos in database
|
||||
- Checks if thumbnails exist
|
||||
- Generates missing thumbnails using Sharp
|
||||
- Updates database with thumbnail paths
|
||||
- Reports success/error statistics
|
||||
|
||||
### Prerequisites:
|
||||
- Node.js environment
|
||||
- Database access
|
||||
- Write permissions to storage directory
|
||||
- Sharp library installed
|
||||
|
||||
## Production Usage
|
||||
|
||||
On your production server:
|
||||
|
||||
1. First, check the storage structure:
|
||||
```bash
|
||||
cd /path/to/picpeak/backend
|
||||
NODE_ENV=production node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||
```
|
||||
|
||||
2. If thumbnails are missing, regenerate them:
|
||||
```bash
|
||||
NODE_ENV=production node scripts/regenerate-thumbnails.js 2
|
||||
```
|
||||
|
||||
Note: Replace `2` with the actual event ID from your database.
|
||||
|
||||
## cleanup-thumbnails.js
|
||||
|
||||
Cleans up temporary and orphaned thumbnail files.
|
||||
|
||||
### Usage:
|
||||
```bash
|
||||
# Dry run - see what would be deleted
|
||||
node scripts/cleanup-thumbnails.js --dry-run
|
||||
|
||||
# Actually delete orphaned thumbnails
|
||||
node scripts/cleanup-thumbnails.js
|
||||
```
|
||||
|
||||
### What it does:
|
||||
- Identifies temporary thumbnails (thumb_temp_*)
|
||||
- Finds orphaned thumbnails not linked to any photo
|
||||
- Removes unnecessary files to free up space
|
||||
- Reports statistics on cleanup
|
||||
|
||||
## diagnose-thumbnails.js
|
||||
|
||||
Diagnoses why thumbnails might not be showing for a specific event.
|
||||
|
||||
### Usage:
|
||||
```bash
|
||||
node scripts/diagnose-thumbnails.js 2
|
||||
```
|
||||
|
||||
### What it checks:
|
||||
- Thumbnail paths in database vs filesystem
|
||||
- Path format inconsistencies
|
||||
- Missing thumbnail files
|
||||
- Provides SQL to fix path issues
|
||||
|
||||
### Common Issues:
|
||||
1. **Path mismatch**: Database has wrong thumbnail path format
|
||||
2. **Missing files**: Thumbnails were never generated
|
||||
3. **Permission issues**: Web server can't read thumbnail files
|
||||
@@ -62,9 +62,14 @@ export const AuthenticatedImage: React.FC<AuthenticatedImageProps> = ({
|
||||
let imageUrl = src;
|
||||
|
||||
// Build full URL for the image
|
||||
const fullImageUrl = imageUrl.startsWith('/') ? buildResourceUrl(imageUrl) : imageUrl;
|
||||
// For API paths that start with /admin, we need to prepend /api
|
||||
const fullImageUrl = imageUrl.startsWith('/admin')
|
||||
? buildResourceUrl(`/api${imageUrl}`)
|
||||
: imageUrl.startsWith('/')
|
||||
? buildResourceUrl(imageUrl)
|
||||
: imageUrl;
|
||||
|
||||
console.log('Fetching authenticated image:', fullImageUrl);
|
||||
// console.log('Fetching authenticated image:', fullImageUrl);
|
||||
const response = await fetch(fullImageUrl, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Plus,
|
||||
@@ -33,12 +33,47 @@ export const EventsListPage: React.FC = () => {
|
||||
const [selectedEvents, setSelectedEvents] = useState<number[]>([]);
|
||||
// const [showFilters, setShowFilters] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<number | null>(null);
|
||||
const [dropdownPosition, setDropdownPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
const [showBulkArchiveModal, setShowBulkArchiveModal] = useState(false);
|
||||
|
||||
// Get filter from URL
|
||||
const statusFilter = searchParams.get('filter') as 'active' | 'archived' | null;
|
||||
const isExpiringFilter = searchParams.get('filter') === 'expiring';
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.dropdown-container')) {
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (activeDropdown !== null) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [activeDropdown]);
|
||||
|
||||
// Update dropdown position on scroll/resize
|
||||
useEffect(() => {
|
||||
const handleScrollOrResize = () => {
|
||||
if (activeDropdown !== null) {
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScrollOrResize, true);
|
||||
window.addEventListener('resize', handleScrollOrResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScrollOrResize, true);
|
||||
window.removeEventListener('resize', handleScrollOrResize);
|
||||
};
|
||||
}, [activeDropdown]);
|
||||
|
||||
// Fetch events
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['admin-events', statusFilter],
|
||||
@@ -347,21 +382,38 @@ export const EventsListPage: React.FC = () => {
|
||||
{event.expires_at ? format(parseISO(event.expires_at), 'MMM d, yyyy') : 'N/A'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="relative inline-block text-left">
|
||||
<div className="relative inline-block text-left dropdown-container">
|
||||
<button
|
||||
onClick={() => setActiveDropdown(activeDropdown === event.id ? null : event.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeDropdown === event.id) {
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
} else {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setActiveDropdown(event.id);
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.right - 224 + window.scrollX // 224px = 14rem (w-56)
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-neutral-400 hover:text-neutral-600 p-1"
|
||||
>
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{activeDropdown === event.id && (
|
||||
<div className="absolute right-0 z-10 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
|
||||
{activeDropdown === event.id && dropdownPosition && (
|
||||
<div
|
||||
className="fixed z-50 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5"
|
||||
style={{ top: `${dropdownPosition.top}px`, left: `${dropdownPosition.left}px` }}
|
||||
>
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/admin/events/${event.id}`);
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
|
||||
>
|
||||
@@ -374,7 +426,10 @@ export const EventsListPage: React.FC = () => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
|
||||
onClick={() => setActiveDropdown(null)}
|
||||
onClick={() => {
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
{t('events.viewGallery')}
|
||||
@@ -385,6 +440,7 @@ export const EventsListPage: React.FC = () => {
|
||||
onClick={() => {
|
||||
archiveMutation.mutate(event.id);
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
|
||||
>
|
||||
@@ -397,6 +453,7 @@ export const EventsListPage: React.FC = () => {
|
||||
onClick={() => {
|
||||
toast.info(t('events.downloadArchiveSoon'));
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 flex items-center gap-2"
|
||||
>
|
||||
@@ -409,6 +466,7 @@ export const EventsListPage: React.FC = () => {
|
||||
if (confirm(t('events.deleteEventConfirm'))) {
|
||||
deleteMutation.mutate(event.id);
|
||||
setActiveDropdown(null);
|
||||
setDropdownPosition(null);
|
||||
}
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
|
||||
|
||||
@@ -100,7 +100,7 @@ export const settingsService = {
|
||||
formData.append('logo', file);
|
||||
|
||||
const response = await api.post<{ logoUrl: string }>(
|
||||
'/api/admin/settings/logo',
|
||||
'/admin/settings/logo',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
@@ -118,7 +118,7 @@ export const settingsService = {
|
||||
formData.append('favicon', file);
|
||||
|
||||
const response = await api.post<{ faviconUrl: string }>(
|
||||
'/api/admin/settings/favicon',
|
||||
'/admin/settings/favicon',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
@@ -136,7 +136,7 @@ export const settingsService = {
|
||||
formData.append('watermarkLogo', file);
|
||||
|
||||
const response = await api.post<{ watermarkLogoUrl: string }>(
|
||||
'/api/admin/settings/branding/watermark-logo',
|
||||
'/admin/settings/branding/watermark-logo',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user