diff --git a/api/db/migrations/20231031125421_user_role/migration.sql b/api/db/migrations/20231031125421_user_role/migration.sql deleted file mode 100644 index 676758b..0000000 --- a/api/db/migrations/20231031125421_user_role/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ --- CreateTable -CREATE TABLE "UserRole" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "name" TEXT NOT NULL, - "userId" INTEGER, - CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "UserRole_name_userId_key" ON "UserRole"("name", "userId"); diff --git a/api/db/migrations/20231031145219_separate_name/migration.sql b/api/db/migrations/20231031145219_separate_name/migration.sql deleted file mode 100644 index 4326da6..0000000 --- a/api/db/migrations/20231031145219_separate_name/migration.sql +++ /dev/null @@ -1,26 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `name` on the `User` table. All the data in the column will be lost. - - Added the required column `firstName` to the `User` table without a default value. This is not possible if the table is not empty. - - Added the required column `lastName` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- RedefineTables -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "email" TEXT NOT NULL, - "hashedPassword" TEXT NOT NULL, - "salt" TEXT NOT NULL, - "resetToken" TEXT, - "resetTokenExpiresAt" DATETIME -); -INSERT INTO "new_User" ("email", "hashedPassword", "id", "resetToken", "resetTokenExpiresAt", "salt") SELECT "email", "hashedPassword", "id", "resetToken", "resetTokenExpiresAt", "salt" FROM "User"; -DROP TABLE "User"; -ALTER TABLE "new_User" RENAME TO "User"; -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -PRAGMA foreign_key_check; -PRAGMA foreign_keys=ON; diff --git a/api/db/migrations/20231101015820_simplify_roles/migration.sql b/api/db/migrations/20231101015820_simplify_roles/migration.sql deleted file mode 100644 index 0aa7f8b..0000000 --- a/api/db/migrations/20231101015820_simplify_roles/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ -/* - Warnings: - - - You are about to drop the `UserRole` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropIndex -DROP INDEX "UserRole_name_userId_key"; - --- DropTable -PRAGMA foreign_keys=off; -DROP TABLE "UserRole"; -PRAGMA foreign_keys=on; - --- RedefineTables -PRAGMA foreign_keys=OFF; -CREATE TABLE "new_User" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "firstName" TEXT NOT NULL, - "lastName" TEXT NOT NULL, - "email" TEXT NOT NULL, - "hashedPassword" TEXT NOT NULL, - "salt" TEXT NOT NULL, - "resetToken" TEXT, - "resetTokenExpiresAt" DATETIME, - "roles" TEXT NOT NULL DEFAULT 'user' -); -INSERT INTO "new_User" ("email", "firstName", "hashedPassword", "id", "lastName", "resetToken", "resetTokenExpiresAt", "salt") SELECT "email", "firstName", "hashedPassword", "id", "lastName", "resetToken", "resetTokenExpiresAt", "salt" FROM "User"; -DROP TABLE "User"; -ALTER TABLE "new_User" RENAME TO "User"; -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); -PRAGMA foreign_key_check; -PRAGMA foreign_keys=ON; diff --git a/api/db/migrations/20231031125027_user_and_part/migration.sql b/api/db/migrations/20231107030305_y/migration.sql similarity index 81% rename from api/db/migrations/20231031125027_user_and_part/migration.sql rename to api/db/migrations/20231107030305_y/migration.sql index 8a2a681..f0b1cea 100644 --- a/api/db/migrations/20231031125027_user_and_part/migration.sql +++ b/api/db/migrations/20231107030305_y/migration.sql @@ -11,12 +11,14 @@ CREATE TABLE "Part" ( -- CreateTable CREATE TABLE "User" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "name" TEXT, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, "email" TEXT NOT NULL, "hashedPassword" TEXT NOT NULL, "salt" TEXT NOT NULL, "resetToken" TEXT, - "resetTokenExpiresAt" DATETIME + "resetTokenExpiresAt" DATETIME, + "roles" TEXT NOT NULL DEFAULT 'user' ); -- CreateIndex diff --git a/api/src/graphql/parts.sdl.ts b/api/src/graphql/parts.sdl.ts index 3e5334a..52869fc 100644 --- a/api/src/graphql/parts.sdl.ts +++ b/api/src/graphql/parts.sdl.ts @@ -51,7 +51,8 @@ export const schema = gql` type Mutation { createPart(input: CreatePartInput!): Part! @requireAuth - updatePart(id: Int!, input: UpdatePartInput!): Part! @requireAuth - deletePart(id: Int!): Part! @requireAuth + updatePart(id: Int!, input: UpdatePartInput!): Part! + @requireAuth(roles: "admin") + deletePart(id: Int!): Part! @requireAuth(roles: "admin") } ` diff --git a/web/config/tailwind.config.js b/web/config/tailwind.config.js index d027457..cfb79ad 100644 --- a/web/config/tailwind.config.js +++ b/web/config/tailwind.config.js @@ -13,7 +13,7 @@ export const theme = { }, }, } -export const plugins = [require('daisyui')] +export const plugins = [require('daisyui'), require('tailwindcss-animate')] export const daisyui = { themes: ['light', 'dark'], } diff --git a/web/package.json b/web/package.json index c2a7286..b495dbd 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "daisyui": "^3.9.3", "postcss": "^8.4.31", "postcss-loader": "^7.3.3", - "tailwindcss": "^3.3.3" + "tailwindcss": "^3.3.3", + "tailwindcss-animate": "^1.0.7" } } diff --git a/web/public/favicon.png b/web/public/favicon.png index 4741429..d53b82f 100644 Binary files a/web/public/favicon.png and b/web/public/favicon.png differ diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index 0c34cb6..6b41579 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -25,6 +25,7 @@ const Routes = () => { + diff --git a/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx index c1571f5..b383196 100644 --- a/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx +++ b/web/src/components/NavbarAccountIcon/NavbarAccountIcon.tsx @@ -11,7 +11,7 @@ interface Props { } const NavbarAccountIcon = ({ mobile, className }: Props) => { - const { isAuthenticated, currentUser, logOut, hasRole } = useAuth() + const { isAuthenticated, currentUser, logOut } = useAuth() return isAuthenticated ? (
@@ -21,12 +21,7 @@ const NavbarAccountIcon = ({ mobile, className }: Props) => { }`} > - +

diff --git a/web/src/components/Part/EditPartCell/EditPartCell.tsx b/web/src/components/Part/EditPartCell/EditPartCell.tsx index ee118ef..d52fe0c 100644 --- a/web/src/components/Part/EditPartCell/EditPartCell.tsx +++ b/web/src/components/Part/EditPartCell/EditPartCell.tsx @@ -6,6 +6,7 @@ import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import PartForm from 'src/components/Part/PartForm' +import ToastNotification from 'src/components/ToastNotification' export const QUERY = gql` query EditPartById($id: Int!) { @@ -41,11 +42,15 @@ export const Failure = ({ error }: CellFailureProps) => ( export const Success = ({ part }: CellSuccessProps) => { const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, { onCompleted: () => { - toast.success('Part updated') + toast.custom((t) => ( + + )) navigate(routes.parts()) }, onError: (error) => { - toast.error(error.message) + toast.custom((t) => ( + + )) }, }) diff --git a/web/src/components/Part/NewPart/NewPart.tsx b/web/src/components/Part/NewPart/NewPart.tsx index 9f37781..a2b8d91 100644 --- a/web/src/components/Part/NewPart/NewPart.tsx +++ b/web/src/components/Part/NewPart/NewPart.tsx @@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import PartForm from 'src/components/Part/PartForm' +import ToastNotification from 'src/components/ToastNotification' const CREATE_PART_MUTATION = gql` mutation CreatePartMutation($input: CreatePartInput!) { @@ -17,11 +18,15 @@ const CREATE_PART_MUTATION = gql` const NewPart = () => { const [createPart, { loading, error }] = useMutation(CREATE_PART_MUTATION, { onCompleted: () => { - toast.success('Part created') + toast.custom((t) => ( + + )) navigate(routes.parts()) }, onError: (error) => { - toast.error(error.message) + toast.custom((t) => ( + + )) }, }) diff --git a/web/src/components/Part/Part/Part.tsx b/web/src/components/Part/Part/Part.tsx index 2f3456c..43d5308 100644 --- a/web/src/components/Part/Part/Part.tsx +++ b/web/src/components/Part/Part/Part.tsx @@ -4,7 +4,8 @@ import { Link, routes, navigate } from '@redwoodjs/router' import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' -import { timeTag } from 'src/pages/lib/formatters' +import ToastNotification from 'src/components/ToastNotification' +import { timeTag } from 'src/lib/formatters' const DELETE_PART_MUTATION = gql` mutation DeletePartMutation($id: Int!) { @@ -21,11 +22,15 @@ interface Props { const Part = ({ part }: Props) => { const [deletePart] = useMutation(DELETE_PART_MUTATION, { onCompleted: () => { - toast.success('Part deleted') + toast.custom((t) => ( + + )) navigate(routes.parts()) }, onError: (error) => { - toast.error(error.message) + toast.custom((t) => ( + + )) }, }) diff --git a/web/src/components/Part/Parts/Parts.tsx b/web/src/components/Part/Parts/Parts.tsx index db6d2ba..81101d7 100644 --- a/web/src/components/Part/Parts/Parts.tsx +++ b/web/src/components/Part/Parts/Parts.tsx @@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import { QUERY } from 'src/components/Part/PartsCell' +import ToastNotification from 'src/components/ToastNotification' import { timeTag, truncate } from 'src/lib/formatters' const DELETE_PART_MUTATION = gql` @@ -18,13 +19,17 @@ const DELETE_PART_MUTATION = gql` const PartsList = ({ parts }: FindParts) => { const [deletePart] = useMutation(DELETE_PART_MUTATION, { onCompleted: () => { - toast.success('Part deleted') + toast.custom((t) => ( + + )) }, onError: (error) => { - toast.error(error.message) + toast.custom((t) => ( + + )) }, // This refetches the query on the list page. Read more about other ways to - // update the cache over here: + // update the cache over here:ya // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates refetchQueries: [{ query: QUERY }], awaitRefetchQueries: true, @@ -44,7 +49,7 @@ const PartsList = ({ parts }: FindParts) => { } return ( -

+
diff --git a/web/src/components/PartDetailsCell/PartDetailsCell.tsx b/web/src/components/PartDetailsCell/PartDetailsCell.tsx index aa43eb8..35a4b7b 100644 --- a/web/src/components/PartDetailsCell/PartDetailsCell.tsx +++ b/web/src/components/PartDetailsCell/PartDetailsCell.tsx @@ -2,17 +2,24 @@ import { useState } from 'react' import { mdiAlert, mdiPlus, mdiMinus } from '@mdi/js' import { Icon } from '@mdi/react' -import type { FindPartById } from 'types/graphql' +import type { FindPartDetailsById } from 'types/graphql' import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { addToBasket } from 'src/lib/basket' + +import ToastNotification from '../ToastNotification' export const QUERY = gql` - query FindPartById($id: Int!) { + query FindPartDetailsById($id: Int!) { part: part(id: $id) { + id name description availableStock imageUrl + createdAt } } ` @@ -47,7 +54,7 @@ const image = (url: string, size: number) => { return parts.join('/') } -export const Success = ({ part }: CellSuccessProps) => { +export const Success = ({ part }: CellSuccessProps) => { const [toTake, setToTake] = useState(1) return (
@@ -91,7 +98,31 @@ export const Success = ({ part }: CellSuccessProps) => {
- + diff --git a/web/src/components/PartsCell/PartsCell.tsx b/web/src/components/PartsCell/PartsCell.tsx index 321711c..89e30cb 100644 --- a/web/src/components/PartsCell/PartsCell.tsx +++ b/web/src/components/PartsCell/PartsCell.tsx @@ -38,6 +38,7 @@ export const QUERY = gql` description availableStock imageUrl + createdAt } count page diff --git a/web/src/components/PartsGridUnit/PartsGridUnit.tsx b/web/src/components/PartsGridUnit/PartsGridUnit.tsx index 887a001..e4bd774 100644 --- a/web/src/components/PartsGridUnit/PartsGridUnit.tsx +++ b/web/src/components/PartsGridUnit/PartsGridUnit.tsx @@ -1,4 +1,9 @@ import { Link, routes } from '@redwoodjs/router' +import { toast } from '@redwoodjs/web/toast' + +import { addToBasket } from 'src/lib/basket' + +import ToastNotification from '../ToastNotification' interface Props { part: { @@ -7,6 +12,7 @@ interface Props { description?: string availableStock: number imageUrl: string + createdAt: string } } @@ -53,13 +59,32 @@ const PartsGridUnit = ({ part }: Props) => { className={`btn btn-primary ${ part.availableStock == 0 ? 'btn-disabled' : '' }`} + onClick={() => { + const newBasket = addToBasket(part, 1) + + if (typeof newBasket == 'string') + toast.custom((t) => ( + + )) + else + toast.custom((t) => ( + + )) + }} > Add to basket - // TODO: add to basket funcionality ) } diff --git a/web/src/components/ToastNotification/ToastNotification.stories.tsx b/web/src/components/ToastNotification/ToastNotification.stories.tsx new file mode 100644 index 0000000..e795107 --- /dev/null +++ b/web/src/components/ToastNotification/ToastNotification.stories.tsx @@ -0,0 +1,25 @@ +// Pass props to your component by passing an `args` object to your story +// +// ```tsx +// export const Primary: Story = { +// args: { +// propName: propValue +// } +// } +// ``` +// +// See https://storybook.js.org/docs/react/writing-stories/args. + +import type { Meta, StoryObj } from '@storybook/react' + +import ToastNotification from './ToastNotification' + +const meta: Meta = { + component: ToastNotification, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/web/src/components/ToastNotification/ToastNotification.test.tsx b/web/src/components/ToastNotification/ToastNotification.test.tsx new file mode 100644 index 0000000..9542856 --- /dev/null +++ b/web/src/components/ToastNotification/ToastNotification.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import ToastNotification from './ToastNotification' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-components + +describe('ToastNotification', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/components/ToastNotification/ToastNotification.tsx b/web/src/components/ToastNotification/ToastNotification.tsx new file mode 100644 index 0000000..e35dd6e --- /dev/null +++ b/web/src/components/ToastNotification/ToastNotification.tsx @@ -0,0 +1,42 @@ +import { mdiCloseCircle, mdiInformation, mdiCheckCircle } from '@mdi/js' +import { Icon } from '@mdi/react' + +import { Toast } from '@redwoodjs/web/toast' + +type NotificationType = 'success' | 'error' | 'info' + +interface Props { + type: NotificationType + message: string + toast: Toast +} + +const ToastNotification = ({ type, message, toast }: Props) => ( +
+ +

{message}

+
+) + +export default ToastNotification diff --git a/web/src/index.css b/web/src/index.css index b5c61c9..73d6915 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,3 +1,12 @@ @tailwind base; @tailwind components; @tailwind utilities; + +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; +} diff --git a/web/src/index.html b/web/src/index.html index d0db47f..a40647e 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -11,7 +11,7 @@ - +
diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx index 1983f85..2e5913e 100644 --- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx +++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx @@ -1,11 +1,15 @@ -import { mdiChip, mdiMenu } from '@mdi/js' +import { useState } from 'react' + +import { mdiChip, mdiMenu, mdiBasket } from '@mdi/js' import Icon from '@mdi/react' import { Link, routes } from '@redwoodjs/router' +import { Toaster } from '@redwoodjs/web/toast' import { useAuth } from 'src/auth' import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon' import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle' +import { getBasket } from 'src/lib/basket' type NavBarLayoutProps = { children?: React.ReactNode @@ -13,9 +17,11 @@ type NavBarLayoutProps = { const NavBarLayout = ({ children }: NavBarLayoutProps) => { const { hasRole } = useAuth() + const [basket] = useState(getBasket()) return ( <> +
{ <> )} + +
+ {basket.length > 0 ? ( + + {basket.length} + + ) : ( + <> + )} + + +
+
{ ) : ( <> )} +
  • + +

    Basket

    + +
  • diff --git a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx index fcc3ece..5445154 100644 --- a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx +++ b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx @@ -21,7 +21,7 @@ const ScaffoldLayout = ({ }: LayoutProps) => { return (
    - +
    diff --git a/web/src/lib/basket.ts b/web/src/lib/basket.ts new file mode 100644 index 0000000..71fd27b --- /dev/null +++ b/web/src/lib/basket.ts @@ -0,0 +1,57 @@ +import { Part } from 'types/graphql' + +const BASKET_KEY = 'basket' + +export interface BasketItem { + part: Part + quantity: number +} + +export const getBasket = (): BasketItem[] => { + const basketRaw = localStorage.getItem(BASKET_KEY) + const basket: BasketItem[] = basketRaw ? JSON.parse(basketRaw) : [] + + return basket +} + +export const setBasket = (newBasket: BasketItem[]): BasketItem[] => { + localStorage.setItem(BASKET_KEY, JSON.stringify(newBasket)) + return getBasket() +} + +export const addToBasket = ( + part: Part, + quantity: number +): BasketItem[] | string => { + const basket = getBasket() + const existingPartIndex = basket.findIndex((item) => item.part.id == part.id) + + if (existingPartIndex !== -1) { + const basketPart = basket[existingPartIndex] + + if (basketPart.quantity + quantity <= basketPart.part.availableStock) + basket[existingPartIndex].quantity += quantity + else return `Cannot exceed number of items left (${part.availableStock})` + } else basket.push({ part, quantity }) + + localStorage.setItem(BASKET_KEY, JSON.stringify(basket)) + + return basket +} + +export const removeFromBasket = (index: number): BasketItem[] | string => { + const basket = getBasket() + + if (index >= 0 && index < basket.length) basket.splice(index, 1) + else return 'Error: index out of bounds' + + localStorage.setItem(BASKET_KEY, JSON.stringify(basket)) + + return basket +} + +export const clearBasket = (): BasketItem[] => { + localStorage.removeItem(BASKET_KEY) + + return [] +} diff --git a/web/src/pages/BasketPage/BasketPage.stories.tsx b/web/src/pages/BasketPage/BasketPage.stories.tsx new file mode 100644 index 0000000..f95fafe --- /dev/null +++ b/web/src/pages/BasketPage/BasketPage.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import BasketPage from './BasketPage' + +const meta: Meta = { + component: BasketPage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/web/src/pages/BasketPage/BasketPage.test.tsx b/web/src/pages/BasketPage/BasketPage.test.tsx new file mode 100644 index 0000000..3e46567 --- /dev/null +++ b/web/src/pages/BasketPage/BasketPage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@redwoodjs/testing/web' + +import BasketPage from './BasketPage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('BasketPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/web/src/pages/BasketPage/BasketPage.tsx b/web/src/pages/BasketPage/BasketPage.tsx new file mode 100644 index 0000000..89d7080 --- /dev/null +++ b/web/src/pages/BasketPage/BasketPage.tsx @@ -0,0 +1,172 @@ +import { useState } from 'react' + +import { mdiDelete, mdiMinus, mdiPlus } from '@mdi/js' +import Icon from '@mdi/react' + +import { MetaTags } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +import { useAuth } from 'src/auth' +import ToastNotification from 'src/components/ToastNotification' +import { + clearBasket, + getBasket, + removeFromBasket, + setBasket, +} from 'src/lib/basket' + +const thumbnail = (url: string) => { + if (url.includes('no_image.png')) return url + const parts = url.split('/') + parts.splice(3, 0, 'resize=width:160') + return parts.join('/') +} + +const BasketPage = () => { + const { isAuthenticated } = useAuth() + const [basket, setBasketState] = useState(getBasket()) + return ( + <> + + +
    +

    Basket

    +
    + {basket.length > 0 ? ( + basket.map((item, i) => ( +
    + {item.part.name} +
    +

    + {item.part.name} +

    +
    +
    + +

    + {item.quantity} +

    + +
    + +
    +
    +
    + )) + ) : ( +
    +
    +

    + It's empty in here... +

    +
    +
    + )} + {basket.length > 0 ? ( +
    + + +
    + ) : ( + <> + )} +
    +
    + + ) +} + +export default BasketPage diff --git a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx index 8a793bd..1414713 100644 --- a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx +++ b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx @@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web' import { toast, Toaster } from '@redwoodjs/web/toast' import { useAuth } from 'src/auth' +import ToastNotification from 'src/components/ToastNotification' const ForgotPasswordPage = () => { const { isAuthenticated, forgotPassword } = useAuth() @@ -25,14 +26,20 @@ const ForgotPasswordPage = () => { const response = await forgotPassword(data.email.toLowerCase()) if (response.error) { - toast.error(response.error) + toast.custom((t) => ( + + )) } else { // The function `forgotPassword.handler` in api/src/functions/auth.js has // been invoked, let the user know how to get the link to reset their // password (sent in email, perhaps?) - toast.success( - 'A link to reset your password was sent to ' + response.email - ) + toast.custom((t) => ( + + )) navigate(routes.login()) } } diff --git a/web/src/pages/LoginPage/LoginPage.tsx b/web/src/pages/LoginPage/LoginPage.tsx index bde21a3..13e474a 100644 --- a/web/src/pages/LoginPage/LoginPage.tsx +++ b/web/src/pages/LoginPage/LoginPage.tsx @@ -1,5 +1,4 @@ -import { useRef } from 'react' -import { useEffect } from 'react' +import { useRef, useEffect } from 'react' import { Form, @@ -13,6 +12,7 @@ import { MetaTags } from '@redwoodjs/web' import { toast, Toaster } from '@redwoodjs/web/toast' import { useAuth } from 'src/auth' +import ToastNotification from 'src/components/ToastNotification' const LoginPage = () => { const { isAuthenticated, logIn } = useAuth() @@ -37,9 +37,13 @@ const LoginPage = () => { if (response.message) { toast(response.message) } else if (response.error) { - toast.error(response.error) + toast.custom((t) => ( + + )) } else { - toast.success('Welcome back!') + toast.custom((t) => ( + + )) } } diff --git a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx index aba9856..0d9e00e 100644 --- a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx +++ b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web' import { toast, Toaster } from '@redwoodjs/web/toast' import { useAuth } from 'src/auth' +import ToastNotification from 'src/components/ToastNotification' const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } = @@ -23,7 +24,9 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { const response = await validateResetToken(resetToken) if (response.error) { setEnabled(false) - toast.error(response.error) + toast.custom((t) => ( + + )) } else { setEnabled(true) } @@ -43,9 +46,17 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => { }) if (response.error) { - toast.error(response.error) + toast.custom((t) => ( + + )) } else { - toast.success('Password changed!') + toast.custom((t) => ( + + )) await reauthenticate() navigate(routes.login()) } diff --git a/web/src/pages/SignupPage/SignupPage.tsx b/web/src/pages/SignupPage/SignupPage.tsx index 66e580f..c3527a3 100644 --- a/web/src/pages/SignupPage/SignupPage.tsx +++ b/web/src/pages/SignupPage/SignupPage.tsx @@ -13,6 +13,7 @@ import { MetaTags } from '@redwoodjs/web' import { toast, Toaster } from '@redwoodjs/web/toast' import { useAuth } from 'src/auth' +import ToastNotification from 'src/components/ToastNotification' const SignupPage = () => { const { isAuthenticated, signUp } = useAuth() @@ -37,14 +38,16 @@ const SignupPage = () => { lastName: data.lastName, }) - if (response.message) { - toast(response.message) - } else if (response.error) { - toast.error(response.error) - } else { - // user is signed in automatically - toast.success('Welcome!') - } + if (response.message) toast(response.message) + else if (response.error) + toast.custom((t) => ( + + )) + // user is signed in automatically + else + toast.custom((t) => ( + + )) } return ( diff --git a/yarn.lock b/yarn.lock index e2535bf..3047a69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19549,6 +19549,15 @@ __metadata: languageName: node linkType: hard +"tailwindcss-animate@npm:^1.0.7": + version: 1.0.7 + resolution: "tailwindcss-animate@npm:1.0.7" + peerDependencies: + tailwindcss: "*" + checksum: ec7dbd1631076b97d66a1fbaaa06e0725fccfa63119221e8d87a997b02dcede98ad88bb1ef6665b968f5d260fcefb10592e0299ca70208d365b37761edf5e19a + languageName: node + linkType: hard + "tailwindcss@npm:^3.1, tailwindcss@npm:^3.3.3": version: 3.3.3 resolution: "tailwindcss@npm:3.3.3" @@ -20946,6 +20955,7 @@ __metadata: react: 18.2.0 react-dom: 18.2.0 tailwindcss: ^3.3.3 + tailwindcss-animate: ^1.0.7 theme-change: ^2.5.0 languageName: unknown linkType: soft