Basket functionality, no DB transaction storing yet, and some more customization to make the theme uniform
This commit is contained in:
@ -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'],
|
||||
}
|
||||
|
@ -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 |
@ -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} />
|
||||
|
@ -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">
|
||||
|
@ -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} />
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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} />
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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} />
|
||||
))
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -38,6 +38,7 @@ export const QUERY = gql`
|
||||
description
|
||||
availableStock
|
||||
imageUrl
|
||||
createdAt
|
||||
}
|
||||
count
|
||||
page
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 components;
|
||||
@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">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="no-scrollbar">
|
||||
<!-- Please keep this div empty -->
|
||||
<div id="redwood-app"></div>
|
||||
</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 { 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>
|
||||
|
@ -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
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 { 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())
|
||||
}
|
||||
}
|
||||
|
@ -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!" />
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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 (
|
||||
|
Reference in New Issue
Block a user