Sieht gut aus und geht (noch keine Bildeingabe)

This commit is contained in:
rxf
2025-09-22 09:41:01 +02:00
parent 6f93db4a12
commit 6d04ab93c0
79 changed files with 16233 additions and 0 deletions

View 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;