Sieht gut aus und geht (noch keine Bildeingabe)
This commit is contained in:
287
nodejs-version/frontend/src/components/RecipeDetail.tsx
Normal file
287
nodejs-version/frontend/src/components/RecipeDetail.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import type { Recipe } from '../services/api';
|
||||
import { recipeApi, imageApi } from '../services/api';
|
||||
import './RecipeDetail.css';
|
||||
|
||||
// Helper function to convert URLs in text to clickable links
|
||||
const linkifyText = (text: string): React.ReactNode => {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const parts = text.split(urlRegex);
|
||||
|
||||
return parts.map((part, index) => {
|
||||
if (urlRegex.test(part)) {
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={part}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="recipe-link"
|
||||
>
|
||||
{part}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
});
|
||||
};
|
||||
|
||||
const RecipeDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [recipe, setRecipe] = useState<Recipe | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRecipe = async () => {
|
||||
if (!id) {
|
||||
setError('Keine Rezept-ID angegeben');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
const response = await recipeApi.getRecipe(parseInt(id));
|
||||
|
||||
if (response.success) {
|
||||
setRecipe(response.data);
|
||||
} else {
|
||||
setError('Rezept konnte nicht geladen werden');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Verbindungsfehler - Ist der Server gestartet?');
|
||||
console.error('Error loading recipe:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRecipe();
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="recipe-detail">
|
||||
<div className="loading">Lade Rezept...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="recipe-detail">
|
||||
<div className="error">
|
||||
<h3>Fehler</h3>
|
||||
<p>{error}</p>
|
||||
<div className="error-actions">
|
||||
<button onClick={() => window.location.reload()} className="retry-button">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
<Link to="/" className="back-button">
|
||||
Zurück zur Liste
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!recipe) {
|
||||
return (
|
||||
<div className="recipe-detail">
|
||||
<div className="not-found">
|
||||
<h3>Rezept nicht gefunden</h3>
|
||||
<p>Das angeforderte Rezept existiert nicht.</p>
|
||||
<Link to="/" className="back-button">
|
||||
Zurück zur Liste
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="recipe-detail">
|
||||
{/* Header */}
|
||||
<div className="recipe-header">
|
||||
<div className="breadcrumb">
|
||||
<Link to="/" className="breadcrumb-link">Alle Rezepte</Link>
|
||||
<span className="breadcrumb-separator">›</span>
|
||||
<span className="breadcrumb-current">{recipe.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="recipe-actions">
|
||||
<Link to={`/recipes/${recipe.id}/edit`} className="edit-button">
|
||||
✏️ Bearbeiten
|
||||
</Link>
|
||||
<button onClick={() => navigate(-1)} className="back-button">
|
||||
← Zurück
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="recipe-content">
|
||||
{/* Recipe Info - Full Width */}
|
||||
<div className="recipe-info">
|
||||
<div className="recipe-title-section">
|
||||
<h1 className="recipe-title">{recipe.title}</h1>
|
||||
{recipe.recipeNumber && (
|
||||
<span className="recipe-number">#{recipe.recipeNumber}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hauptbild (xxx_0.jpg) */}
|
||||
{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');
|
||||
});
|
||||
|
||||
if (mainImage) {
|
||||
return (
|
||||
<img
|
||||
src={imageApi.getImageUrl(mainImage.filePath)}
|
||||
alt={`${recipe.title} - Hauptbild`}
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="recipe-meta">
|
||||
{recipe.category && (
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Kategorie:</span>
|
||||
<span className="meta-value">{recipe.category}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="meta-item">
|
||||
<span className="meta-label">Portionen:</span>
|
||||
<span className="meta-value">👥 {recipe.servings}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two Column Layout for Description/Ingredients and Preparation */}
|
||||
<div className="recipe-columns">
|
||||
{/* Left Column - Description and Ingredients */}
|
||||
<div className="recipe-sidebar">
|
||||
{recipe.description && (
|
||||
<div className="recipe-description">
|
||||
<h3>Beschreibung</h3>
|
||||
<p>{linkifyText(recipe.description)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zutaten */}
|
||||
{recipe.ingredients && (
|
||||
<div className="recipe-section">
|
||||
<h3>Zutaten</h3>
|
||||
<div className="ingredients-content">
|
||||
{recipe.ingredients.split('\n').map((ingredient, index) => (
|
||||
<div key={index} className="ingredient-item">
|
||||
{ingredient.trim()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Preparation and Instructions */}
|
||||
<div className="recipe-main-content">
|
||||
|
||||
{/* Vorbereitung */}
|
||||
{recipe.preparation && (
|
||||
<div className="recipe-section">
|
||||
<h3>Vorbereitung</h3>
|
||||
<div className="preparation-content">
|
||||
{recipe.preparation.split('\n').map((step, index) => (
|
||||
<p key={index} className="preparation-step">
|
||||
{step.trim()}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Anweisungen */}
|
||||
{recipe.instructions && (
|
||||
<div className="recipe-section">
|
||||
<h3>Zubereitung</h3>
|
||||
<div className="instructions-content">
|
||||
{(() => {
|
||||
// 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$/);
|
||||
})
|
||||
.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);
|
||||
}) || [];
|
||||
|
||||
return recipe.instructions.split('\n').map((instruction, index) => {
|
||||
// Get the corresponding image for this step
|
||||
const stepImage = preparationImages[index];
|
||||
|
||||
return (
|
||||
<div key={index} className="instruction-step-with-image">
|
||||
{stepImage && (
|
||||
<div className="step-image">
|
||||
<img
|
||||
src={imageApi.getImageUrl(stepImage.filePath)}
|
||||
alt={`${recipe.title} - Schritt ${index + 1}`}
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="instruction-step">
|
||||
<span className="step-number">{index + 1}</span>
|
||||
<p className="step-text">{instruction.trim()}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kommentar */}
|
||||
{recipe.comment && (
|
||||
<div className="recipe-section">
|
||||
<h3>Tipp</h3>
|
||||
<div className="comment-content">
|
||||
<p className="recipe-comment">{recipe.comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecipeDetail;
|
||||
Reference in New Issue
Block a user