1
0

Basket functionality, no DB transaction storing yet, and some more customization to make the theme uniform

This commit is contained in:
Ahmed Al-Taiar
2023-11-06 22:06:44 -05:00
parent 53e0070fd8
commit f6f01594ec
33 changed files with 539 additions and 122 deletions

View File

@ -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");

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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")
}
`

View File

@ -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'],
}

View File

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -25,6 +25,7 @@ const Routes = () => {
<Set wrap={NavbarLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/part/{id:Int}" page={PartPage} name="partDetails" />
<Route path="/basket" page={BasketPage} name="basket" />
</Set>
<Route notfound page={NotFoundPage} />

View File

@ -11,7 +11,7 @@ interface Props {
}
const NavbarAccountIcon = ({ mobile, className }: Props) => {
const { isAuthenticated, currentUser, logOut, hasRole } = useAuth()
const { isAuthenticated, currentUser, logOut } = useAuth()
return isAuthenticated ? (
<div className={className}>
@ -21,12 +21,7 @@ const NavbarAccountIcon = ({ mobile, className }: Props) => {
}`}
>
<summary className="btn btn-ghost swap swap-rotate w-12 hover:shadow-lg">
<Icon
path={mdiAccount}
className={`h-8 w-8 ${
hasRole('admin') ? 'text-error' : 'text-base-content'
}`}
/>
<Icon path={mdiAccount} className="h-8 w-8 text-base-content" />
</summary>
<div className="dropdown-content flex w-auto flex-row items-center space-x-3 rounded-xl bg-base-100 p-3 shadow-lg">
<p className="whitespace-nowrap font-inter text-lg">

View File

@ -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<EditPartById>) => {
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
onCompleted: () => {
toast.success('Part updated')
toast.custom((t) => (
<ToastNotification toast={t} type="success" message="Part updated" />
))
navigate(routes.parts())
},
onError: (error) => {
toast.error(error.message)
toast.custom((t) => (
<ToastNotification toast={t} type="error" message={error.message} />
))
},
})

View File

@ -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) => (
<ToastNotification toast={t} type="success" message="Part created" />
))
navigate(routes.parts())
},
onError: (error) => {
toast.error(error.message)
toast.custom((t) => (
<ToastNotification toast={t} type="error" message={error.message} />
))
},
})

View File

@ -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) => (
<ToastNotification toast={t} type="success" message="Part deleted" />
))
navigate(routes.parts())
},
onError: (error) => {
toast.error(error.message)
toast.custom((t) => (
<ToastNotification toast={t} type="error" message={error.message} />
))
},
})

View File

@ -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) => (
<ToastNotification toast={t} type="success" message="Part deleted" />
))
},
onError: (error) => {
toast.error(error.message)
toast.custom((t) => (
<ToastNotification toast={t} type="error" message={error.message} />
))
},
// 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 (
<div className="rw-segment rw-table-wrapper-responsive">
<div className="rw-segment rw-table-wrapper-responsive font-inter">
<table className="rw-table">
<thead>
<tr>

View File

@ -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<FindPartById>) => {
export const Success = ({ part }: CellSuccessProps<FindPartDetailsById>) => {
const [toTake, setToTake] = useState(1)
return (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
@ -91,7 +98,31 @@ export const Success = ({ part }: CellSuccessProps<FindPartById>) => {
<Icon path={mdiPlus} className="h-6 w-6" />
</button>
</div>
<button className="btn btn-primary">Add to basket</button>
<button
className="btn btn-primary"
onClick={() => {
const newBasket = addToBasket(part, toTake)
if (typeof newBasket == 'string')
toast.custom((t) => (
<ToastNotification
toast={t}
type="error"
message={newBasket}
/>
))
else
toast.custom((t) => (
<ToastNotification
toast={t}
type="success"
message={`Added ${toTake} ${part.name} to basket`}
/>
))
}}
>
Add to basket
</button>
</div>
</div>
</div>

View File

@ -38,6 +38,7 @@ export const QUERY = gql`
description
availableStock
imageUrl
createdAt
}
count
page

View File

@ -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) => (
<ToastNotification
toast={t}
type="error"
message={newBasket}
/>
))
else
toast.custom((t) => (
<ToastNotification
toast={t}
type="success"
message={`Added 1 ${part.name} to basket`}
/>
))
}}
>
Add to basket
</button>
</div>
</div>
</div>
// TODO: add to basket funcionality
)
}

View File

@ -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<typeof ToastNotification> = {
component: ToastNotification,
}
export default meta
type Story = StoryObj<typeof ToastNotification>
export const Primary: Story = {}

View File

@ -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(<ToastNotification />)
}).not.toThrow()
})
})

View File

@ -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) => (
<div
className={`${
toast.visible
? 'duration-200 animate-in slide-in-from-top'
: 'duration-200 animate-out slide-out-to-top'
} pointer-events-auto flex w-full max-w-sm items-center rounded-2xl bg-base-100 shadow-lg`}
>
<Icon
className={`m-3 h-8 w-8 ${
type == 'success'
? 'text-success'
: type == 'error'
? 'text-error'
: 'text-info'
}`}
path={
type == 'success'
? mdiCheckCircle
: type == 'error'
? mdiCloseCircle
: mdiInformation
}
/>
<p className="font-inter">{message}</p>
</div>
)
export default ToastNotification

View File

@ -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;
}

View File

@ -11,7 +11,7 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Open+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<body class="no-scrollbar">
<!-- Please keep this div empty -->
<div id="redwood-app"></div>
</body>

View File

@ -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 (
<>
<Toaster />
<div className="navbar sticky top-0 z-50 bg-base-100 shadow-lg">
<div className="justify-start space-x-3">
<Icon
@ -47,6 +53,22 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
<></>
)}
<ThemeToggle />
<Link
to={routes.basket()}
className="items-cente btn btn-ghost hidden hover:shadow-lg lg:flex"
>
<div className="indicator">
{basket.length > 0 ? (
<span className="badge indicator-item badge-primary font-inter">
{basket.length}
</span>
) : (
<></>
)}
<Icon path={mdiBasket} className="h-8 w-8 text-base-content" />
</div>
</Link>
<NavbarAccountIcon mobile={false} className="hidden lg:block" />
<div className="lg:hidden">
<input
@ -94,6 +116,14 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
) : (
<></>
)}
<li>
<Link
to={routes.basket()}
className="btn btn-ghost w-full hover:shadow-lg"
>
<p className="font-inter text-base">Basket</p>
</Link>
</li>
</ul>
</div>
</div>

View File

@ -21,7 +21,7 @@ const ScaffoldLayout = ({
}: LayoutProps) => {
return (
<div className="rw-scaffold">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<Toaster />
<header className="rw-header">
<div className="space-x-3">
<Link to={routes.home()} className="btn btn-ghost">

57
web/src/lib/basket.ts Normal file
View File

@ -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 []
}

View File

@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react'
import BasketPage from './BasketPage'
const meta: Meta<typeof BasketPage> = {
component: BasketPage,
}
export default meta
type Story = StoryObj<typeof BasketPage>
export const Primary: Story = {}

View File

@ -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(<BasketPage />)
}).not.toThrow()
})
})

View File

@ -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 (
<>
<MetaTags title="Basket" description="Basket page" />
<div className="m-8">
<h1 className="mb-8 font-inter text-3xl font-bold">Basket</h1>
<div className="space-y-3">
{basket.length > 0 ? (
basket.map((item, i) => (
<div
key={i}
className="flex max-w-5xl items-center rounded-xl bg-base-100 shadow-xl"
>
<img
alt={item.part.name}
className="hidden h-20 w-20 rounded-l-xl object-cover sm:flex"
src={thumbnail(item.part.imageUrl)}
/>
<div className="m-3 w-full items-center justify-between space-y-3 sm:flex sm:space-y-0">
<p className="overflow-hidden text-ellipsis whitespace-nowrap font-inter text-lg font-bold">
{item.part.name}
</p>
<div className="flex justify-between space-x-3">
<div className="join">
<button
className={`btn join-item ${
item.quantity == 1 ? 'btn-disabled' : ''
}`}
onClick={() => {
const newBasket = basket
newBasket[i].quantity -= 1
setBasketState(setBasket(newBasket))
}}
>
<Icon path={mdiMinus} className="h-6 w-6" />
</button>
<p className="btn join-item items-center font-inter text-lg">
{item.quantity}
</p>
<button
className={`btn join-item ${
item.quantity == item.part.availableStock
? 'btn-disabled'
: ''
}`}
onClick={() => {
const newBasket = basket
newBasket[i].quantity += 1
setBasketState(setBasket(newBasket))
}}
>
<Icon path={mdiPlus} className="h-6 w-6" />
</button>
</div>
<button
className="btn btn-ghost hover:shadow-lg"
onClick={() => {
const newBasket = removeFromBasket(i)
if (typeof newBasket == 'string')
toast.custom((t) => (
<ToastNotification
toast={t}
type="error"
message={newBasket}
/>
))
else {
setBasketState(newBasket)
toast.custom((t) => (
<ToastNotification
toast={t}
type="success"
message={`Removed ${item.part.name} from basket`}
/>
))
}
}}
>
<Icon
path={mdiDelete}
className="h-8 w-8 text-base-content"
/>
</button>
</div>
</div>
</div>
))
) : (
<div className="flex">
<div className="alert w-auto shadow-lg">
<p className="text-center font-inter">
It&#39;s empty in here...
</p>
</div>
</div>
)}
{basket.length > 0 ? (
<div className="flex space-x-3 pt-3">
<button
onClick={() => {
setBasketState(clearBasket())
toast.custom((t) => (
<ToastNotification
toast={t}
type="success"
message="Basket cleared"
/>
))
}}
className="btn font-inter"
>
Clear basket
</button>
<button
onClick={() => {
if (!isAuthenticated)
toast.custom((t) => (
<ToastNotification
toast={t}
type="error"
message="You must be logged in to do that"
/>
))
else {
toast.custom((t) => (
<ToastNotification toast={t} type="info" message="Todo" />
))
}
}}
className="btn btn-primary font-inter"
>
Checkout
</button>
</div>
) : (
<></>
)}
</div>
</div>
</>
)
}
export default BasketPage

View File

@ -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) => (
<ToastNotification toast={t} type="error" message={response.error} />
))
} 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) => (
<ToastNotification
toast={t}
type="success"
message={`A link to reset your password was sent to ${response.email}`}
/>
))
navigate(routes.login())
}
}

View File

@ -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) => (
<ToastNotification toast={t} type="error" message={response.error} />
))
} else {
toast.success('Welcome back!')
toast.custom((t) => (
<ToastNotification toast={t} type="success" message="Welcome back!" />
))
}
}

View File

@ -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) => (
<ToastNotification toast={t} type="error" message={response.error} />
))
} else {
setEnabled(true)
}
@ -43,9 +46,17 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
})
if (response.error) {
toast.error(response.error)
toast.custom((t) => (
<ToastNotification toast={t} type="error" message={response.error} />
))
} else {
toast.success('Password changed!')
toast.custom((t) => (
<ToastNotification
toast={t}
type="success"
message="Password changed!"
/>
))
await reauthenticate()
navigate(routes.login())
}

View File

@ -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) => (
<ToastNotification toast={t} type="error" message={response.error} />
))
// user is signed in automatically
else
toast.custom((t) => (
<ToastNotification toast={t} type="success" message="Welcome!" />
))
}
return (

View File

@ -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