Bilder von Hand sortieren

This commit is contained in:
2025-09-25 19:09:58 +00:00
parent da9d08c149
commit 0bfb8b2074
16 changed files with 462 additions and 163 deletions

View File

@@ -185,7 +185,8 @@
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
background: #f8f9fa;
/* Use pure white so transparent PNGs are not auf grauem Untergrund leicht "ausgewaschen" */
background: #ffffff;
max-height: 400px;
}
@@ -475,6 +476,8 @@
width: 100%;
height: 150px;
overflow: hidden;
/* Neutraler Hintergrund für transparente Bereiche */
background: #ffffff;
}
.image-preview img {

View File

@@ -102,6 +102,28 @@ const RecipeDetail: React.FC = () => {
}
};
const handleReorder = async (imageId: number, direction: 'up' | 'down') => {
if (!recipe || !recipe.images || !id) return;
const idx = recipe.images.findIndex(img => img.id === imageId);
if (idx === -1) return;
const swapWith = direction === 'up' ? idx - 1 : idx + 1;
if (swapWith < 0 || swapWith >= recipe.images.length) return;
const newOrder = [...recipe.images];
[newOrder[idx], newOrder[swapWith]] = [newOrder[swapWith], newOrder[idx]];
try {
// Send ordered IDs
await imageApi.reorderImages(parseInt(id), newOrder.map(i => i.id));
// Reload recipe to pick up new filenames (indexes changed)
const response = await recipeApi.getRecipe(parseInt(id));
if (response.success) setRecipe(response.data);
} catch (e) {
console.error('Reorder failed', e);
setError('Fehler beim Neusortieren der Bilder');
}
};
if (loading) {
return (
<div className="recipe-detail">
@@ -143,6 +165,15 @@ const RecipeDetail: React.FC = () => {
);
}
// Helper utilities for image handling
const getFileName = (fp: string) => fp.split('/').pop() || '';
const isMainImage = (fileName: string) => /_0\.(jpg|jpeg|png|webp)$/i.test(fileName);
const isStepImage = (fileName: string) => /_[1-9]\d*\.(jpg|jpeg|png|webp)$/i.test(fileName);
const getStepIndex = (fileName: string) => {
const m = fileName.match(/_(\d+)\.(jpg|jpeg|png|webp)$/i);
return m ? parseInt(m[1], 10) : -1;
};
return (
<div className="recipe-detail">
{/* Header */}
@@ -185,12 +216,12 @@ const RecipeDetail: React.FC = () => {
{recipe.images && recipe.images.length > 0 && (
<div className="main-recipe-image">
{(() => {
// Find the main image (xxx_0.jpg)
const mainImage = recipe.images.find(image => {
const fileName = image.filePath.split('/').pop() || '';
return fileName.includes('_0.jpg');
});
// Find the main image *_0.<ext>; if none, fallback to first image
let mainImage = recipe.images.find(img => isMainImage(getFileName(img.filePath)));
if (!mainImage && recipe.images.length > 0) {
mainImage = recipe.images[0];
}
if (mainImage) {
return (
<img
@@ -255,6 +286,22 @@ const RecipeDetail: React.FC = () => {
src={imageApi.getImageUrl(image.filePath)}
alt={`Bild ${index + 1}`}
/>
<div className="image-actions-inline" style={{ position: 'absolute', top: 4, left: 4, display: 'flex', gap: '4px' }}>
<button
disabled={index === 0}
onClick={() => handleReorder(image.id, 'up')}
title="Nach oben"
style={{ opacity: index === 0 ? 0.4 : 1 }}
className="reorder-btn"
></button>
<button
disabled={index === recipe.images!.length - 1}
onClick={() => handleReorder(image.id, 'down')}
title="Nach unten"
style={{ opacity: index === recipe.images!.length - 1 ? 0.4 : 1 }}
className="reorder-btn"
></button>
</div>
<button
className="delete-image-btn"
onClick={() => handleImageDelete(image.id)}
@@ -267,7 +314,7 @@ const RecipeDetail: React.FC = () => {
<span className="image-name">
{image.filePath.split('/').pop()}
</span>
{image.filePath.includes('_0.jpg') && (
{isMainImage(getFileName(image.filePath)) && (
<span className="main-image-badge">Hauptbild</span>
)}
</div>
@@ -330,19 +377,11 @@ const RecipeDetail: React.FC = () => {
{(() => {
// Get all preparation images (exclude main image _0.jpg)
const preparationImages = recipe.images
?.filter(image => {
const fileName = image.filePath.split('/').pop() || '';
// Match pattern like R005_1.jpg, R005_2.jpg, etc. but not R005_0.jpg
return fileName.match(/_[1-9]\d*\.jpg$/);
?.filter(img => {
const fn = getFileName(img.filePath);
return isStepImage(fn);
})
.sort((a, b) => {
// Sort by the number in the filename
const getNumber = (path: string) => {
const match = path.match(/_(\d+)\.jpg$/);
return match ? parseInt(match[1]) : 0;
};
return getNumber(a.filePath) - getNumber(b.filePath);
}) || [];
.sort((a, b) => getStepIndex(getFileName(a.filePath)) - getStepIndex(getFileName(b.filePath))) || [];
return recipe.instructions.split('\n').map((instruction, index) => {
// Get the corresponding image for this step

View File

@@ -178,6 +178,12 @@ export const imageApi = {
const response = await api.delete(`/images/${imageId}`);
return response.data;
},
// Reorder images for a recipe
reorderImages: async (recipeId: number, orderedIds: number[]): Promise<ApiResponse<RecipeImage[]>> => {
const response = await api.post(`/images/reorder/${recipeId}`, { order: orderedIds });
return response.data;
},
};
// Health check