@@ -21,12 +21,7 @@ const NavbarAccountIcon = ({ mobile, className }: Props) => {
}`}
>
diff --git a/web/src/components/Part/EditPartCell/EditPartCell.tsx b/web/src/components/Part/EditPartCell/EditPartCell.tsx
index ee118ef..d52fe0c 100644
--- a/web/src/components/Part/EditPartCell/EditPartCell.tsx
+++ b/web/src/components/Part/EditPartCell/EditPartCell.tsx
@@ -6,6 +6,7 @@ import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import PartForm from 'src/components/Part/PartForm'
+import ToastNotification from 'src/components/ToastNotification'
export const QUERY = gql`
query EditPartById($id: Int!) {
@@ -41,11 +42,15 @@ export const Failure = ({ error }: CellFailureProps) => (
export const Success = ({ part }: CellSuccessProps) => {
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
onCompleted: () => {
- toast.success('Part updated')
+ toast.custom((t) => (
+
+ ))
navigate(routes.parts())
},
onError: (error) => {
- toast.error(error.message)
+ toast.custom((t) => (
+
+ ))
},
})
diff --git a/web/src/components/Part/NewPart/NewPart.tsx b/web/src/components/Part/NewPart/NewPart.tsx
index 9f37781..a2b8d91 100644
--- a/web/src/components/Part/NewPart/NewPart.tsx
+++ b/web/src/components/Part/NewPart/NewPart.tsx
@@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import PartForm from 'src/components/Part/PartForm'
+import ToastNotification from 'src/components/ToastNotification'
const CREATE_PART_MUTATION = gql`
mutation CreatePartMutation($input: CreatePartInput!) {
@@ -17,11 +18,15 @@ const CREATE_PART_MUTATION = gql`
const NewPart = () => {
const [createPart, { loading, error }] = useMutation(CREATE_PART_MUTATION, {
onCompleted: () => {
- toast.success('Part created')
+ toast.custom((t) => (
+
+ ))
navigate(routes.parts())
},
onError: (error) => {
- toast.error(error.message)
+ toast.custom((t) => (
+
+ ))
},
})
diff --git a/web/src/components/Part/Part/Part.tsx b/web/src/components/Part/Part/Part.tsx
index 2f3456c..43d5308 100644
--- a/web/src/components/Part/Part/Part.tsx
+++ b/web/src/components/Part/Part/Part.tsx
@@ -4,7 +4,8 @@ import { Link, routes, navigate } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
-import { timeTag } from 'src/pages/lib/formatters'
+import ToastNotification from 'src/components/ToastNotification'
+import { timeTag } from 'src/lib/formatters'
const DELETE_PART_MUTATION = gql`
mutation DeletePartMutation($id: Int!) {
@@ -21,11 +22,15 @@ interface Props {
const Part = ({ part }: Props) => {
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
onCompleted: () => {
- toast.success('Part deleted')
+ toast.custom((t) => (
+
+ ))
navigate(routes.parts())
},
onError: (error) => {
- toast.error(error.message)
+ toast.custom((t) => (
+
+ ))
},
})
diff --git a/web/src/components/Part/Parts/Parts.tsx b/web/src/components/Part/Parts/Parts.tsx
index db6d2ba..81101d7 100644
--- a/web/src/components/Part/Parts/Parts.tsx
+++ b/web/src/components/Part/Parts/Parts.tsx
@@ -5,6 +5,7 @@ import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { QUERY } from 'src/components/Part/PartsCell'
+import ToastNotification from 'src/components/ToastNotification'
import { timeTag, truncate } from 'src/lib/formatters'
const DELETE_PART_MUTATION = gql`
@@ -18,13 +19,17 @@ const DELETE_PART_MUTATION = gql`
const PartsList = ({ parts }: FindParts) => {
const [deletePart] = useMutation(DELETE_PART_MUTATION, {
onCompleted: () => {
- toast.success('Part deleted')
+ toast.custom((t) => (
+
+ ))
},
onError: (error) => {
- toast.error(error.message)
+ toast.custom((t) => (
+
+ ))
},
// This refetches the query on the list page. Read more about other ways to
- // update the cache over here:
+ // update the cache over here:ya
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
@@ -44,7 +49,7 @@ const PartsList = ({ parts }: FindParts) => {
}
return (
-
+
diff --git a/web/src/components/PartDetailsCell/PartDetailsCell.tsx b/web/src/components/PartDetailsCell/PartDetailsCell.tsx
index aa43eb8..35a4b7b 100644
--- a/web/src/components/PartDetailsCell/PartDetailsCell.tsx
+++ b/web/src/components/PartDetailsCell/PartDetailsCell.tsx
@@ -2,17 +2,24 @@ import { useState } from 'react'
import { mdiAlert, mdiPlus, mdiMinus } from '@mdi/js'
import { Icon } from '@mdi/react'
-import type { FindPartById } from 'types/graphql'
+import type { FindPartDetailsById } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
+import { toast } from '@redwoodjs/web/toast'
+
+import { addToBasket } from 'src/lib/basket'
+
+import ToastNotification from '../ToastNotification'
export const QUERY = gql`
- query FindPartById($id: Int!) {
+ query FindPartDetailsById($id: Int!) {
part: part(id: $id) {
+ id
name
description
availableStock
imageUrl
+ createdAt
}
}
`
@@ -47,7 +54,7 @@ const image = (url: string, size: number) => {
return parts.join('/')
}
-export const Success = ({ part }: CellSuccessProps) => {
+export const Success = ({ part }: CellSuccessProps) => {
const [toTake, setToTake] = useState(1)
return (
@@ -91,7 +98,31 @@ export const Success = ({ part }: CellSuccessProps) => {
-
+
diff --git a/web/src/components/PartsCell/PartsCell.tsx b/web/src/components/PartsCell/PartsCell.tsx
index 321711c..89e30cb 100644
--- a/web/src/components/PartsCell/PartsCell.tsx
+++ b/web/src/components/PartsCell/PartsCell.tsx
@@ -38,6 +38,7 @@ export const QUERY = gql`
description
availableStock
imageUrl
+ createdAt
}
count
page
diff --git a/web/src/components/PartsGridUnit/PartsGridUnit.tsx b/web/src/components/PartsGridUnit/PartsGridUnit.tsx
index 887a001..e4bd774 100644
--- a/web/src/components/PartsGridUnit/PartsGridUnit.tsx
+++ b/web/src/components/PartsGridUnit/PartsGridUnit.tsx
@@ -1,4 +1,9 @@
import { Link, routes } from '@redwoodjs/router'
+import { toast } from '@redwoodjs/web/toast'
+
+import { addToBasket } from 'src/lib/basket'
+
+import ToastNotification from '../ToastNotification'
interface Props {
part: {
@@ -7,6 +12,7 @@ interface Props {
description?: string
availableStock: number
imageUrl: string
+ createdAt: string
}
}
@@ -53,13 +59,32 @@ const PartsGridUnit = ({ part }: Props) => {
className={`btn btn-primary ${
part.availableStock == 0 ? 'btn-disabled' : ''
}`}
+ onClick={() => {
+ const newBasket = addToBasket(part, 1)
+
+ if (typeof newBasket == 'string')
+ toast.custom((t) => (
+
+ ))
+ else
+ toast.custom((t) => (
+
+ ))
+ }}
>
Add to basket
- // TODO: add to basket funcionality
)
}
diff --git a/web/src/components/ToastNotification/ToastNotification.stories.tsx b/web/src/components/ToastNotification/ToastNotification.stories.tsx
new file mode 100644
index 0000000..e795107
--- /dev/null
+++ b/web/src/components/ToastNotification/ToastNotification.stories.tsx
@@ -0,0 +1,25 @@
+// Pass props to your component by passing an `args` object to your story
+//
+// ```tsx
+// export const Primary: Story = {
+// args: {
+// propName: propValue
+// }
+// }
+// ```
+//
+// See https://storybook.js.org/docs/react/writing-stories/args.
+
+import type { Meta, StoryObj } from '@storybook/react'
+
+import ToastNotification from './ToastNotification'
+
+const meta: Meta = {
+ component: ToastNotification,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/web/src/components/ToastNotification/ToastNotification.test.tsx b/web/src/components/ToastNotification/ToastNotification.test.tsx
new file mode 100644
index 0000000..9542856
--- /dev/null
+++ b/web/src/components/ToastNotification/ToastNotification.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@redwoodjs/testing/web'
+
+import ToastNotification from './ToastNotification'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-components
+
+describe('ToastNotification', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/components/ToastNotification/ToastNotification.tsx b/web/src/components/ToastNotification/ToastNotification.tsx
new file mode 100644
index 0000000..e35dd6e
--- /dev/null
+++ b/web/src/components/ToastNotification/ToastNotification.tsx
@@ -0,0 +1,42 @@
+import { mdiCloseCircle, mdiInformation, mdiCheckCircle } from '@mdi/js'
+import { Icon } from '@mdi/react'
+
+import { Toast } from '@redwoodjs/web/toast'
+
+type NotificationType = 'success' | 'error' | 'info'
+
+interface Props {
+ type: NotificationType
+ message: string
+ toast: Toast
+}
+
+const ToastNotification = ({ type, message, toast }: Props) => (
+
+)
+
+export default ToastNotification
diff --git a/web/src/index.css b/web/src/index.css
index b5c61c9..73d6915 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,3 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+.no-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+.no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
diff --git a/web/src/index.html b/web/src/index.html
index d0db47f..a40647e 100644
--- a/web/src/index.html
+++ b/web/src/index.html
@@ -11,7 +11,7 @@
-
+
diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
index 1983f85..2e5913e 100644
--- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx
+++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx
@@ -1,11 +1,15 @@
-import { mdiChip, mdiMenu } from '@mdi/js'
+import { useState } from 'react'
+
+import { mdiChip, mdiMenu, mdiBasket } from '@mdi/js'
import Icon from '@mdi/react'
import { Link, routes } from '@redwoodjs/router'
+import { Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
+import { getBasket } from 'src/lib/basket'
type NavBarLayoutProps = {
children?: React.ReactNode
@@ -13,9 +17,11 @@ type NavBarLayoutProps = {
const NavBarLayout = ({ children }: NavBarLayoutProps) => {
const { hasRole } = useAuth()
+ const [basket] = useState(getBasket())
return (
<>
+
{
<>>
)}
+
+
+ {basket.length > 0 ? (
+
+ {basket.length}
+
+ ) : (
+ <>>
+ )}
+
+
+
+
diff --git a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
index fcc3ece..5445154 100644
--- a/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
+++ b/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx
@@ -21,7 +21,7 @@ const ScaffoldLayout = ({
}: LayoutProps) => {
return (
-
+
diff --git a/web/src/lib/basket.ts b/web/src/lib/basket.ts
new file mode 100644
index 0000000..71fd27b
--- /dev/null
+++ b/web/src/lib/basket.ts
@@ -0,0 +1,57 @@
+import { Part } from 'types/graphql'
+
+const BASKET_KEY = 'basket'
+
+export interface BasketItem {
+ part: Part
+ quantity: number
+}
+
+export const getBasket = (): BasketItem[] => {
+ const basketRaw = localStorage.getItem(BASKET_KEY)
+ const basket: BasketItem[] = basketRaw ? JSON.parse(basketRaw) : []
+
+ return basket
+}
+
+export const setBasket = (newBasket: BasketItem[]): BasketItem[] => {
+ localStorage.setItem(BASKET_KEY, JSON.stringify(newBasket))
+ return getBasket()
+}
+
+export const addToBasket = (
+ part: Part,
+ quantity: number
+): BasketItem[] | string => {
+ const basket = getBasket()
+ const existingPartIndex = basket.findIndex((item) => item.part.id == part.id)
+
+ if (existingPartIndex !== -1) {
+ const basketPart = basket[existingPartIndex]
+
+ if (basketPart.quantity + quantity <= basketPart.part.availableStock)
+ basket[existingPartIndex].quantity += quantity
+ else return `Cannot exceed number of items left (${part.availableStock})`
+ } else basket.push({ part, quantity })
+
+ localStorage.setItem(BASKET_KEY, JSON.stringify(basket))
+
+ return basket
+}
+
+export const removeFromBasket = (index: number): BasketItem[] | string => {
+ const basket = getBasket()
+
+ if (index >= 0 && index < basket.length) basket.splice(index, 1)
+ else return 'Error: index out of bounds'
+
+ localStorage.setItem(BASKET_KEY, JSON.stringify(basket))
+
+ return basket
+}
+
+export const clearBasket = (): BasketItem[] => {
+ localStorage.removeItem(BASKET_KEY)
+
+ return []
+}
diff --git a/web/src/pages/BasketPage/BasketPage.stories.tsx b/web/src/pages/BasketPage/BasketPage.stories.tsx
new file mode 100644
index 0000000..f95fafe
--- /dev/null
+++ b/web/src/pages/BasketPage/BasketPage.stories.tsx
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import BasketPage from './BasketPage'
+
+const meta: Meta
= {
+ component: BasketPage,
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {}
diff --git a/web/src/pages/BasketPage/BasketPage.test.tsx b/web/src/pages/BasketPage/BasketPage.test.tsx
new file mode 100644
index 0000000..3e46567
--- /dev/null
+++ b/web/src/pages/BasketPage/BasketPage.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@redwoodjs/testing/web'
+
+import BasketPage from './BasketPage'
+
+// Improve this test with help from the Redwood Testing Doc:
+// https://redwoodjs.com/docs/testing#testing-pages-layouts
+
+describe('BasketPage', () => {
+ it('renders successfully', () => {
+ expect(() => {
+ render()
+ }).not.toThrow()
+ })
+})
diff --git a/web/src/pages/BasketPage/BasketPage.tsx b/web/src/pages/BasketPage/BasketPage.tsx
new file mode 100644
index 0000000..89d7080
--- /dev/null
+++ b/web/src/pages/BasketPage/BasketPage.tsx
@@ -0,0 +1,172 @@
+import { useState } from 'react'
+
+import { mdiDelete, mdiMinus, mdiPlus } from '@mdi/js'
+import Icon from '@mdi/react'
+
+import { MetaTags } from '@redwoodjs/web'
+import { toast } from '@redwoodjs/web/toast'
+
+import { useAuth } from 'src/auth'
+import ToastNotification from 'src/components/ToastNotification'
+import {
+ clearBasket,
+ getBasket,
+ removeFromBasket,
+ setBasket,
+} from 'src/lib/basket'
+
+const thumbnail = (url: string) => {
+ if (url.includes('no_image.png')) return url
+ const parts = url.split('/')
+ parts.splice(3, 0, 'resize=width:160')
+ return parts.join('/')
+}
+
+const BasketPage = () => {
+ const { isAuthenticated } = useAuth()
+ const [basket, setBasketState] = useState(getBasket())
+ return (
+ <>
+
+
+
+
Basket
+
+ {basket.length > 0 ? (
+ basket.map((item, i) => (
+
+
})
+
+
+ {item.part.name}
+
+
+
+
+
+ {item.quantity}
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ It's empty in here...
+
+
+
+ )}
+ {basket.length > 0 ? (
+
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+ >
+ )
+}
+
+export default BasketPage
diff --git a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
index 8a793bd..1414713 100644
--- a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
+++ b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx
@@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
+import ToastNotification from 'src/components/ToastNotification'
const ForgotPasswordPage = () => {
const { isAuthenticated, forgotPassword } = useAuth()
@@ -25,14 +26,20 @@ const ForgotPasswordPage = () => {
const response = await forgotPassword(data.email.toLowerCase())
if (response.error) {
- toast.error(response.error)
+ toast.custom((t) => (
+
+ ))
} else {
// The function `forgotPassword.handler` in api/src/functions/auth.js has
// been invoked, let the user know how to get the link to reset their
// password (sent in email, perhaps?)
- toast.success(
- 'A link to reset your password was sent to ' + response.email
- )
+ toast.custom((t) => (
+
+ ))
navigate(routes.login())
}
}
diff --git a/web/src/pages/LoginPage/LoginPage.tsx b/web/src/pages/LoginPage/LoginPage.tsx
index bde21a3..13e474a 100644
--- a/web/src/pages/LoginPage/LoginPage.tsx
+++ b/web/src/pages/LoginPage/LoginPage.tsx
@@ -1,5 +1,4 @@
-import { useRef } from 'react'
-import { useEffect } from 'react'
+import { useRef, useEffect } from 'react'
import {
Form,
@@ -13,6 +12,7 @@ import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
+import ToastNotification from 'src/components/ToastNotification'
const LoginPage = () => {
const { isAuthenticated, logIn } = useAuth()
@@ -37,9 +37,13 @@ const LoginPage = () => {
if (response.message) {
toast(response.message)
} else if (response.error) {
- toast.error(response.error)
+ toast.custom((t) => (
+
+ ))
} else {
- toast.success('Welcome back!')
+ toast.custom((t) => (
+
+ ))
}
}
diff --git a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
index aba9856..0d9e00e 100644
--- a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
+++ b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx
@@ -6,6 +6,7 @@ import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
+import ToastNotification from 'src/components/ToastNotification'
const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
const { isAuthenticated, reauthenticate, validateResetToken, resetPassword } =
@@ -23,7 +24,9 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
const response = await validateResetToken(resetToken)
if (response.error) {
setEnabled(false)
- toast.error(response.error)
+ toast.custom((t) => (
+
+ ))
} else {
setEnabled(true)
}
@@ -43,9 +46,17 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
})
if (response.error) {
- toast.error(response.error)
+ toast.custom((t) => (
+
+ ))
} else {
- toast.success('Password changed!')
+ toast.custom((t) => (
+
+ ))
await reauthenticate()
navigate(routes.login())
}
diff --git a/web/src/pages/SignupPage/SignupPage.tsx b/web/src/pages/SignupPage/SignupPage.tsx
index 66e580f..c3527a3 100644
--- a/web/src/pages/SignupPage/SignupPage.tsx
+++ b/web/src/pages/SignupPage/SignupPage.tsx
@@ -13,6 +13,7 @@ import { MetaTags } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import { useAuth } from 'src/auth'
+import ToastNotification from 'src/components/ToastNotification'
const SignupPage = () => {
const { isAuthenticated, signUp } = useAuth()
@@ -37,14 +38,16 @@ const SignupPage = () => {
lastName: data.lastName,
})
- if (response.message) {
- toast(response.message)
- } else if (response.error) {
- toast.error(response.error)
- } else {
- // user is signed in automatically
- toast.success('Welcome!')
- }
+ if (response.message) toast(response.message)
+ else if (response.error)
+ toast.custom((t) => (
+
+ ))
+ // user is signed in automatically
+ else
+ toast.custom((t) => (
+
+ ))
}
return (
diff --git a/yarn.lock b/yarn.lock
index e2535bf..3047a69 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -19549,6 +19549,15 @@ __metadata:
languageName: node
linkType: hard
+"tailwindcss-animate@npm:^1.0.7":
+ version: 1.0.7
+ resolution: "tailwindcss-animate@npm:1.0.7"
+ peerDependencies:
+ tailwindcss: "*"
+ checksum: ec7dbd1631076b97d66a1fbaaa06e0725fccfa63119221e8d87a997b02dcede98ad88bb1ef6665b968f5d260fcefb10592e0299ca70208d365b37761edf5e19a
+ languageName: node
+ linkType: hard
+
"tailwindcss@npm:^3.1, tailwindcss@npm:^3.3.3":
version: 3.3.3
resolution: "tailwindcss@npm:3.3.3"
@@ -20946,6 +20955,7 @@ __metadata:
react: 18.2.0
react-dom: 18.2.0
tailwindcss: ^3.3.3
+ tailwindcss-animate: ^1.0.7
theme-change: ^2.5.0
languageName: unknown
linkType: soft