more detail on index; move delete to recipe page
This commit is contained in:
parent
27ac9cd92d
commit
91439cbcfa
9 changed files with 95 additions and 75 deletions
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,9 +30,6 @@ const RecipeBookTabs = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastViewedRecipeId) {
|
if (!lastViewedRecipeId) {
|
||||||
loadRandomRecipeId();
|
loadRandomRecipeId();
|
||||||
} else {
|
|
||||||
console.log('id found', lastViewedRecipeId)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
console.log(recipes)
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
|
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>
|
||||||
|
|
|
@ -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);
|
||||||
setRecipe(recipe);
|
if (!recipe.details) {
|
||||||
setStars(recipe.details?.stars ?? 0)
|
setError("Sorry, this recipe no longer exists")
|
||||||
setInitialStars(recipe.details?.stars ?? 0);
|
} else {
|
||||||
console.log(recipe)
|
setRecipe(recipe);
|
||||||
|
setStars(recipe.details?.stars ?? 0)
|
||||||
|
setInitialStars(recipe.details?.stars ?? 0);
|
||||||
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
|
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 >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue