From 6ad3e687cc31d3ef97336203d35cb3c96667e7ed Mon Sep 17 00:00:00 2001 From: Joar Hansson <124060119+JoarHansson@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:20:26 +0100 Subject: [PATCH] feat: add error and success messages on edit page (#227) * add notifications for loading, success and error replace the error handling with useStates for error and success and conditional Alert component. * put notification handling in a separate file - less code is needed in the components to use notifications for loading/success/error. - notifications will be consistent across the app and style/settings can be easily changed in one place. - a tsx file was needed to have the Icon component in the notifications. -> src/lib/notifications.tsx * refactor layout to have buttons up top and sticky * edit some spacing * remove DeleteRecipeButton component its now handled inline in the EditRecipeForm component * remove DeleteRecipeButton and title there now rendered in the EditRecipeForm component instead * add buttons with text for larger screens and change title placement + content ({recipe.title} instead of "Edit Recipe") * refactor sticky element slightly - zIndex:1000 made it appear on top of dropdowns/drawers -> zIndex:100 - small edit of margins * integrate modals for recipe deletion confirmation * increase notification auto-close duration to 2s * fix singular/plural typo --------- Co-authored-by: sirisayshello --- package-lock.json | 252 ++++++++++++++++++++----- package.json | 8 +- src/app/dashboard/[slug]/edit/page.tsx | 7 +- src/app/dashboard/page.tsx | 3 +- src/app/layout.tsx | 15 +- src/components/DeleteRecipeButton.tsx | 26 --- src/components/EditRecipeForm.tsx | 208 ++++++++++++++------ src/components/RecipeForm.tsx | 61 ++---- src/components/RecipesList.tsx | 13 +- src/lib/notifications.tsx | 43 +++++ 10 files changed, 447 insertions(+), 189 deletions(-) delete mode 100644 src/components/DeleteRecipeButton.tsx create mode 100644 src/lib/notifications.tsx diff --git a/package-lock.json b/package-lock.json index 6d59bb1..d8d7e68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,11 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^17.0.0", - "@mantine/core": "^7.13.4", + "@mantine/core": "^7.14.1", "@mantine/form": "^7.13.5", - "@mantine/hooks": "^7.13.3", - "@mantine/notifications": "^7.13.4", + "@mantine/hooks": "^7.14.1", + "@mantine/modals": "^7.14.1", + "@mantine/notifications": "^7.14.1", "@prisma/client": "^5.21.0", "@tabler/icons-react": "^3.20.0", "bcrypt": "^5.1.1", @@ -40,6 +41,7 @@ "eslint-config-prettier": "^9.1.0", "lint-staged": "^15.2.10", "postcss": "^8.4.47", + "postcss-preset-mantine": "^1.17.0", "prisma": "^5.21.0", "ts-node": "^10.9.2", "tsx": "^4.19.1", @@ -478,9 +480,9 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz", - "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==", + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.6.0", @@ -488,9 +490,9 @@ } }, "node_modules/@floating-ui/react": { - "version": "0.26.25", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.25.tgz", - "integrity": "sha512-hZOmgN0NTOzOuZxI1oIrDu3Gcl8WViIkvPMpB4xdd4QD6xAMtwgwr3VPoiyH/bLtRcS1cDnhxLSD1NsMJmwh/A==", + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.1.2", @@ -673,22 +675,22 @@ } }, "node_modules/@mantine/core": { - "version": "7.13.4", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.13.4.tgz", - "integrity": "sha512-9I6+SqTq90pnI3WPmOQzQ1PL7IkhQg/5ft8Awhgut8tvk1VaKruDm/K5ysUG3ncHrP+QTI2UHYjNlUrux6HKlw==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.14.1.tgz", + "integrity": "sha512-oHqaOE1n4KJkvJgF628OCVXE2zUgkEotEsPUdVaC58qRfJ7SvZAI26JNbUG8+MoqHHEqHKtBaRkTyuEVMbomxw==", "license": "MIT", "dependencies": { - "@floating-ui/react": "^0.26.9", + "@floating-ui/react": "^0.26.27", "clsx": "^2.1.1", - "react-number-format": "^5.3.1", - "react-remove-scroll": "^2.5.7", - "react-textarea-autosize": "8.5.3", - "type-fest": "^4.12.0" + "react-number-format": "^5.4.2", + "react-remove-scroll": "^2.6.0", + "react-textarea-autosize": "8.5.4", + "type-fest": "^4.26.1" }, "peerDependencies": { - "@mantine/hooks": "7.13.4", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@mantine/hooks": "7.14.1", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/form": { @@ -704,37 +706,49 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.13.4", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.13.4.tgz", - "integrity": "sha512-B2QCegQyWlLdenVNaLNK8H9cTAjLW9JKJ3xWg+ShhpjZDHT2hjZz4L0Nt071Z7mPvyAaOwKGM0FyqTcTjdECfg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.14.1.tgz", + "integrity": "sha512-VlgTyV/9WNFCwCshW1KHMYNzLt+M8aG68E1lWaqOXtyWSLJo+X5zQJGg0f8bwGbJvIMQCpQd0yTLfnjD6uAtrA==", "license": "MIT", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/modals": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.14.1.tgz", + "integrity": "sha512-jpcRS5fzD+CPRJ7mebA9lt0bmSI+2lJVZloG7SPIY3S66ZtUsEis0fHkDtuAqGANbhr43Enjhno0M+4x9IzuZw==", + "license": "MIT", + "peerDependencies": { + "@mantine/core": "7.14.1", + "@mantine/hooks": "7.14.1", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/notifications": { - "version": "7.13.4", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.13.4.tgz", - "integrity": "sha512-CKd3tDGDAegkJYJIMHtF0St4hBpBVAujdmtsEin7UYeVq5N0YYe7j2T1Xu7Ry6dfObkuxeig6csxiJyBrZ2bew==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.14.1.tgz", + "integrity": "sha512-08suBIh/EJuTnzF1/Aao73S534KXvD7MiEaRNPXG+vBFz57Lu4DOtyLG4mXju6eNK99KJziVlK7CMIv6ADcQNg==", "license": "MIT", "dependencies": { - "@mantine/store": "7.13.4", + "@mantine/store": "7.14.1", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.13.4", - "@mantine/hooks": "7.13.4", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@mantine/core": "7.14.1", + "@mantine/hooks": "7.14.1", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/store": { - "version": "7.13.4", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.13.4.tgz", - "integrity": "sha512-DUlnXizE7aCjbVg2J3XLLKsOzt2c2qfQl2Xmx9l/BPE4FFZZKUqGDkYaTDbTAmnN3FVZ9xXycL7bAlq9udO8mA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.14.1.tgz", + "integrity": "sha512-wpemDaqOJc1zsvnjaic1+KRQSy7dZhQ4XDwxqqq5MwG6aImCHqEBVf17Qhj3sDjpA7pnpxnKAHotLqfzjQn3dQ==", "license": "MIT", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.x || ^19.x" } }, "node_modules/@mapbox/node-pre-gyp": { @@ -1946,6 +1960,16 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001669", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", @@ -2211,6 +2235,19 @@ "tiny-invariant": "^1.0.6" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5774,6 +5811,120 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-mixins": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-9.0.4.tgz", + "integrity": "sha512-XVq5jwQJDRu5M1XGkdpgASqLk37OqkH4JCFDXl/Dn7janOJjCTEKL+36cnRVy7bMtoBzALfO7bV7nTIsFnUWLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "postcss-js": "^4.0.0", + "postcss-simple-vars": "^7.0.0", + "sugarss": "^4.0.1" + }, + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-preset-mantine": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.17.0.tgz", + "integrity": "sha512-ji1PMDBUf2Vsx/HE5faMSs1+ff6qE6YRulTr4Ja+6HD3gop8rSMTCYdpN7KrdsEg079kfBKkO/PaKhG9uR0zwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-mixins": "^9.0.4", + "postcss-nested": "^6.0.1" + }, + "peerDependencies": { + "postcss": ">=8.0.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-simple-vars": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz", + "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.1" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -6020,9 +6171,9 @@ } }, "node_modules/react-textarea-autosize": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", - "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.4.tgz", + "integrity": "sha512-eSSjVtRLcLfFwFcariT77t9hcbVJHQV76b51QjQGarQIHml2+gM2lms0n3XrhnDmgK5B+/Z7TmQk5OHNzqYm/A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.13", @@ -6789,6 +6940,23 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, + "node_modules/sugarss": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", + "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -7015,9 +7183,9 @@ } }, "node_modules/type-fest": { - "version": "4.26.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", - "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.27.0.tgz", + "integrity": "sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" diff --git a/package.json b/package.json index 589dd71..8104ade 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,11 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^17.0.0", - "@mantine/core": "^7.13.4", + "@mantine/core": "^7.14.1", "@mantine/form": "^7.13.5", - "@mantine/hooks": "^7.13.3", - "@mantine/notifications": "^7.13.4", + "@mantine/hooks": "^7.14.1", + "@mantine/modals": "^7.14.1", + "@mantine/notifications": "^7.14.1", "@prisma/client": "^5.21.0", "@tabler/icons-react": "^3.20.0", "bcrypt": "^5.1.1", @@ -43,6 +44,7 @@ "eslint-config-prettier": "^9.1.0", "lint-staged": "^15.2.10", "postcss": "^8.4.47", + "postcss-preset-mantine": "^1.17.0", "prisma": "^5.21.0", "ts-node": "^10.9.2", "tsx": "^4.19.1", diff --git a/src/app/dashboard/[slug]/edit/page.tsx b/src/app/dashboard/[slug]/edit/page.tsx index fab5248..e0b865d 100644 --- a/src/app/dashboard/[slug]/edit/page.tsx +++ b/src/app/dashboard/[slug]/edit/page.tsx @@ -1,10 +1,9 @@ + import { Breadcrumbs } from "@/components/Breadcrumbs"; -import DeleteRecipeButton from "@/components/DeleteRecipeButton"; import { EditRecipeForm } from "@/components/EditRecipeForm"; import { getAuth } from "@/lib/auth"; import prisma from "@/lib/db"; import { getUserTags } from "@/lib/queries"; -import { Group, Title } from "@mantine/core"; import { notFound } from "next/navigation"; export default async function EditRecipePage({ @@ -36,10 +35,6 @@ export default async function EditRecipePage({ return ( <> - - Edit recipe - - diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f33bec4..e5d110d 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -26,7 +26,8 @@ export default async function Dashboard() { Your recipes - You have {numberOfRecipes} delicious recipes in your collection + You have {numberOfRecipes} delicious{" "} + {numberOfRecipes === 1 ? "recipe" : "recipes"} in your collection diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f65093f..910e801 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { theme } from "./theme"; import SessionWrapper from "@/components/SessionWrapper"; import { Notifications } from "@mantine/notifications"; import "@mantine/notifications/styles.css"; +import { ModalsProvider } from "@mantine/modals"; export const metadata: Metadata = { title: "Recipe Declutter", @@ -27,12 +28,14 @@ export default function RootLayout({ - - - - - {/* 56px current height of navbar. Update if it changes */} - {children} + + + + + + {/* 56px current height of navbar. Update if it changes */} + {children} + diff --git a/src/components/DeleteRecipeButton.tsx b/src/components/DeleteRecipeButton.tsx deleted file mode 100644 index 9df7452..0000000 --- a/src/components/DeleteRecipeButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { deleteRecipeById } from "@/lib/queries"; -import { useRouter } from "next/navigation"; -import { IconTrash } from "@tabler/icons-react"; -import { ActionIcon } from "@mantine/core"; - -export default function DeleteRecipeButton({ id }: { id: number }) { - const router = useRouter(); - - const handleClick = () => { - deleteRecipeById(id); - router.push("/dashboard"); - // add a confirmation/toast message on the dashboard when successful - }; - - return ( - - - - ); -} diff --git a/src/components/EditRecipeForm.tsx b/src/components/EditRecipeForm.tsx index b589550..6d56132 100644 --- a/src/components/EditRecipeForm.tsx +++ b/src/components/EditRecipeForm.tsx @@ -1,21 +1,36 @@ "use client"; -import { updateRecipe } from "@/lib/queries"; +import { deleteRecipeById, updateRecipe } from "@/lib/queries"; import { isSectionedInstruction, isSimpleInstruction } from "@/lib/utils"; import { + ActionIcon, Button, Fieldset, + Flex, Group, + Paper, Space, TagsInput, + Text, TextInput, - Alert, - Paper, + Title, } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { EditIngredientAndInstructionList } from "./EditIngredientsAndInstructionsLists"; +import Link from "next/link"; +import { + showLoadingNotification, + updateNotificationAsError, + updateNotificationAsSuccess, +} from "@/lib/notifications"; +import { + IconChevronLeft, + IconDeviceFloppy, + IconTrash, +} from "@tabler/icons-react"; +import { modals } from "@mantine/modals"; type EditRecipeProps = { recipe: UserRecipe; @@ -29,8 +44,6 @@ export const EditRecipeForm = ({ recipe, userTags }: EditRecipeProps) => { const allUserTags = userTags?.map((tag) => tag.name); const existingTags = recipe.tags?.map((tag) => tag.tag.name); const [tags, setTags] = useState(existingTags || []); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); const router = useRouter(); // Check if the instructions are sectioned or simple @@ -58,6 +71,11 @@ export const EditRecipeForm = ({ recipe, userTags }: EditRecipeProps) => { // Function to handle form submission async function handleSubmit(values: FormValues) { + // Notification for each form submit. Initially as a loading notification. + const loadingNotification = showLoadingNotification( + "Saving your changes..." + ); + if (!form.validate().hasErrors) { const updatedRecipe = { ...recipe, @@ -82,38 +100,161 @@ export const EditRecipeForm = ({ recipe, userTags }: EditRecipeProps) => { try { const result = await updateRecipe(updatedRecipe); - if (!result.success) + if (!result.success) { throw new Error(result.error?.message || "Failed to save recipe"); + } - setSuccess(true); - console.log(success); - setTimeout( - () => router.push(`/dashboard/${recipe.slug}?id=${recipe.id}`), - 1500 + // Update notification to show success message + updateNotificationAsSuccess( + loadingNotification, + "Recipe successfully updated." ); + + router.refresh(); } catch (err) { console.error(err); - setError("An error occurred while saving. Please try again."); + + // Update notification to show an error message + updateNotificationAsError( + loadingNotification, + "An error occurred while saving. Please try again." + ); } } } + const openDeleteModal = () => + modals.openConfirmModal({ + title: ( + + Delete Recipe + + ), + centered: true, + children: ( + Are you sure you want to delete this recipe? + ), + labels: { confirm: "Delete it", cancel: "Keep it" }, + confirmProps: { color: "red" }, + onCancel: () => console.log("Cancel"), + onConfirm: () => { + console.log(recipe); + + const test = showLoadingNotification("deleting recipe..."); + + deleteRecipeById(recipe.id as number); + + updateNotificationAsSuccess(test, "Deleted recipe successfully"); + + router.push("/dashboard"); + router.refresh(); + }, + }); + return ( <>
+ + + + + + + + + + + + + openDeleteModal()} + size={"lg"} + > + + + + + + + + {recipe.title} + + {/* General info */} +
+ { />
- - - - - {error && ( - - {error} - - )} - - - - - - - + ); diff --git a/src/components/RecipeForm.tsx b/src/components/RecipeForm.tsx index 8ec98cd..15f1ba9 100644 --- a/src/components/RecipeForm.tsx +++ b/src/components/RecipeForm.tsx @@ -8,20 +8,24 @@ import { Button, Box, Title, - rem, Divider, Alert, Stack, Transition, } from "@mantine/core"; import { useField } from "@mantine/form"; -import { notifications, useNotifications } from "@mantine/notifications"; +import { useNotifications } from "@mantine/notifications"; import { IngredientsAndInstructionsToggle } from "./IngredientsAndInstructionsToggle"; import { SaveRecipeModal } from "./SaveRecipeModal"; import { Session } from "next-auth"; -import { IconCheck, IconChefHat, IconX } from "@tabler/icons-react"; +import { IconChefHat } from "@tabler/icons-react"; import Link from "next/link"; +import { + showLoadingNotification, + updateNotificationAsError, + updateNotificationAsSuccess, +} from "@/lib/notifications"; import { ScreenAwakeToggle } from "./ScreenAwakeToggle"; type RecipeFormProps = { @@ -86,15 +90,9 @@ export const RecipeForm = ({ session, userTags }: RecipeFormProps) => { setRecipe(undefined); // Notification for each form submit. Initially as a loading notification. - const loadingNotification = notifications.show({ - loading: true, - title: "Just a moment", - message: "Fetching recipe from URL", - autoClose: false, - withCloseButton: false, - withBorder: true, - px: "lg", - }); + const loadingNotification = showLoadingNotification( + "Fetching recipe from URL" + ); try { const url = field.getValue(); @@ -107,46 +105,19 @@ export const RecipeForm = ({ session, userTags }: RecipeFormProps) => { setRecipe({ ...data.recipe, url }); // Update notification to show success message - notifications.update({ - id: loadingNotification, - loading: false, - autoClose: 1000, // show success for 1 second - withCloseButton: true, - closeButtonProps: { "aria-label": "Hide notification" }, - color: "teal", - title: "Success!", - message: "Recipe successfully fetched.", - icon: , - }); + updateNotificationAsSuccess( + loadingNotification, + "Recipe successfully fetched." + ); } else if (data.error) { console.error("Error:", data.error); // Update notification to show error message - notifications.update({ - id: loadingNotification, - loading: false, - autoClose: 5000, // show error for 5 seconds - withCloseButton: true, - closeButtonProps: { "aria-label": "Hide notification" }, - color: "red", - title: "Oh no!", - message: data.error.message, - icon: , - }); + updateNotificationAsError(loadingNotification, data.error.message); } } catch (error) { // Update notification to show error message - notifications.update({ - id: loadingNotification, - loading: false, - autoClose: 5000, // show error for 5 seconds - withCloseButton: true, - closeButtonProps: { "aria-label": "Hide notification" }, - color: "red", - title: "Oh no!", - message: "Failed to fetch recipe", - icon: , - }); + updateNotificationAsError(loadingNotification, "Failed to fetch recipe"); if (error instanceof Error) { console.error(error); diff --git a/src/components/RecipesList.tsx b/src/components/RecipesList.tsx index c132696..b98b46c 100644 --- a/src/components/RecipesList.tsx +++ b/src/components/RecipesList.tsx @@ -48,12 +48,6 @@ export default function RecipesList({ recipes, title, tags }: RecipeListProps) { onTagsChange={setFilteredTags} /> )} - {!recipes && ( -

- Ready to start your collection? Click the + button to add your - first recipe! -

- )} )} + + {recipes.length < 1 && ( +

+ Ready to start your collection? Click "Add New Recipe" to + add your first recipe! +

+ )} ); } diff --git a/src/lib/notifications.tsx b/src/lib/notifications.tsx new file mode 100644 index 0000000..a3b1263 --- /dev/null +++ b/src/lib/notifications.tsx @@ -0,0 +1,43 @@ +import { notifications } from "@mantine/notifications"; +import { IconCheck, IconX } from "@tabler/icons-react"; +import { rem } from "@mantine/core"; + +export function showLoadingNotification(message: string) { + return notifications.show({ + loading: true, + title: "Just a moment", + message, + autoClose: false, + withCloseButton: false, + withBorder: true, + px: "lg", + }); +} + +export function updateNotificationAsError(id: string, message: string) { + notifications.update({ + id, + loading: false, + autoClose: 5000, // show errors for 5 seconds + withCloseButton: true, + closeButtonProps: { "aria-label": "Hide notification" }, + color: "red", + title: "Oh no!", + message, + icon: , + }); +} + +export function updateNotificationAsSuccess(id: string, message: string) { + notifications.update({ + id, + loading: false, + autoClose: 2000, // show success for 2 seconds + withCloseButton: true, + closeButtonProps: { "aria-label": "Hide notification" }, + color: "teal", + title: "Success!", + message, + icon: , + }); +}