Bilder von Hand sortieren
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user