diff --git a/app/models/recipe.server.ts b/app/models/recipe.server.ts index b990856..8712f41 100644 --- a/app/models/recipe.server.ts +++ b/app/models/recipe.server.ts @@ -18,7 +18,7 @@ export function getRecipe({ export function getRecipeListItems({userId}: UserId) { return prisma.recipe.findMany({ - select: {title: true, tags: true}, + select: {title: true, tags: true, id: true}, where: {userId} }) } @@ -46,13 +46,15 @@ export function updateRecipe({ title, source, steps, - ingredients + ingredients, + userId }: Pick & {steps?: Step[]} & {ingredients?: Ingredient[]} +& UserId ) { return prisma.recipe.update({ - where: {id}, + where: {id_userId: { id, userId}}, data: { title, source, diff --git a/app/routes/notes.$noteId.tsx b/app/routes/notes.$noteId.tsx index 3edd6ff..4ada87b 100644 --- a/app/routes/notes.$noteId.tsx +++ b/app/routes/notes.$noteId.tsx @@ -1,5 +1,5 @@ -import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; -import { json, redirect } from "@remix-run/node"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json, } from "@remix-run/node"; import { Form, isRouteErrorResponse, @@ -8,7 +8,7 @@ import { } from "@remix-run/react"; import invariant from "tiny-invariant"; -import { deleteNote, getNote } from "~/models/note.server"; +import { getNote } from "~/models/note.server"; import { requireUserId } from "~/session.server"; export const loader = async ({ params, request }: LoaderFunctionArgs) => { @@ -22,15 +22,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => { return json({ note }); }; -export const action = async ({ params, request }: ActionFunctionArgs) => { - const userId = await requireUserId(request); - invariant(params.noteId, "noteId not found"); - - await deleteNote({ id: params.noteId, userId }); - - return redirect("/notes"); -}; - export default function NoteDetailsPage() { const data = useLoaderData(); @@ -39,7 +30,7 @@ export default function NoteDetailsPage() {

{data.note.title}

{data.note.body}


-
+
+ + + {!isEdit ? ( + + ) : ( + + )} + + ); +} + +export function ErrorBoundary() { + const error = useRouteError(); + + if (error instanceof Error) { + return
An unexpected error occurred: {error.message}
; + } + + if (!isRouteErrorResponse(error)) { + return

Unknown Error

; + } + + if (error.status === 404) { + return
Note not found
; + } + + return
An unexpected error occurred: {error.statusText}
; +} diff --git a/app/routes/recipes._index.tsx b/app/routes/recipes._index.tsx new file mode 100644 index 0000000..0cce6b9 --- /dev/null +++ b/app/routes/recipes._index.tsx @@ -0,0 +1,14 @@ +import { Form } from "@remix-run/react"; + +export default function RecipesIndexPage() { + return ( +

+ No recipe selected. Select a recipe on the left, or{" "} +

+ +
+

+ ); +} diff --git a/app/routes/recipes.delete.$recipeId.tsx b/app/routes/recipes.delete.$recipeId.tsx new file mode 100644 index 0000000..5ab3979 --- /dev/null +++ b/app/routes/recipes.delete.$recipeId.tsx @@ -0,0 +1,14 @@ +import { ActionFunctionArgs, redirect } from "@remix-run/node"; +import invariant from "tiny-invariant"; + +import { deleteRecipe } from "~/models/recipe.server"; +import { requireUserId } from "~/session.server"; + +export const action = async ({ params, request }: ActionFunctionArgs) => { + const userId = await requireUserId(request); + invariant(params.recipeId, "recipeId not found"); + + await deleteRecipe({ id: params.recipeId, userId }); + + return redirect("/notes"); + }; \ No newline at end of file diff --git a/app/routes/recipes.new.tsx b/app/routes/recipes.new.tsx new file mode 100644 index 0000000..a48c0e1 --- /dev/null +++ b/app/routes/recipes.new.tsx @@ -0,0 +1,26 @@ +import { LoaderFunctionArgs, redirect } from "@remix-run/node"; + +import { createRecipe } from "~/models/recipe.server"; +import { requireUserId } from "~/session.server"; + +export const action = async ({ request }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + // invariant(params.recipeId, "recipeId not found"); + + // const formData = await request.formData(); + // const title = formData.get("title"); + + // if (typeof recipe.title !== "string" || recipe.title.length === 0) { + // return json( + // { errors: { title: "Title is required" } }, + // { status: 400 }, + // ); + // } + + const recipe = await createRecipe({ userId }); + return redirect(`/recipes/${recipe.id}?edit=true`); +}; + +// export default function NewRecipe() { + +// } diff --git a/app/routes/recipes.tsx b/app/routes/recipes.tsx new file mode 100644 index 0000000..e16827e --- /dev/null +++ b/app/routes/recipes.tsx @@ -0,0 +1,73 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react"; + +import { getRecipeListItems } from "~/models/recipe.server"; +import { requireUserId } from "~/session.server"; +import { useUser } from "~/utils"; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const userId = await requireUserId(request); + const recipeListItems = await getRecipeListItems({ userId }); + return json({ recipeListItems: recipeListItems }); +}; + +export default function RecipesPage() { + const data = useLoaderData(); + const user = useUser(); + + return ( +
+
+

+ Recipes +

+ Notes +

{user.email}

+
+ +
+
+ +
+
+
+ +
+ +
+ + {data.recipeListItems.length === 0 ? ( +

No recipes yet

+ ) : ( +
    + {data.recipeListItems.map((recipe) => ( +
  1. + + `block border-b p-4 text-xl ${isActive ? "bg-white" : ""}` + } + to={recipe.id} + > + 📝 {recipe.title} + +
  2. + ))} +
+ )} +
+ +
+ +
+
+
+ ); +} diff --git a/app/utils.ts b/app/utils.ts index f6da6bd..2e25046 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -1,3 +1,4 @@ +import { Recipe } from "@prisma/client"; import { useMatches } from "@remix-run/react"; import { useMemo } from "react"; @@ -74,3 +75,12 @@ export function useUser(): User { export function validateEmail(email: unknown): email is string { return typeof email === "string" && email.length > 3 && email.includes("@"); } + +export function getRecipeFromForm(formData: FormData) { + const values = Object.fromEntries(formData); + + return { + title: values.title, + source: values.source, + } as Recipe; +} \ No newline at end of file diff --git a/prisma/migrations/20231108165715_recipe02/migration.sql b/prisma/migrations/20231108165715_recipe02/migration.sql new file mode 100644 index 0000000..555d462 --- /dev/null +++ b/prisma/migrations/20231108165715_recipe02/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[id,userId]` on the table `Recipe` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Recipe_id_userId_key" ON "Recipe"("id", "userId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4bb8047..143a2c7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,6 +55,7 @@ model Recipe { userId String @@unique([userId, title]) + @@unique([id, userId]) @@index([title, userId]) }