diff --git a/app/api/meals/[id]/route.ts b/app/api/meals/[id]/route.ts new file mode 100644 index 0000000..5048e43 --- /dev/null +++ b/app/api/meals/[id]/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getSession } from "@/lib/auth"; + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await getSession(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { id } = await params; + const mealId = parseInt(id); + if (isNaN(mealId)) { + return NextResponse.json({ error: "Invalid meal ID" }, { status: 400 }); + } + + try { + const meal = await prisma.meal.findUnique({ + where: { id: mealId }, + }); + + if (!meal) { + return NextResponse.json({ error: "Meal not found" }, { status: 404 }); + } + + if (meal.userId !== session.user.id) { + return NextResponse.json( + { error: "You can only delete your own meals" }, + { status: 403 } + ); + } + + await prisma.meal.delete({ + where: { id: mealId }, + }); + + return NextResponse.json({ ok: true }, { status: 200 }); + } catch (error) { + console.error("Error deleting meal:", error); + return NextResponse.json( + { error: "Failed to delete meal" }, + { status: 500 } + ); + } +} diff --git a/app/api/meals/route.ts b/app/api/meals/route.ts new file mode 100644 index 0000000..e0b2dbd --- /dev/null +++ b/app/api/meals/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getSession } from "@/lib/auth"; +import { MealType } from "@/app/generated/prisma/enums"; + +export async function POST(request: Request) { + const session = await getSession(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = (await request.json().catch(() => null)) as + | { + name: string; + description?: string; + ingredients: Record; + recipe?: string[]; + suggestedMealType: MealType[]; + tagIds: number[]; + } + | null; + + if (!body || !body.name || !body.ingredients || !body.suggestedMealType) { + return NextResponse.json( + { error: "Missing required fields: name, ingredients, suggestedMealType" }, + { status: 400 } + ); + } + + try { + const meal = await prisma.meal.create({ + data: { + name: body.name, + description: body.description || null, + ingredients: body.ingredients, + recipe: body.recipe || null, + suggestedMealType: body.suggestedMealType, + userId: session.user.id, + tags: { + connect: body.tagIds.map((id) => ({ id })), + }, + }, + include: { + tags: true, + }, + }); + + return NextResponse.json({ ok: true, meal }, { status: 201 }); + } catch (error) { + console.error("Error creating meal:", error); + return NextResponse.json( + { error: "Failed to create meal" }, + { status: 500 } + ); + } +} diff --git a/app/meals/DeleteMealButton.tsx b/app/meals/DeleteMealButton.tsx new file mode 100644 index 0000000..0d460fb --- /dev/null +++ b/app/meals/DeleteMealButton.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +interface Props { + mealId: number; + mealName: string; +} + +export default function DeleteMealButton({ mealId, mealName }: Props) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + + const handleDelete = async () => { + setLoading(true); + try { + const res = await fetch(`/api/meals/${mealId}`, { + method: "DELETE", + }); + + if (res.ok) { + router.refresh(); + } else { + const data = await res.json(); + alert(data.error || "Nie udało się usunąć posiłku"); + } + } catch (err) { + console.error("Error deleting meal:", err); + alert("Wystąpił błąd podczas usuwania posiłku"); + } finally { + setLoading(false); + setShowConfirm(false); + } + }; + + if (!showConfirm) { + return ( + + ); + } + + return ( +
+ + +
+ ); +} diff --git a/app/meals/new/NewMealForm.tsx b/app/meals/new/NewMealForm.tsx new file mode 100644 index 0000000..1e9dab8 --- /dev/null +++ b/app/meals/new/NewMealForm.tsx @@ -0,0 +1,328 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { MealType } from "@/app/generated/prisma/enums"; + +interface Tag { + id: number; + name: string; +} + +interface Props { + tags: Tag[]; +} + +const mealTypeLabels: Record = { + BREAKFAST: "Śniadanie", + LUNCH: "Obiad", + DINNER: "Kolacja", + DESSERT: "Deser", +}; + +export default function NewMealForm({ tags }: Props) { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [ingredients, setIngredients] = useState<{ name: string; amount: string }[]>([ + { name: "", amount: "" }, + ]); + const [recipeSteps, setRecipeSteps] = useState([""]); + const [selectedMealTypes, setSelectedMealTypes] = useState([]); + const [selectedTagIds, setSelectedTagIds] = useState([]); + + const addIngredient = () => { + setIngredients([...ingredients, { name: "", amount: "" }]); + }; + + const removeIngredient = (index: number) => { + setIngredients(ingredients.filter((_, i) => i !== index)); + }; + + const updateIngredient = (index: number, field: "name" | "amount", value: string) => { + const updated = [...ingredients]; + updated[index][field] = value; + setIngredients(updated); + }; + + const addRecipeStep = () => { + setRecipeSteps([...recipeSteps, ""]); + }; + + const removeRecipeStep = (index: number) => { + setRecipeSteps(recipeSteps.filter((_, i) => i !== index)); + }; + + const updateRecipeStep = (index: number, value: string) => { + const updated = [...recipeSteps]; + updated[index] = value; + setRecipeSteps(updated); + }; + + const toggleMealType = (mealType: MealType) => { + if (selectedMealTypes.includes(mealType)) { + setSelectedMealTypes(selectedMealTypes.filter((t) => t !== mealType)); + } else { + setSelectedMealTypes([...selectedMealTypes, mealType]); + } + }; + + const toggleTag = (tagId: number) => { + if (selectedTagIds.includes(tagId)) { + setSelectedTagIds(selectedTagIds.filter((id) => id !== tagId)); + } else { + setSelectedTagIds([...selectedTagIds, tagId]); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!name.trim()) { + setError("Nazwa posiłku jest wymagana"); + return; + } + + if (selectedMealTypes.length === 0) { + setError("Wybierz przynajmniej jeden typ posiłku"); + return; + } + + const validIngredients = ingredients.filter( + (ing) => ing.name.trim() && ing.amount.trim() + ); + + if (validIngredients.length === 0) { + setError("Dodaj przynajmniej jeden składnik"); + return; + } + + const ingredientsObj: Record = {}; + for (const ing of validIngredients) { + const amount = parseFloat(ing.amount); + if (isNaN(amount) || amount <= 0) { + setError(`Nieprawidłowa ilość dla składnika: ${ing.name}`); + return; + } + ingredientsObj[ing.name.trim()] = amount; + } + + const validRecipeSteps = recipeSteps.filter((step) => step.trim()); + + setLoading(true); + + try { + const res = await fetch("/api/meals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: name.trim(), + description: description.trim() || undefined, + ingredients: ingredientsObj, + recipe: validRecipeSteps.length > 0 ? validRecipeSteps : undefined, + suggestedMealType: selectedMealTypes, + tagIds: selectedTagIds, + }), + }); + + if (res.ok) { + router.push("/meals"); + } else { + const data = await res.json(); + setError(data.error || "Nie udało się dodać posiłku"); + } + } catch (err) { + console.error("Error submitting meal:", err); + setError("Wystąpił błąd podczas dodawania posiłku"); + } finally { + setLoading(false); + } + }; + + return ( +
+ {error && ( +
+ {error} +
+ )} + + {/* Name */} +
+ + setName(e.target.value)} + className="w-full rounded-lg border border-zinc-700 bg-zinc-900/60 px-4 py-2 text-zinc-100 focus:border-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50" + placeholder="np. Spaghetti Carbonara" + /> +
+ + {/* Description */} +
+ +