Basket functionality, no DB transaction storing yet, and some more customization to make the theme uniform
This commit is contained in:
@ -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");
|
|
@ -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;
|
|
@ -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;
|
|
@ -11,12 +11,14 @@ CREATE TABLE "Part" (
|
|||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "User" (
|
CREATE TABLE "User" (
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
"name" TEXT,
|
"firstName" TEXT NOT NULL,
|
||||||
|
"lastName" TEXT NOT NULL,
|
||||||
"email" TEXT NOT NULL,
|
"email" TEXT NOT NULL,
|
||||||
"hashedPassword" TEXT NOT NULL,
|
"hashedPassword" TEXT NOT NULL,
|
||||||
"salt" TEXT NOT NULL,
|
"salt" TEXT NOT NULL,
|
||||||
"resetToken" TEXT,
|
"resetToken" TEXT,
|
||||||
"resetTokenExpiresAt" DATETIME
|
"resetTokenExpiresAt" DATETIME,
|
||||||
|
"roles" TEXT NOT NULL DEFAULT 'user'
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
@ -51,7 +51,8 @@ export const schema = gql`
|
|||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
createPart(input: CreatePartInput!): Part! @requireAuth
|
createPart(input: CreatePartInput!): Part! @requireAuth
|
||||||
updatePart(id: Int!, input: UpdatePartInput!): Part! @requireAuth
|
updatePart(id: Int!, input: UpdatePartInput!): Part!
|
||||||
deletePart(id: Int!): Part! @requireAuth
|
@requireAuth(roles: "admin")
|
||||||
|
deletePart(id: Int!): Part! @requireAuth(roles: "admin")
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -13,7 +13,7 @@ export const theme = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
export const plugins = [require('daisyui')]
|
export const plugins = [require('daisyui'), require('tailwindcss-animate')]
|
||||||
export const daisyui = {
|
export const daisyui = {
|
||||||
themes: ['light', 'dark'],
|
themes: ['light', 'dark'],
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
"daisyui": "^3.9.3",
|
"daisyui": "^3.9.3",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-loader": "^7.3.3",
|
"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 |
@ -25,6 +25,7 @@ const Routes = () => {
|
|||||||
<Set wrap={NavbarLayout}>
|
<Set wrap={NavbarLayout}>
|
||||||
<Route path="/" page={HomePage} name="home" />
|
<Route path="/" page={HomePage} name="home" />
|
||||||
<Route path="/part/{id:Int}" page={PartPage} name="partDetails" />
|
<Route path="/part/{id:Int}" page={PartPage} name="partDetails" />
|
||||||
|
<Route path="/basket" page={BasketPage} name="basket" />
|
||||||
</Set>
|
</Set>
|
||||||
|
|
||||||
<Route notfound page={NotFoundPage} />
|
<Route notfound page={NotFoundPage} />
|
||||||
|
@ -11,7 +11,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NavbarAccountIcon = ({ mobile, className }: Props) => {
|
const NavbarAccountIcon = ({ mobile, className }: Props) => {
|
||||||
const { isAuthenticated, currentUser, logOut, hasRole } = useAuth()
|
const { isAuthenticated, currentUser, logOut } = useAuth()
|
||||||
|
|
||||||
return isAuthenticated ? (
|
return isAuthenticated ? (
|
||||||
<div className={className}>
|
<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">
|
<summary className="btn btn-ghost swap swap-rotate w-12 hover:shadow-lg">
|
||||||
<Icon
|
<Icon path={mdiAccount} className="h-8 w-8 text-base-content" />
|
||||||
path={mdiAccount}
|
|
||||||
className={`h-8 w-8 ${
|
|
||||||
hasRole('admin') ? 'text-error' : 'text-base-content'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</summary>
|
</summary>
|
||||||
<div className="dropdown-content flex w-auto flex-row items-center space-x-3 rounded-xl bg-base-100 p-3 shadow-lg">
|
<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">
|
<p className="whitespace-nowrap font-inter text-lg">
|
||||||
|
@ -6,6 +6,7 @@ import { useMutation } from '@redwoodjs/web'
|
|||||||
import { toast } from '@redwoodjs/web/toast'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import PartForm from 'src/components/Part/PartForm'
|
import PartForm from 'src/components/Part/PartForm'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
export const QUERY = gql`
|
export const QUERY = gql`
|
||||||
query EditPartById($id: Int!) {
|
query EditPartById($id: Int!) {
|
||||||
@ -41,11 +42,15 @@ export const Failure = ({ error }: CellFailureProps) => (
|
|||||||
export const Success = ({ part }: CellSuccessProps<EditPartById>) => {
|
export const Success = ({ part }: CellSuccessProps<EditPartById>) => {
|
||||||
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Part updated')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Part updated" />
|
||||||
|
))
|
||||||
navigate(routes.parts())
|
navigate(routes.parts())
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={error.message} />
|
||||||
|
))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web'
|
|||||||
import { toast } from '@redwoodjs/web/toast'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import PartForm from 'src/components/Part/PartForm'
|
import PartForm from 'src/components/Part/PartForm'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
const CREATE_PART_MUTATION = gql`
|
const CREATE_PART_MUTATION = gql`
|
||||||
mutation CreatePartMutation($input: CreatePartInput!) {
|
mutation CreatePartMutation($input: CreatePartInput!) {
|
||||||
@ -17,11 +18,15 @@ const CREATE_PART_MUTATION = gql`
|
|||||||
const NewPart = () => {
|
const NewPart = () => {
|
||||||
const [createPart, { loading, error }] = useMutation(CREATE_PART_MUTATION, {
|
const [createPart, { loading, error }] = useMutation(CREATE_PART_MUTATION, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Part created')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Part created" />
|
||||||
|
))
|
||||||
navigate(routes.parts())
|
navigate(routes.parts())
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={error.message} />
|
||||||
|
))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -4,7 +4,8 @@ import { Link, routes, navigate } from '@redwoodjs/router'
|
|||||||
import { useMutation } from '@redwoodjs/web'
|
import { useMutation } from '@redwoodjs/web'
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
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`
|
const DELETE_PART_MUTATION = gql`
|
||||||
mutation DeletePartMutation($id: Int!) {
|
mutation DeletePartMutation($id: Int!) {
|
||||||
@ -21,11 +22,15 @@ interface Props {
|
|||||||
const Part = ({ part }: Props) => {
|
const Part = ({ part }: Props) => {
|
||||||
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
|
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Part deleted')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Part deleted" />
|
||||||
|
))
|
||||||
navigate(routes.parts())
|
navigate(routes.parts())
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={error.message} />
|
||||||
|
))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web'
|
|||||||
import { toast } from '@redwoodjs/web/toast'
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { QUERY } from 'src/components/Part/PartsCell'
|
import { QUERY } from 'src/components/Part/PartsCell'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
import { timeTag, truncate } from 'src/lib/formatters'
|
import { timeTag, truncate } from 'src/lib/formatters'
|
||||||
|
|
||||||
const DELETE_PART_MUTATION = gql`
|
const DELETE_PART_MUTATION = gql`
|
||||||
@ -18,13 +19,17 @@ const DELETE_PART_MUTATION = gql`
|
|||||||
const PartsList = ({ parts }: FindParts) => {
|
const PartsList = ({ parts }: FindParts) => {
|
||||||
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
|
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
toast.success('Part deleted')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Part deleted" />
|
||||||
|
))
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
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
|
// 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
|
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
|
||||||
refetchQueries: [{ query: QUERY }],
|
refetchQueries: [{ query: QUERY }],
|
||||||
awaitRefetchQueries: true,
|
awaitRefetchQueries: true,
|
||||||
@ -44,7 +49,7 @@ const PartsList = ({ parts }: FindParts) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rw-segment rw-table-wrapper-responsive">
|
<div className="rw-segment rw-table-wrapper-responsive font-inter">
|
||||||
<table className="rw-table">
|
<table className="rw-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -2,17 +2,24 @@ import { useState } from 'react'
|
|||||||
|
|
||||||
import { mdiAlert, mdiPlus, mdiMinus } from '@mdi/js'
|
import { mdiAlert, mdiPlus, mdiMinus } from '@mdi/js'
|
||||||
import { Icon } from '@mdi/react'
|
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 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`
|
export const QUERY = gql`
|
||||||
query FindPartById($id: Int!) {
|
query FindPartDetailsById($id: Int!) {
|
||||||
part: part(id: $id) {
|
part: part(id: $id) {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
description
|
description
|
||||||
availableStock
|
availableStock
|
||||||
imageUrl
|
imageUrl
|
||||||
|
createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@ -47,7 +54,7 @@ const image = (url: string, size: number) => {
|
|||||||
return parts.join('/')
|
return parts.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Success = ({ part }: CellSuccessProps<FindPartById>) => {
|
export const Success = ({ part }: CellSuccessProps<FindPartDetailsById>) => {
|
||||||
const [toTake, setToTake] = useState(1)
|
const [toTake, setToTake] = useState(1)
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
|
<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" />
|
<Icon path={mdiPlus} className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +38,7 @@ export const QUERY = gql`
|
|||||||
description
|
description
|
||||||
availableStock
|
availableStock
|
||||||
imageUrl
|
imageUrl
|
||||||
|
createdAt
|
||||||
}
|
}
|
||||||
count
|
count
|
||||||
page
|
page
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { toast } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
|
import { addToBasket } from 'src/lib/basket'
|
||||||
|
|
||||||
|
import ToastNotification from '../ToastNotification'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
part: {
|
part: {
|
||||||
@ -7,6 +12,7 @@ interface Props {
|
|||||||
description?: string
|
description?: string
|
||||||
availableStock: number
|
availableStock: number
|
||||||
imageUrl: string
|
imageUrl: string
|
||||||
|
createdAt: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,13 +59,32 @@ const PartsGridUnit = ({ part }: Props) => {
|
|||||||
className={`btn btn-primary ${
|
className={`btn btn-primary ${
|
||||||
part.availableStock == 0 ? 'btn-disabled' : ''
|
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
|
Add to basket
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
// TODO: add to basket funcionality
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 = {}
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
42
web/src/components/ToastNotification/ToastNotification.tsx
Normal file
42
web/src/components/ToastNotification/ToastNotification.tsx
Normal 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
|
@ -1,3 +1,12 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
@ -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">
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="no-scrollbar">
|
||||||
<!-- Please keep this div empty -->
|
<!-- Please keep this div empty -->
|
||||||
<div id="redwood-app"></div>
|
<div id="redwood-app"></div>
|
||||||
</body>
|
</body>
|
||||||
|
@ -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 Icon from '@mdi/react'
|
||||||
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
|
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
|
||||||
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
||||||
|
import { getBasket } from 'src/lib/basket'
|
||||||
|
|
||||||
type NavBarLayoutProps = {
|
type NavBarLayoutProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@ -13,9 +17,11 @@ type NavBarLayoutProps = {
|
|||||||
|
|
||||||
const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
||||||
const { hasRole } = useAuth()
|
const { hasRole } = useAuth()
|
||||||
|
const [basket] = useState(getBasket())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Toaster />
|
||||||
<div className="navbar sticky top-0 z-50 bg-base-100 shadow-lg">
|
<div className="navbar sticky top-0 z-50 bg-base-100 shadow-lg">
|
||||||
<div className="justify-start space-x-3">
|
<div className="justify-start space-x-3">
|
||||||
<Icon
|
<Icon
|
||||||
@ -47,6 +53,22 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<ThemeToggle />
|
<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" />
|
<NavbarAccountIcon mobile={false} className="hidden lg:block" />
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
<input
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,7 +21,7 @@ const ScaffoldLayout = ({
|
|||||||
}: LayoutProps) => {
|
}: LayoutProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="rw-scaffold">
|
<div className="rw-scaffold">
|
||||||
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
|
<Toaster />
|
||||||
<header className="rw-header">
|
<header className="rw-header">
|
||||||
<div className="space-x-3">
|
<div className="space-x-3">
|
||||||
<Link to={routes.home()} className="btn btn-ghost">
|
<Link to={routes.home()} className="btn btn-ghost">
|
||||||
|
57
web/src/lib/basket.ts
Normal file
57
web/src/lib/basket.ts
Normal 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 []
|
||||||
|
}
|
13
web/src/pages/BasketPage/BasketPage.stories.tsx
Normal file
13
web/src/pages/BasketPage/BasketPage.stories.tsx
Normal 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 = {}
|
14
web/src/pages/BasketPage/BasketPage.test.tsx
Normal file
14
web/src/pages/BasketPage/BasketPage.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
172
web/src/pages/BasketPage/BasketPage.tsx
Normal file
172
web/src/pages/BasketPage/BasketPage.tsx
Normal 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'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
|
@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web'
|
|||||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
const ForgotPasswordPage = () => {
|
const ForgotPasswordPage = () => {
|
||||||
const { isAuthenticated, forgotPassword } = useAuth()
|
const { isAuthenticated, forgotPassword } = useAuth()
|
||||||
@ -25,14 +26,20 @@ const ForgotPasswordPage = () => {
|
|||||||
const response = await forgotPassword(data.email.toLowerCase())
|
const response = await forgotPassword(data.email.toLowerCase())
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
toast.error(response.error)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={response.error} />
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
// The function `forgotPassword.handler` in api/src/functions/auth.js has
|
// 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
|
// been invoked, let the user know how to get the link to reset their
|
||||||
// password (sent in email, perhaps?)
|
// password (sent in email, perhaps?)
|
||||||
toast.success(
|
toast.custom((t) => (
|
||||||
'A link to reset your password was sent to ' + response.email
|
<ToastNotification
|
||||||
)
|
toast={t}
|
||||||
|
type="success"
|
||||||
|
message={`A link to reset your password was sent to ${response.email}`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
navigate(routes.login())
|
navigate(routes.login())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useRef } from 'react'
|
import { useRef, useEffect } from 'react'
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -13,6 +12,7 @@ import { MetaTags } from '@redwoodjs/web'
|
|||||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const { isAuthenticated, logIn } = useAuth()
|
const { isAuthenticated, logIn } = useAuth()
|
||||||
@ -37,9 +37,13 @@ const LoginPage = () => {
|
|||||||
if (response.message) {
|
if (response.message) {
|
||||||
toast(response.message)
|
toast(response.message)
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
toast.error(response.error)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={response.error} />
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
toast.success('Welcome back!')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Welcome back!" />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web'
|
|||||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
||||||
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
|
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
|
||||||
@ -23,7 +24,9 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
|||||||
const response = await validateResetToken(resetToken)
|
const response = await validateResetToken(resetToken)
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
setEnabled(false)
|
setEnabled(false)
|
||||||
toast.error(response.error)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={response.error} />
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
setEnabled(true)
|
setEnabled(true)
|
||||||
}
|
}
|
||||||
@ -43,9 +46,17 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
toast.error(response.error)
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="error" message={response.error} />
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
toast.success('Password changed!')
|
toast.custom((t) => (
|
||||||
|
<ToastNotification
|
||||||
|
toast={t}
|
||||||
|
type="success"
|
||||||
|
message="Password changed!"
|
||||||
|
/>
|
||||||
|
))
|
||||||
await reauthenticate()
|
await reauthenticate()
|
||||||
navigate(routes.login())
|
navigate(routes.login())
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { MetaTags } from '@redwoodjs/web'
|
|||||||
import { toast, Toaster } from '@redwoodjs/web/toast'
|
import { toast, Toaster } from '@redwoodjs/web/toast'
|
||||||
|
|
||||||
import { useAuth } from 'src/auth'
|
import { useAuth } from 'src/auth'
|
||||||
|
import ToastNotification from 'src/components/ToastNotification'
|
||||||
|
|
||||||
const SignupPage = () => {
|
const SignupPage = () => {
|
||||||
const { isAuthenticated, signUp } = useAuth()
|
const { isAuthenticated, signUp } = useAuth()
|
||||||
@ -37,14 +38,16 @@ const SignupPage = () => {
|
|||||||
lastName: data.lastName,
|
lastName: data.lastName,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.message) {
|
if (response.message) toast(response.message)
|
||||||
toast(response.message)
|
else if (response.error)
|
||||||
} else if (response.error) {
|
toast.custom((t) => (
|
||||||
toast.error(response.error)
|
<ToastNotification toast={t} type="error" message={response.error} />
|
||||||
} else {
|
))
|
||||||
// user is signed in automatically
|
// user is signed in automatically
|
||||||
toast.success('Welcome!')
|
else
|
||||||
}
|
toast.custom((t) => (
|
||||||
|
<ToastNotification toast={t} type="success" message="Welcome!" />
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -19549,6 +19549,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tailwindcss@npm:^3.1, tailwindcss@npm:^3.3.3":
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
resolution: "tailwindcss@npm:3.3.3"
|
resolution: "tailwindcss@npm:3.3.3"
|
||||||
@ -20946,6 +20955,7 @@ __metadata:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0
|
react-dom: 18.2.0
|
||||||
tailwindcss: ^3.3.3
|
tailwindcss: ^3.3.3
|
||||||
|
tailwindcss-animate: ^1.0.7
|
||||||
theme-change: ^2.5.0
|
theme-change: ^2.5.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
Reference in New Issue
Block a user