diff --git a/package.json b/package.json index bfa6cd78..f639f9de 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "contentlayer": "^0.3.4", "date-fns": "^2.30.0", "langchain": "^0.0.153", + "lodash": "^4.17.21", "lucide-react": "^0.279.0", "next": "13.5.2", "next-contentlayer": "^0.3.4", @@ -102,6 +103,7 @@ "simplebar-react": "^3.2.4", "sonner": "^1.0.3", "stripe": "^13.7.0", + "superjson": "^2.0.0", "tailwind-merge": "^1.14.0", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", @@ -117,6 +119,7 @@ "@faker-js/faker": "^8.1.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.0", "@types/eslint": "^8.44.3", + "@types/lodash": "^4.14.200", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "eslint": "8.51.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ffedae6..175054cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,6 +167,9 @@ dependencies: langchain: specifier: ^0.0.153 version: 0.0.153(@pinecone-database/pinecone@1.1.0)(@planetscale/database@1.11.0)(pdf-parse@1.1.1) + lodash: + specifier: ^4.17.21 + version: 4.17.21 lucide-react: specifier: ^0.279.0 version: 0.279.0(react@18.2.0) @@ -233,6 +236,9 @@ dependencies: stripe: specifier: ^13.7.0 version: 13.9.0 + superjson: + specifier: ^2.0.0 + version: 2.0.0 tailwind-merge: specifier: ^1.14.0 version: 1.14.0 @@ -265,6 +271,9 @@ devDependencies: '@types/eslint': specifier: ^8.44.3 version: 8.44.3 + '@types/lodash': + specifier: ^4.14.200 + version: 4.14.200 '@typescript-eslint/eslint-plugin': specifier: ^6.7.4 version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.51.0)(typescript@5.2.2) @@ -3322,12 +3331,11 @@ packages: /@types/lodash-es@4.17.9: resolution: {integrity: sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ==} dependencies: - '@types/lodash': 4.14.199 + '@types/lodash': 4.14.200 dev: false - /@types/lodash@4.14.199: - resolution: {integrity: sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==} - dev: false + /@types/lodash@4.14.200: + resolution: {integrity: sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==} /@types/mdast@3.0.13: resolution: {integrity: sha512-HjiGiWedR0DVFkeNljpa6Lv4/IZU1+30VY5d747K7lBudFc3R0Ibr6yJ9lN3BE28VnZyDfLF/VB1Ql1ZIbKrmg==} @@ -3656,7 +3664,7 @@ packages: '@vue/shared': 3.3.4 estree-walker: 2.0.2 magic-string: 0.30.4 - postcss: 8.4.30 + postcss: 8.4.31 source-map-js: 1.0.2 dev: false @@ -4362,6 +4370,13 @@ packages: engines: {node: '>= 0.6'} dev: false + /copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.15 + dev: false + /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false @@ -6031,6 +6046,11 @@ packages: get-intrinsic: 1.2.1 dev: true + /is-what@4.1.15: + resolution: {integrity: sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==} + engines: {node: '>=12.13'} + dev: false + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true @@ -8048,15 +8068,6 @@ packages: source-map-js: 1.0.2 dev: false - /postcss@8.4.30: - resolution: {integrity: sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false - /postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -9125,6 +9136,13 @@ packages: pirates: 4.0.6 ts-interface-checker: 0.1.13 + /superjson@2.0.0: + resolution: {integrity: sha512-W3n+NJ7TFjaLle8ihIIvsr/bbuKpnxeatsyjmhy7iSkom+/cshaHziCQAWXrHGWJVQSQFDOuES6C3nSEvcbrQg==} + engines: {node: '>=16'} + dependencies: + copy-anything: 3.0.5 + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 421509f8..4a90852c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,12 +15,12 @@ datasource db { model Recipe { id String @id @default(cuid()) title String - description String? @db.LongText + description String @db.LongText isPublic Boolean @default(false) userId String - timeInKitchen Int? - waitingTime Int? - numberOfPeople Int? + timeInKitchen Int + waitingTime Int + numberOfPeople Int ratings RecipeRating[] ingredients IngredientsOnRecipes[] steps Step[] @@ -40,7 +40,7 @@ model RecipeRating { model Ingredient { id String @id @default(cuid()) name String - unit String? + unit String userId String recipes IngredientsOnRecipes[] } @@ -52,12 +52,10 @@ model IngredientsOnRecipes { recipeId String quantity Float assignedAt DateTime @default(now()) - assignedBy String? @@id([ingredientId, recipeId]) @@index([ingredientId]) @@index([recipeId]) - @@index([assignedBy]) } model Step { diff --git a/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/loading.tsx b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/loading.tsx new file mode 100644 index 00000000..91bf8960 --- /dev/null +++ b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/loading.tsx @@ -0,0 +1,5 @@ +import { RecipeSkeleton } from '@/components/skeleton/RecipeSkeleton'; + +export default function RecipeLoading() { + return ; +} diff --git a/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/not-found.tsx b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/not-found.tsx new file mode 100644 index 00000000..ccd7f8af --- /dev/null +++ b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/not-found.tsx @@ -0,0 +1,15 @@ +import { ErrorCard } from '@/components/cards/error-card'; +import { Shell } from '@/components/shells/shell'; + +export default function RecipeNotFound() { + return ( + + + + ); +} diff --git a/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/page.tsx b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/page.tsx new file mode 100644 index 00000000..517afe36 --- /dev/null +++ b/src/app/(dashboard)/dashboard/recipe/[recipeId]/edit/page.tsx @@ -0,0 +1,68 @@ +import { serverClient } from '@/app/_trpc/serverClient'; +import { EditRecipeForm } from '@/components/forms/recipe/UpdateRecipeForm'; +import { Breadcrumbs } from '@/components/pagers/breadcrumbs'; +import { Shell } from '@/components/shells/shell'; +import { toTitleCase } from '@/lib/utils'; +import { currentUser } from '@clerk/nextjs/server'; +import { notFound } from 'next/navigation'; + +interface ProductPageProps { + params: { + recipeId: string; + }; +} + +export async function generateMetadata({ params }: ProductPageProps) { + if (!params.recipeId) { + return {}; + } + + const recipe = await serverClient.recipe + .getRecipe({ id: params.recipeId }) + .catch(() => null); + + if (!recipe) { + return {}; + } + + return { + title: toTitleCase(recipe.title), + description: recipe.description ?? undefined, + }; +} + +export default async function ProductPage({ + params, +}: Readonly) { + if (!params.recipeId) { + notFound(); + } + + const user = await currentUser(); + + const recipe = await serverClient.recipe + .getRecipe({ id: params.recipeId }) + .catch(() => null); + + if (!recipe || !user || recipe.userId !== user.id) { + notFound(); + } + + return ( + + + + + ); +} diff --git a/src/app/(dashboard)/dashboard/recipes/new/page.tsx b/src/app/(dashboard)/dashboard/recipes/new/page.tsx index 9ac91efb..99837d7b 100644 --- a/src/app/(dashboard)/dashboard/recipes/new/page.tsx +++ b/src/app/(dashboard)/dashboard/recipes/new/page.tsx @@ -1,4 +1,4 @@ -import { AddRecipeForm } from '@/components/forms/add-recipe-form'; +import { AddRecipeForm } from '@/components/forms/recipe/AddRecipeForm'; import { PageHeader, PageHeaderDescription, diff --git a/src/app/actions.ts b/src/app/actions.ts index 4b69637e..1512c361 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -9,11 +9,19 @@ export async function createRecipeRevalidate() { revalidatePath('/dashboard/recipes'); } +export async function updateRecipeRevalidate(id: string) { + revalidatePath('/recipes'); + revalidatePath(`/recipe/${id}`); + revalidatePath(`/dashboard/recipes`); + revalidatePath(`/dashboard/recipe/${id}`); + revalidatePath(`/dashboard/recipe/${id}/edit`); +} + export async function deleteRecipeRevalidate(id: string) { revalidatePath('/recipes'); - revalidatePath(`/recipes/${id}`); + revalidatePath(`/recipe/${id}`); revalidatePath(`/dashboard/recipes`); - revalidatePath(`/dashboard/recipes/${id}`); + revalidatePath(`/dashboard/recipe/${id}`); } // For development diff --git a/src/components/forms/add-recipe-form.tsx b/src/components/forms/add-recipe-form.tsx index abee8830..946c5f39 100644 --- a/src/components/forms/add-recipe-form.tsx +++ b/src/components/forms/add-recipe-form.tsx @@ -25,7 +25,7 @@ import { toast } from 'sonner'; import type { z } from 'zod'; import { Separator } from '../ui/separator'; -type Inputs = z.infer; +export type RecipeFormInput = z.infer; export function AddRecipeForm() { const router = useRouter(); @@ -34,21 +34,21 @@ export function AddRecipeForm() { const createRecipe = trpc.recipe.createRecipe.useMutation(); // react-hook-form - const form = useForm({ + const form = useForm({ resolver: zodResolver(createRecipeInput), defaultValues: { title: '', description: '', - ingredients: [{ name: '', quantity: '1', unit: 'g' }], - steps: [{ step: '' }], - timeInKitchen: '15', - waitingTime: '30', - numberOfPeople: '2', + ingredients: [{ ingredient: { name: '', unit: 'g' }, quantity: 1 }], + steps: [{ content: '' }], + timeInKitchen: 15, + waitingTime: 30, + numberOfPeople: 2, }, mode: 'onTouched', }); - function onSubmit(data: Inputs) { + function onSubmit(data: RecipeFormInput) { startTransition(async () => { try { await createRecipe.mutateAsync(data); @@ -156,7 +156,7 @@ export function AddRecipeForm() { > ( Ingredient Name @@ -189,7 +189,7 @@ export function AddRecipeForm() { /> ( Unit @@ -205,7 +205,7 @@ export function AddRecipeForm() { onClick={(event) => { event.preventDefault(); remove(index); - form.setFocus(`ingredients.${index - 1}.name`); + form.setFocus(`ingredients.${index - 1}.ingredient.name`); }} disabled={fields.length === 1} > @@ -217,7 +217,7 @@ export function AddRecipeForm() { className="mx-auto w-fit bg-cyan-600/90 hover:bg-cyan-600" onClick={(event) => { event.preventDefault(); - append({ name: '', quantity: '1', unit: 'g' }); + append({ ingredient: { name: '', unit: 'g' }, quantity: 1 }); }} >