more detail on index; move delete to recipe page

This commit is contained in:
fred 2025-08-06 16:18:06 -07:00
parent 27ac9cd92d
commit 91439cbcfa
9 changed files with 95 additions and 75 deletions

View file

@ -15,7 +15,7 @@ app.get("/backend/test", async (req, res) => {
// ### GET ALL RECIPES ### // ### GET ALL RECIPES ###
app.get("/backend/recipes", async (req, res) => { app.get("/backend/recipes", async (req, res) => {
try { try {
const recipes = await db("recipes").select("id", "name", "cuisine"); const recipes = await db("recipes").select("id", "name", "cuisine", "stars", "prep_minutes", "cook_minutes");
res.json(recipes); res.json(recipes);
} catch (err) { } catch (err) {
console.log(err); console.log(err);

View file

@ -1,55 +1,21 @@
import { useState } from 'react';
import { type RecipeSmall } from "../types/Recipe" import { type RecipeSmall } from "../types/Recipe"
import Modal from '../components/Modal.tsx'
import DemoModal from '../components/DemoModal.tsx'
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import StarRating from '../components/StarRating.tsx'
interface CookbookRecipeTileProps { function CookbookRecipeTile({ recipe }: { recipe: RecipeSmall }) {
recipe: RecipeSmall;
handleDelete: (id: number | undefined) => void;
}
function CookbookRecipeTile({ recipe, handleDelete }: CookbookRecipeTileProps) {
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [showDemoModal, setShowDemoModal] = useState(false);
const openModal = () => { setShowConfirmModal(true) };
const closeModal = () => { setShowConfirmModal(false) };
const confirmDelete = () => {
if (process.env.NODE_ENV === 'demo') {
closeModal();
setShowDemoModal(true);
} else {
handleDelete(recipe.id);
closeModal();
}
};
return ( return (
<div className="recipe-card m-2 bg-amber-300 p-4 rounded shadow"> <div className="recipe-card m-2 bg-amber-200 p-4 rounded shadow">
<div className="flex justify-between items-center recipe-name"> <div className="flex justify-between items-center recipe-name">
<h3 className="font-bold"><Link to={`/recipe/${recipe.id}`} className="text-blue-500">{recipe.name}</Link></h3> <h3 className="font-bold text-xl"><Link to={`/recipe/${recipe.id}`} className="text-blue-500">{recipe.name}</Link></h3>
<button onClick={openModal} className="text-red-500 focus:outline-none"> <div className="ar-button bg-amber-600 text-white py-0 px-2 rounded m-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="w-6 h-6"> {recipe.cuisine}
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> </div>
</svg> </div>
</button> <div className="flex justify-between items-center">
{recipe.prep_minutes + recipe.cook_minutes} min
<StarRating rating={recipe.stars} onRatingChange={() => { }} />
</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> </div>
); );
}; };

View file

@ -30,9 +30,6 @@ const RecipeBookTabs = () => {
useEffect(() => { useEffect(() => {
if (!lastViewedRecipeId) { if (!lastViewedRecipeId) {
loadRandomRecipeId(); loadRandomRecipeId();
} else {
console.log('id found', lastViewedRecipeId)
} }
}, []); }, []);

View file

@ -13,7 +13,7 @@ const StarRating = ({ rating, onRatingChange }: StarRatingProps) => {
<span <span
key={index} key={index}
onClick={() => onRatingChange(index)} onClick={() => onRatingChange(index)}
style={{ color: index <= rating ? 'gold' : 'gray', fontSize: '2rem', cursor: 'pointer' }} style={{ color: index <= rating ? '#FFB800' : 'gray', fontSize: '1.5rem', cursor: 'pointer' }}
> >
</span> </span>

View file

@ -5,7 +5,6 @@ import AddBulkIngredients from "../components/AddBulkIngredients.tsx"
import AddBulkSteps from "../components/AddBulkSteps.tsx" import AddBulkSteps from "../components/AddBulkSteps.tsx"
import StarRating from "../components/StarRating.tsx" import StarRating from "../components/StarRating.tsx"
import DemoModal from '../components/DemoModal.tsx' import DemoModal from '../components/DemoModal.tsx'
// import { type Step } from "../types/Recipe";
interface Step { interface Step {
step_number: number; step_number: number;
@ -14,7 +13,6 @@ interface Step {
function AddRecipe() { function AddRecipe() {
const [newRecipeId, setNewRecipeId] = useState<number | null>(null); const [newRecipeId, setNewRecipeId] = useState<number | null>(null);
const navigate = useNavigate();
const [ingredients, setIngredients] = useState<string[]>([]); const [ingredients, setIngredients] = useState<string[]>([]);
const [steps, setSteps] = useState<Step[]>([]); const [steps, setSteps] = useState<Step[]>([]);
const [showBulkForm, setShowBulkForm] = useState(true); const [showBulkForm, setShowBulkForm] = useState(true);
@ -25,6 +23,7 @@ function AddRecipe() {
const [prepMinutes, setPrepMinutes] = useState(5); const [prepMinutes, setPrepMinutes] = useState(5);
const [cookMinutes, setCookMinutes] = useState(5); const [cookMinutes, setCookMinutes] = useState(5);
const [showDemoModal, setShowDemoModal] = useState(false); const [showDemoModal, setShowDemoModal] = useState(false);
const navigate = useNavigate();
const addRecipeForm = async (event: React.FormEvent) => { const addRecipeForm = async (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import CookbookRecipeTile from "../components/CookbookRecipeTile.tsx" import CookbookRecipeTile from "../components/CookbookRecipeTile.tsx"
import { getRecipes, deleteRecipe } from "../services/frontendApi.js"; import { getRecipes } from "../services/frontendApi.js";
import { type RecipeSmall } from "../types/Recipe.ts" import { type RecipeSmall } from "../types/Recipe.ts"
@ -18,12 +18,13 @@ function AllRecipes() {
try { try {
const recipes = await getRecipes(); const recipes = await getRecipes();
setRecipes(recipes); setRecipes(recipes);
if (process.env.NODE_ENV === 'dev') {
console.log(recipes) console.log(recipes)
}
const uniqueCuisines: string[] = recipes.length > 0 const uniqueCuisines: string[] = recipes.length > 0
? Array.from(new Set(recipes.map((recipe: RecipeSmall) => recipe.cuisine))) ? Array.from(new Set(recipes.map((recipe: RecipeSmall) => recipe.cuisine)))
: []; : [];
setCuisines(uniqueCuisines) setCuisines(uniqueCuisines)
console.log(cuisines)
} catch (error) { } catch (error) {
console.log(error); console.log(error);
setError("Failed to load recipes..."); setError("Failed to load recipes...");
@ -36,14 +37,6 @@ function AllRecipes() {
} }
}, [shouldFetchRecipes]); }, [shouldFetchRecipes]);
const handleDelete = async (id: number | void) => {
try {
await deleteRecipe(id);
setShouldFetchRecipes(true);
} catch (error) {
console.error("Error deleting recipe:", error);
}
};
const filteredRecipes = selectedCuisine ? recipes.filter(recipe => recipe.cuisine === selectedCuisine) : recipes; const filteredRecipes = selectedCuisine ? recipes.filter(recipe => recipe.cuisine === selectedCuisine) : recipes;
return ( return (
@ -74,7 +67,7 @@ function AllRecipes() {
<div className="recipes-grid grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8"> <div className="recipes-grid grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
{filteredRecipes.map((recipe) => ( {filteredRecipes.map((recipe) => (
recipe.name.toLowerCase().includes(searchQuery.toLowerCase()) && recipe.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
<CookbookRecipeTile recipe={recipe} key={recipe.id} handleDelete={handleDelete} /> <CookbookRecipeTile recipe={recipe} key={recipe.id} />
))} ))}
</div> </div>
</div> </div>

View file

@ -1,9 +1,11 @@
import { useParams } from "react-router-dom"; import { useParams, useNavigate, Link } from "react-router-dom";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getRecipeById } from "../services/frontendApi.js"; import { getRecipeById, deleteRecipe } from "../services/frontendApi.js";
import { type Recipe, type Ingredient } from "../types/Recipe" import { type Recipe, type Ingredient } from "../types/Recipe"
import StarRating from "../components/StarRating.tsx" import StarRating from "../components/StarRating.tsx"
import { setDBStars } from "../services/frontendApi.js"; import { setDBStars } from "../services/frontendApi.js";
import Modal from '../components/Modal.tsx'
import DemoModal from '../components/DemoModal.tsx'
function RecipePage() { function RecipePage() {
const [recipe, setRecipe] = useState<Recipe>({ const [recipe, setRecipe] = useState<Recipe>({
@ -13,21 +15,43 @@ function RecipePage() {
}); });
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true); 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 { id } = useParams();
const isWebSource = recipe && recipe.details && recipe.details.author const isWebSource = recipe && recipe.details && recipe.details.author
? /http|com/.test(recipe.details.author) //etc ? /http|com/.test(recipe.details.author) //etc
: false; : false;
const [stars, setStars] = useState<number>(0); const navigate = useNavigate();
const [initialStars, setInitialStars] = useState<number | null>(null);
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(() => { useEffect(() => {
const loadRecipe = async () => { const loadRecipe = async () => {
try { try {
const recipe = await getRecipeById(id); const recipe = await getRecipeById(id);
if (!recipe.details) {
setError("Sorry, this recipe no longer exists")
} else {
setRecipe(recipe); setRecipe(recipe);
setStars(recipe.details?.stars ?? 0) setStars(recipe.details?.stars ?? 0)
setInitialStars(recipe.details?.stars ?? 0); setInitialStars(recipe.details?.stars ?? 0);
if (process.env.NODE_ENV === 'dev') {
console.log(recipe) console.log(recipe)
}
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
setError("Failed to load recipes..."); setError("Failed to load recipes...");
@ -49,16 +73,36 @@ function RecipePage() {
updateStarsInDB(); updateStarsInDB();
}, [stars]); }, [stars]);
const handleDelete = async (id: number | void) => {
try {
await deleteRecipe(id);
navigate('/')
} catch (error) {
console.error("Error deleting recipe:", error);
}
};
return ( return (
<div className="recipe page-outer"> <div className="recipe page-outer">
{error && <div className="error-message">{error}</div>}
{loading ? ( {loading ? (
<div className="loading">Loading...</div> <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-amber-600 text-white py-2 px-4 rounded hover:bg-amber-700">
Return to Cookbook
</Link>
</div>
</div>
) : ( ) : (
<div className="recipe-card"> <div className="recipe-card relative">
<button onClick={openModal} className="ar-button bg-gray-200 text-white py-1 px-2 rounded hover:bg-gray-300 m-2 absolute top-0 right-0">
🗑
</button>
<div className="border-b-2 border-amber-300 pb-4 mb-6"> <div className="border-b-2 border-amber-300 pb-4 mb-6">
<h3 className="text-2xl lg:text-3xl font-bold text-amber-900 mb-2">{recipe.details.name}</h3> <h3 className="text-2xl lg:text-3xl font-bold text-amber-900 mb-2">{recipe.details.name}</h3>
<p className="text-amber-700 italic text-lg">{recipe.details.cuisine}</p> <p className="text-amber-700 italic text-lg">{recipe.details.cuisine}</p>
@ -110,8 +154,23 @@ function RecipePage() {
</div> </div>
</div> </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> </div >
); );
} }

View file

@ -23,6 +23,9 @@ export const getRecipeIngredients = async () => {
export const getRecipeById = async (id) => { export const getRecipeById = async (id) => {
const response = await fetch(`${baseUrl}backend/recipe/${id}`); const response = await fetch(`${baseUrl}backend/recipe/${id}`);
const data = await response.json(); const data = await response.json();
if (!data || !data.details) {
return { details: null };
}
return data; return data;
}; };

View file

@ -29,6 +29,9 @@ interface RecipeSmall {
id: number; id: number;
name: string; name: string;
cuisine: string; cuisine: string;
stars: number;
cook_minutes: number;
prep_minutes: number;
} }