recipe_app/frontend/src/pages/RecipePage.tsx
2025-10-20 11:43:29 -07:00

234 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useParams, useNavigate, Link } from "react-router-dom";
import { useState, useEffect } from "react";
import {
getRecipeById,
deleteRecipe,
setDBStars,
} from "../services/frontendApi.js";
import { type Recipe } from "../types/Recipe";
import Modal from "../components/Modal.tsx";
import DemoModal from "../components/DemoModal.tsx";
import StarRating from "../components/StarRating.tsx";
import TimeDisplay from "../components/TimeDisplay.tsx";
function RecipePage() {
const [recipe, setRecipe] = useState<Recipe>({
details: {},
ingredients: [],
steps: [],
});
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [stars, setStars] = useState<number>(0);
const [initialStars, setInitialStars] = useState<number | null>(null);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [showDemoModal, setShowDemoModal] = useState(false);
const { id } = useParams();
const isWebSource =
recipe && recipe.details && recipe.details.author
? /http|com/.test(recipe.details.author) //etc
: false;
const navigate = useNavigate();
const openModal = () => {
setShowConfirmModal(true);
};
const closeModal = () => {
setShowConfirmModal(false);
};
const confirmDelete = () => {
if (process.env.NODE_ENV === "demo") {
closeModal();
setShowDemoModal(true);
} else {
handleDelete(recipe.details.id);
closeModal();
}
};
useEffect(() => {
const loadRecipe = async () => {
try {
const recipe = await getRecipeById(id);
if (!recipe.details) {
setError("Sorry, this recipe no longer exists");
} else {
setRecipe(recipe);
setStars(recipe.details?.stars ?? 0);
setInitialStars(recipe.details?.stars ?? 0);
if (process.env.NODE_ENV === "dev") {
console.log(recipe);
}
}
} catch (error) {
console.log(error);
setError("Failed to load recipes...");
} finally {
setLoading(false);
}
};
loadRecipe();
}, [id]);
useEffect(() => {
if (initialStars === null || initialStars === stars) {
return;
}
const updateStarsInDB = async () => {
await setDBStars(id, stars);
};
updateStarsInDB();
}, [stars]);
const handleDelete = async (id: number | void) => {
try {
await deleteRecipe(id);
navigate("/");
} catch (error) {
console.error("Error deleting recipe:", error);
}
};
return (
<div className="recipe page-outer">
{loading ? (
<div className="loading">Loading...</div>
) : error ? (
<div>
<div className="error-message text-lg">{error}</div>
<div className="m-2">
<Link
to="/"
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-2 px-4 rounded hover:bg-[var(--color-buttonBgHover)]"
>
Return to Cookbook
</Link>
</div>
</div>
) : (
<div className="border-b-2 border-[var(--color-primaryBorder)] pb-4">
<div className="recipe-card">
<div className="flex relative justify-between">
<div className="invisible-buttons">
<button
onClick={() => {}}
className="invisible ar-button py-1 px-1 rounded mr-2 self-start"
>
🗑
</button>
<button
onClick={() => {}}
className="invisible ar-button py-1 px-1 rounded self-start"
>
🗑
</button>
</div>
<h3 className="text-center max-w-lg px-4 text-2xl lg:text-3xl font-bold text-[var(--color-textDark)]">
{recipe.details.name}
</h3>
<div className="modify-buttons">
<button
onClick={() => navigate(`/edit-recipe/${recipe.details.id}`)}
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] mr-2 self-start"
>
🔧
</button>
<button
onClick={openModal}
className="ar-button bg-[var(--color-buttonBg)] text-[var(--color-textLight)] py-1 px-1 rounded hover:bg-[var(--color-buttonBgHover)] self-start"
>
🗑
</button>
</div>
</div>
<div className="mt-1">
<p className="text-[var(--color-textDark)] italic text-lg">
{recipe.details.cuisine}
</p>
<p>
prep: <TimeDisplay minutes={recipe.details.prep_minutes ?? 0} />{" "}
| cook:{" "}
<TimeDisplay minutes={recipe.details.cook_minutes ?? 0} />
</p>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-6 mb-6">
<div className="bg-[var(--color-backdrop)] rounded-lg p-4 shadow-sm border border-[var(--color-primaryBorder)]">
<h4 className="text-xl font-semibold text-[var(--color-textDark)] mb-3 flex items-center">
Ingredients:
</h4>
<ul className="space-y-2">
{recipe.ingredients.map((ingredient: string, index) => (
<li
key={index}
className="text-[var(--color-secondaryTextDark)] flex items-start"
>
<span className="w-1.5 h-1.5 bg-[var(--color-buttonBg)] rounded-full mt-2 mr-3 flex-shrink-0"></span>
<span className="font-medium text-left">{ingredient}</span>
</li>
))}
</ul>
</div>
<div className="bg-[var(--color-backdrop)] rounded-lg p-4 shadow-sm border border-[var(--color-primaryBorder)]">
<h4 className="text-xl font-semibold text-[var(--color-textDark)] mb-3 flex items-center">
Instructions:
</h4>
<ol className="space-y-3">
{recipe.steps &&
Object.keys(recipe.steps || {}).map((stepNumber) => (
<li
key={stepNumber}
className="text-[var(--color-secondaryTextDark)] flex items-start"
>
<span className="bg-[var(--color-buttonBg)] text-[var(--color-textLight)] rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold mr-3 mt-0.5 flex-shrink-0">
{recipe.steps[parseInt(stepNumber)].step_number}
</span>
<span className="leading-relaxed text-left">
{recipe.steps[parseInt(stepNumber)].instruction}
</span>
</li>
))}
</ol>
</div>
</div>
<div className="border-t-2 border-[var(--color-primaryBorder)] pt-4">
<div className="flex justify-between items-center text-sm text-[var(--color-textDark)]">
{isWebSource ? (
<span>Source: {recipe.details.author}</span>
) : (
<span>From the kitchen of {recipe.details.author}</span>
)}
<span>
<StarRating
rating={stars}
onRatingChange={(newRating: number) => setStars(newRating)}
/>
</span>
</div>
</div>
</div>
)}
<Modal
isOpen={showConfirmModal}
onClose={closeModal}
message="Are you sure you want to delete this recipe?"
confirmAction={confirmDelete}
cancelAction={closeModal}
/>
{showDemoModal && (
<DemoModal
isOpen={showDemoModal}
onClose={() => setShowDemoModal(false)}
closeModal={() => setShowDemoModal(false)}
/>
)}
</div>
);
}
export default RecipePage;