fix: multiple improvements and CI/CD updates
Frontend fixes: - Add missing translations for chunk upload (upload.uploadingChunks, common.chunk) - Fix photo deletion visual bug by tracking deletion state per photo - Prevent UI confusion when deleting photos in admin grid Backend fixes: - Add file existence checks before deleting thumbnails - Prevent ENOENT errors for missing thumbnail files - Improve error handling in photo deletion CI/CD updates: - Remove Gitea release creation from Drone pipeline - Simplify GitHub mirror workflow (remove history rewriting, keep file removal) - Add clean-git-history.sh script for manual history cleanup These changes improve the admin photo management experience and streamline the CI/CD process for better maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [deletingPhotoId, setDeletingPhotoId] = useState<number | null>(null);
|
||||
const [deletingPhotos, setDeletingPhotos] = useState<Set<number>>(new Set());
|
||||
|
||||
const handlePhotoSelect = (photoId: number, e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
@@ -54,15 +54,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingPhotoId(photo.id);
|
||||
setDeletingPhotos(prev => new Set(prev).add(photo.id));
|
||||
try {
|
||||
await photosService.deletePhoto(eventId, photo.id);
|
||||
toast.success('Photo deleted successfully');
|
||||
onPhotosDeleted();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete photo');
|
||||
} finally {
|
||||
setDeletingPhotoId(null);
|
||||
setDeletingPhotos(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(photo.id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -75,14 +78,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
const selectedIds = Array.from(selectedPhotos);
|
||||
setDeletingPhotos(new Set(selectedIds));
|
||||
|
||||
try {
|
||||
await photosService.deletePhotos(eventId, Array.from(selectedPhotos));
|
||||
await photosService.deletePhotos(eventId, selectedIds);
|
||||
toast.success(`${count} photo${count > 1 ? 's' : ''} deleted successfully`);
|
||||
setSelectedPhotos(new Set());
|
||||
setIsSelectionMode(false);
|
||||
onPhotosDeleted();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete photos');
|
||||
setDeletingPhotos(new Set());
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
@@ -155,13 +162,15 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
|
||||
{/* Photo Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{photos.map((photo, index) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 ${
|
||||
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
|
||||
}`}
|
||||
onClick={() => isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index)}
|
||||
{photos.map((photo, index) => {
|
||||
const isDeleting = deletingPhotos.has(photo.id);
|
||||
return (
|
||||
<div
|
||||
key={photo.id}
|
||||
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 transition-opacity ${
|
||||
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
|
||||
} ${isDeleting ? 'opacity-50' : ''}`}
|
||||
onClick={() => !isDeleting && (isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index))}
|
||||
>
|
||||
{/* Selection Checkbox */}
|
||||
{isSelectionMode && (
|
||||
@@ -219,8 +228,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDeleteSingle(photo, e)}
|
||||
className="p-1 text-white hover:bg-white/20 rounded"
|
||||
disabled={deletingPhotoId === photo.id}
|
||||
className="p-1 text-white hover:bg-white/20 rounded disabled:opacity-50"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -238,7 +247,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{photos.length === 0 && (
|
||||
|
||||
@@ -256,7 +256,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
<div className="flex justify-between text-sm text-neutral-600 mb-1">
|
||||
<span>
|
||||
{t('upload.uploading')}
|
||||
{totalChunks > 1 && ` (${t('common.chunk') || 'Chunk'} ${currentChunk}/${totalChunks})`}
|
||||
{totalChunks > 1 && ` (${t('common.chunk')} ${currentChunk}/${totalChunks})`}
|
||||
</span>
|
||||
<span>{uploadProgress}%</span>
|
||||
</div>
|
||||
@@ -268,7 +268,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
</div>
|
||||
{totalChunks > 1 && (
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
{t('upload.uploadingChunks') || `Uploading ${selectedFiles.length} files in ${totalChunks} batches...`}
|
||||
{t('upload.uploadingChunks', { count: selectedFiles.length, total: totalChunks })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"days": "Tage",
|
||||
"customize": "Anpassen",
|
||||
"hide": "Ausblenden",
|
||||
"unknown": "Unbekannt"
|
||||
"unknown": "Unbekannt",
|
||||
"chunk": "Teil"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Fotokategorie",
|
||||
@@ -51,7 +52,8 @@
|
||||
"uploadPhotos": "Fotos hochladen",
|
||||
"maxFilesReached": "Maximal 500 Dateien erlaubt",
|
||||
"someFilesSkipped": "Einige Dateien wurden übersprungen (500 Dateien Limit)",
|
||||
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden"
|
||||
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden",
|
||||
"uploadingChunks": "Lade {{count}} Dateien in {{total}} Teilen hoch..."
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
"days": "days",
|
||||
"customize": "Customize",
|
||||
"hide": "Hide",
|
||||
"unknown": "Unknown"
|
||||
"unknown": "Unknown",
|
||||
"chunk": "Chunk"
|
||||
},
|
||||
"upload": {
|
||||
"photoCategory": "Photo Category",
|
||||
@@ -51,7 +52,8 @@
|
||||
"uploadPhotos": "Upload Photos",
|
||||
"maxFilesReached": "Maximum 500 files allowed",
|
||||
"someFilesSkipped": "Some files were skipped (500 file limit)",
|
||||
"tooManyFiles": "Maximum 500 files can be uploaded at once"
|
||||
"tooManyFiles": "Maximum 500 files can be uploaded at once",
|
||||
"uploadingChunks": "Uploading {{count}} files in {{total}} batches..."
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
|
||||
Reference in New Issue
Block a user