1
0

Part browsing

This commit is contained in:
Ahmed Al-Taiar
2023-11-04 17:32:14 -04:00
parent af93899796
commit 53e0070fd8
28 changed files with 817 additions and 62 deletions

View File

@ -8,7 +8,29 @@ export const schema = gql`
createdAt: DateTime!
}
type PartPage {
parts: [Part!]!
count: Int!
page: Int!
sort: SortMethod!
order: SortOrder!
}
enum SortMethod {
id
name
description
stock
createdAt
}
enum SortOrder {
ascending
descending
}
type Query {
partPage(page: Int, sort: SortMethod, order: SortOrder): PartPage @skipAuth
parts: [Part!]! @skipAuth
part(id: Int!): Part @skipAuth
}

View File

@ -1,8 +1,18 @@
import * as Filestack from 'filestack-js'
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
import type {
QueryResolvers,
MutationResolvers,
SortMethod,
SortOrder,
} from 'types/graphql'
import { db } from 'src/lib/db'
const PARTS_PER_PAGE = 8
const removeEnding = (input: string): string =>
input.endsWith('ending') ? input.slice(0, -6) : input
export const parts: QueryResolvers['parts'] = () => {
return db.part.findMany()
}
@ -13,6 +23,59 @@ export const part: QueryResolvers['part'] = ({ id }) => {
})
}
export const partPage = ({
page = 1,
sort = 'id',
order = 'ascending',
}: {
page: number
sort: SortMethod
order: SortOrder
}) => {
const offset = (page - 1) * PARTS_PER_PAGE
let orderByCase
switch (sort) {
case 'id':
orderByCase = { id: removeEnding(order) }
break
case 'name':
orderByCase = { name: removeEnding(order) }
break
case 'createdAt':
orderByCase = { createdAt: removeEnding(order) }
break
case 'description':
orderByCase = { description: removeEnding(order) }
break
case 'stock':
orderByCase = {
availableStock: removeEnding(order),
}
break
default:
orderByCase = { id: removeEnding(order) }
break
}
return {
parts: db.part.findMany({
take: PARTS_PER_PAGE,
skip: offset,
orderBy: orderByCase,
}),
count: db.part.count(),
page,
sort,
order,
}
}
export const createPart: MutationResolvers['createPart'] = ({ input }) => {
return db.part.create({
data: input,

View File

@ -1,12 +1,3 @@
// In this file, all Page components from 'src/pages` are auto-imported. Nested
// directories are supported, and should be uppercase. Each subdirectory will be
// prepended onto the component name.
//
// Examples:
//
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage
import { Router, Route, Set, Private } from '@redwoodjs/router'
import NavbarLayout from 'src/layouts/NavbarLayout'
@ -21,6 +12,7 @@ const Routes = () => {
<Route path="/signup" page={SignupPage} name="signup" />
<Route path="/forgot-password" page={ForgotPasswordPage} name="forgotPassword" />
<Route path="/reset-password" page={ResetPasswordPage} name="resetPassword" />
<Private unauthenticated="home" roles="admin">
<Set wrap={ScaffoldLayout} title="Parts" titleTo="parts" buttonLabel="New Part" buttonTo="newPart">
<Route path="/admin/parts/new" page={PartNewPartPage} name="newPart" />
@ -29,9 +21,12 @@ const Routes = () => {
<Route path="/admin/parts" page={PartPartsPage} name="parts" />
</Set>
</Private>
<Set wrap={NavbarLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/part/{id:Int}" page={PartPage} name="partDetails" />
</Set>
<Route notfound page={NotFoundPage} />
</Router>
)

View File

@ -11,7 +11,7 @@ interface Props {
}
const NavbarAccountIcon = ({ mobile, className }: Props) => {
const { isAuthenticated, currentUser, logOut } = useAuth()
const { isAuthenticated, currentUser, logOut, hasRole } = useAuth()
return isAuthenticated ? (
<div className={className}>
@ -21,11 +21,16 @@ 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 text-base-content" />
<Icon
path={mdiAccount}
className={`h-8 w-8 ${
hasRole('admin') ? 'text-error' : '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">
Hello, {currentUser.firstName}!
{currentUser ? `Hello, ${currentUser.firstName}!` : ``}
</p>
<button className="btn btn-ghost" type="button" onClick={logOut}>
<Icon path={mdiLogout} className="h-7 w-7 text-base-content" />

View File

@ -4,7 +4,7 @@ import { Link, routes, navigate } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { timeTag } from 'src/lib/formatters'
import { timeTag } from 'src/pages/lib/formatters'
const DELETE_PART_MUTATION = gql`
mutation DeletePartMutation($id: Int!) {

View File

@ -28,7 +28,10 @@ const PartForm = (props: PartFormProps) => {
const [imageUrl, setImageUrl] = useState(props?.part?.imageUrl)
const onSubmit = (data: FormPart) => {
const dataWithImageUrl = Object.assign(data, { imageUrl })
console.log(imageUrl)
const dataWithImageUrl = Object.assign(data, {
imageUrl: imageUrl ?? '/no_image.png',
})
props.onSave(dataWithImageUrl, props?.part?.id)
}
@ -81,6 +84,7 @@ const PartForm = (props: PartFormProps) => {
errorClassName="rw-input rw-input-error min-w-full"
validation={{ required: true, min: 0 }}
min={0}
max={2147483647}
/>
<FieldError name="availableStock" className="rw-field-error pb-3" />
@ -112,7 +116,7 @@ const PartForm = (props: PartFormProps) => {
/>
<button
onClick={() => setImageUrl(null)}
className="rw-button rw-button-blue"
className="rw-button btn-primary"
>
Replace Image
</button>

View File

@ -0,0 +1,4 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
partDetails: [{ id: 42 }, { id: 43 }, { id: 44 }],
})

View File

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Loading, Empty, Failure, Success } from './PartDetailsCell'
import { standard } from './PartDetailsCell.mock'
const meta: Meta = {
title: 'Cells/PartDetailsCell',
}
export default meta
export const loading: StoryObj<typeof Loading> = {
render: () => {
return Loading ? <Loading /> : <></>
},
}
export const empty: StoryObj<typeof Empty> = {
render: () => {
return Empty ? <Empty /> : <></>
},
}
export const failure: StoryObj<typeof Failure> = {
render: (args) => {
return Failure ? <Failure error={new Error('Oh no')} {...args} /> : <></>
},
}
export const success: StoryObj<typeof Success> = {
render: (args) => {
return Success ? <Success {...standard()} {...args} /> : <></>
},
}

View File

@ -0,0 +1,41 @@
import { render } from '@redwoodjs/testing/web'
import { Loading, Empty, Failure, Success } from './PartDetailsCell'
import { standard } from './PartDetailsCell.mock'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float and DateTime types.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-cells
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('PartDetailsCell', () => {
it('renders Loading successfully', () => {
expect(() => {
render(<Loading />)
}).not.toThrow()
})
it('renders Empty successfully', async () => {
expect(() => {
render(<Empty />)
}).not.toThrow()
})
it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).not.toThrow()
})
// When you're ready to test the actual output of your component render
// you could test that, for example, certain text is present:
//
// 1. import { screen } from '@redwoodjs/testing/web'
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
it('renders Success successfully', async () => {
expect(() => {
render(<Success partDetails={standard().partDetails} />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,99 @@
import { useState } from 'react'
import { mdiAlert, mdiPlus, mdiMinus } from '@mdi/js'
import { Icon } from '@mdi/react'
import type { FindPartById } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
export const QUERY = gql`
query FindPartById($id: Int!) {
part: part(id: $id) {
name
description
availableStock
imageUrl
}
}
`
export const Loading = () => (
<div className="flex w-auto justify-center">
<p className="loading loading-bars loading-lg" />
</div>
)
export const Empty = () => (
<div className="flex justify-center">
<div className="alert w-auto">
<p className="text-center font-inter">It&#39;s empty in here...</p>
</div>
</div>
)
export const Failure = ({ error }: CellFailureProps) => (
<div className="flex w-auto justify-center">
<div className="alert alert-error w-auto">
<Icon path={mdiAlert} className="h-6 w-6" />
<p className="font-inter">Error! {error?.message}</p>
</div>
</div>
)
const image = (url: string, size: number) => {
if (url.includes('no_image.png')) return url
const parts = url.split('/')
parts.splice(3, 0, `resize=height:${size}`)
return parts.join('/')
}
export const Success = ({ part }: CellSuccessProps<FindPartById>) => {
const [toTake, setToTake] = useState(1)
return (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
<div className="col-span-2">
<h1 className="font-inter text-4xl font-bold">{part.name}</h1>
</div>
<div className="order-first col-span-2 sm:order-2 sm:col-span-1">
<div className="-z-10 flex sm:sticky sm:top-28">
<div className="h-0 w-full bg-base-100" />
<img
alt={part.name}
src={image(part.imageUrl, 640)}
className="mb-8 w-full rounded-3xl object-cover sm:mb-0 sm:w-64 md:w-80 lg:w-96 xl:w-[32rem] 2xl:w-[40rem]"
/>
</div>
</div>
<div className="col-span-2 space-y-8 font-inter sm:col-span-1">
<p className="text-lg">{part.description}</p>
<div className="divider" />
<p className="text-xl">
<strong>Current stock:</strong> {part.availableStock}
</p>
<div className="flex space-x-5">
<div className="join">
<button
className={`btn join-item ${toTake == 1 ? 'btn-disabled' : ''}`}
onClick={() => setToTake(toTake - 1)}
>
<Icon path={mdiMinus} className="h-6 w-6" />
</button>
<p className="btn join-item items-center font-inter text-lg">
{toTake}
</p>
<button
className={`btn join-item ${
toTake == part.availableStock ? 'btn-disabled' : ''
}`}
onClick={() => setToTake(toTake + 1)}
>
<Icon path={mdiPlus} className="h-6 w-6" />
</button>
</div>
<button className="btn btn-primary">Add to basket</button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,4 @@
// Define your own mock data here:
export const standard = (/* vars, { ctx, req } */) => ({
parts: [{ id: 42 }, { id: 43 }, { id: 44 }],
})

View File

@ -0,0 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react'
import { Loading, Empty, Failure, Success } from './PartsCell'
import { standard } from './PartsCell.mock'
const meta: Meta = {
title: 'Cells/PartsCell',
}
export default meta
export const loading: StoryObj<typeof Loading> = {
render: () => {
return Loading ? <Loading /> : <></>
},
}
export const empty: StoryObj<typeof Empty> = {
render: () => {
return Empty ? <Empty /> : <></>
},
}
export const failure: StoryObj<typeof Failure> = {
render: (args) => {
return Failure ? <Failure error={new Error('Oh no')} {...args} /> : <></>
},
}
export const success: StoryObj<typeof Success> = {
render: (args) => {
return Success ? <Success {...standard()} {...args} /> : <></>
},
}

View File

@ -0,0 +1,41 @@
import { render } from '@redwoodjs/testing/web'
import { Loading, Empty, Failure, Success } from './PartsCell'
import { standard } from './PartsCell.mock'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float and DateTime types.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-cells
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('PartsCell', () => {
it('renders Loading successfully', () => {
expect(() => {
render(<Loading />)
}).not.toThrow()
})
it('renders Empty successfully', async () => {
expect(() => {
render(<Empty />)
}).not.toThrow()
})
it('renders Failure successfully', async () => {
expect(() => {
render(<Failure error={new Error('Oh no')} />)
}).not.toThrow()
})
// When you're ready to test the actual output of your component render
// you could test that, for example, certain text is present:
//
// 1. import { screen } from '@redwoodjs/testing/web'
// 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument()
it('renders Success successfully', async () => {
expect(() => {
render(<Success parts={standard().parts} />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,238 @@
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import {
mdiAlert,
mdiChevronRight,
mdiChevronLeft,
mdiChevronDoubleRight,
mdiChevronDoubleLeft,
} from '@mdi/js'
import { Icon } from '@mdi/react'
import type { PartsQuery, SortMethod, SortOrder } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
import PartsGridUnit from '../PartsGridUnit/PartsGridUnit'
const POSTS_PER_PAGE = 8
export const beforeQuery = ({ page, sort, order }) => {
page = page ? parseInt(page, 10) : 1
sort =
sort &&
(['createdAt', 'description', 'id', 'name', 'stock'].includes(sort)
? sort
: 'id')
order =
order && (['ascending', 'descending'].includes(order) ? order : 'ascending')
return { variables: { page, sort, order } }
}
export const QUERY = gql`
query PartsQuery($page: Int, $sort: SortMethod, $order: SortOrder) {
partPage(page: $page, sort: $sort, order: $order) {
parts {
id
name
description
availableStock
imageUrl
}
count
page
sort
order
}
}
`
export const Loading = () => (
<div className="flex w-auto justify-center">
<p className="loading loading-bars loading-lg" />
</div>
)
export const Empty = () => (
<div className="flex justify-center">
<div className="alert w-auto">
<p className="text-center font-inter">It&#39;s empty in here...</p>
</div>
</div>
)
export const Failure = ({ error }: CellFailureProps) => (
<div className="flex w-auto justify-center">
<div className="alert alert-error w-auto">
<Icon path={mdiAlert} className="h-6 w-6" />
<p className="font-inter">Error! {error?.message}</p>
</div>
</div>
)
export const Success = ({ partPage }: CellSuccessProps<PartsQuery>) => {
const sortMethodToText = (sortByText: string) => {
switch (sortByText as SortMethod) {
case 'createdAt':
sortByText = 'Created at'
break
case 'description':
sortByText = 'Description'
break
case 'id':
sortByText = 'ID'
break
case 'name':
sortByText = 'Name'
break
case 'stock':
sortByText = 'Stock'
break
}
return sortByText
}
const sortOrderToText = (orderText: string) => {
switch (orderText as SortOrder) {
case 'ascending':
orderText = 'Ascending'
break
case 'descending':
orderText = 'Descending'
break
}
return orderText
}
if (partPage.count == 0) return Empty()
else {
const sortByText: string = sortMethodToText(partPage.sort)
const orderText: string = sortOrderToText(partPage.order)
return (
<div className="flex flex-col items-center space-y-6">
<div className="flex space-x-3 font-inter">
<div className="dropdown">
<label tabIndex={0} className="btn m-1 normal-case">
Sort: {sortByText}
</label>
<ul
tabIndex={0}
className="dropdown-content rounded-box z-[1] mt-3 w-auto space-y-3 bg-base-100 p-3 shadow"
>
{['id', 'createdAt', 'description', 'name', 'stock'].map(
(sort) => (
<li key={sort}>
<Link
className="btn btn-ghost w-full normal-case"
to={routes.home({
page: partPage.page,
sort,
order: partPage.order,
})}
>
{sortMethodToText(sort)}
</Link>
</li>
)
)}
</ul>
</div>
<div className="dropdown">
<label tabIndex={0} className="btn m-1 normal-case">
Order: {orderText}
</label>
<ul
tabIndex={0}
className="dropdown-content rounded-box z-[1] mt-3 w-auto space-y-3 bg-base-100 p-4 shadow"
>
{['ascending', 'descending'].map((order) => (
<li key={order}>
<Link
className="btn btn-ghost w-full normal-case"
to={routes.home({
page: partPage.page,
sort: partPage.sort,
order,
})}
>
{sortOrderToText(order)}
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="grid place-items-center gap-x-6 gap-y-6 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{partPage.parts.map((part) => (
<PartsGridUnit key={part.id} part={part} />
))}
</div>
<div className="join">
<Link
className={`btn join-item ${
partPage.page == 1 ? 'btn-disabled' : ''
}`}
to={routes.home({
page: 1,
sort: partPage.sort,
order: partPage.order,
})}
>
<Icon path={mdiChevronDoubleLeft} className="h-6 w-6" />
</Link>
<Link
className={`btn join-item ${
partPage.page == 1 ? 'btn-disabled' : ''
}`}
to={routes.home({
page: partPage.page - 1,
sort: partPage.sort,
order: partPage.order,
})}
>
<Icon path={mdiChevronLeft} className="h-6 w-6" />
</Link>
<p className="btn join-item items-center font-inter normal-case">
Page {partPage.page} of {Math.ceil(partPage.count / POSTS_PER_PAGE)}
</p>
<Link
className={`btn join-item ${
partPage.page == Math.ceil(partPage.count / POSTS_PER_PAGE)
? 'btn-disabled'
: ''
}`}
to={routes.home({
page: partPage.page + 1,
sort: partPage.sort,
order: partPage.order,
})}
>
<Icon path={mdiChevronRight} className="h-6 w-6" />
</Link>
<Link
className={`btn join-item ${
partPage.page == Math.ceil(partPage.count / POSTS_PER_PAGE)
? 'btn-disabled'
: ''
}`}
to={routes.home({
page: Math.ceil(partPage.count / POSTS_PER_PAGE),
sort: partPage.sort,
order: partPage.order,
})}
>
<Icon path={mdiChevronDoubleRight} className="h-6 w-6" />
</Link>
</div>
</div>
)
}
}

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 PartsGridUnit from './PartsGridUnit'
const meta: Meta<typeof PartsGridUnit> = {
component: PartsGridUnit,
}
export default meta
type Story = StoryObj<typeof PartsGridUnit>
export const Primary: Story = {}

View File

@ -0,0 +1,14 @@
import { render } from '@redwoodjs/testing/web'
import PartsGridUnit from './PartsGridUnit'
// Improve this test with help from the Redwood Testing Doc:
// https://redwoodjs.com/docs/testing#testing-components
describe('PartsGridUnit', () => {
it('renders successfully', () => {
expect(() => {
render(<PartsGridUnit />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,66 @@
import { Link, routes } from '@redwoodjs/router'
interface Props {
part: {
id: number
name: string
description?: string
availableStock: number
imageUrl: string
}
}
const thumbnail = (url: string) => {
if (url.includes('no_image.png')) return url
const parts = url.split('/')
parts.splice(3, 0, 'resize=width:384')
return parts.join('/')
}
const PartsGridUnit = ({ part }: Props) => {
return (
<div className="card-compact card w-72 space-y-3 bg-base-100 font-inter shadow-xl sm:w-96">
<figure>
<img
className="h-48 object-cover"
src={thumbnail(part.imageUrl)}
width={384}
height={128}
alt={part.name + ' image'}
/>
</figure>
<div className="card-body">
<h2 className="card-title justify-between">
<Link
className="link-hover link w-auto overflow-hidden text-ellipsis whitespace-nowrap"
to={routes.partDetails({ id: part.id })}
>
{part.name}
</Link>
{part.availableStock == 0 ? (
<div className="badge badge-error">Out of stock</div>
) : (
<div className="badge badge-ghost whitespace-nowrap">
{part.availableStock + ' left'}
</div>
)}
</h2>
<p className="overflow-hidden text-ellipsis whitespace-nowrap">
{part.description}
</p>
<div className="card-actions justify-end">
<button
className={`btn btn-primary ${
part.availableStock == 0 ? 'btn-disabled' : ''
}`}
>
Add to basket
</button>
</div>
</div>
</div>
// TODO: add to basket funcionality
)
}
export default PartsGridUnit

View File

@ -1,13 +1,3 @@
/**
* START --- SETUP TAILWINDCSS EDIT
*
* `yarn rw setup ui tailwindcss` placed these directives here
* to inject Tailwind's styles into your CSS.
* For more information, see: https://tailwindcss.com/docs/installation
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
/**
* END --- SETUP TAILWINDCSS EDIT
*/

View File

@ -3,6 +3,7 @@ import Icon from '@mdi/react'
import { Link, routes } from '@redwoodjs/router'
import { useAuth } from 'src/auth'
import NavbarAccountIcon from 'src/components/NavbarAccountIcon/NavbarAccountIcon'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
@ -11,9 +12,11 @@ type NavBarLayoutProps = {
}
const NavBarLayout = ({ children }: NavBarLayoutProps) => {
const { hasRole } = useAuth()
return (
<>
<div className="navbar 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">
<Icon
path={mdiChip}
@ -29,16 +32,20 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
</Link>
</div>
<div className="ml-auto justify-end space-x-3">
{/* <ul className="relative hidden items-center space-x-3 lg:flex">
<li>
<Link
to={routes.faq()}
className="btn btn-ghost font-inter hover:shadow-lg"
>
FAQ
</Link>
</li>
</ul> */}
{hasRole('admin') ? (
<ul className="relative hidden items-center space-x-3 lg:flex">
<li>
<Link
to={routes.parts()}
className="btn btn-ghost font-inter hover:shadow-lg"
>
Parts
</Link>
</li>
</ul>
) : (
<></>
)}
<ThemeToggle />
<NavbarAccountIcon mobile={false} className="hidden lg:block" />
<div className="lg:hidden">
@ -75,14 +82,18 @@ const NavBarLayout = ({ children }: NavBarLayoutProps) => {
<NavbarAccountIcon mobile={true} />
</div>
</li>
{/* <li>
<Link
to={routes.faq()}
className="btn btn-ghost w-full hover:shadow-lg"
>
<p className="font-inter text-base">FAQ</p>
</Link>
</li> */}
{hasRole('admin') ? (
<li>
<Link
to={routes.parts()}
className="btn btn-ghost w-full hover:shadow-lg"
>
<p className="font-inter text-base">Parts</p>
</Link>
</li>
) : (
<></>
)}
</ul>
</div>
</div>

View File

@ -1,3 +1,6 @@
import { mdiHome } from '@mdi/js'
import Icon from '@mdi/react'
import { Link, routes } from '@redwoodjs/router'
import { Toaster } from '@redwoodjs/web/toast'
@ -20,9 +23,14 @@ const ScaffoldLayout = ({
<div className="rw-scaffold">
<Toaster toastOptions={{ className: 'rw-toast', duration: 6000 }} />
<header className="rw-header">
<h1 className="rw-heading rw-heading-primary rw-button btn-ghost normal-case">
<Link to={routes[titleTo]()}>{title}</Link>
</h1>
<div className="space-x-3">
<Link to={routes.home()} className="btn btn-ghost">
<Icon path={mdiHome} className="h-8 w-8" />
</Link>
<h1 className="rw-heading rw-heading-primary rw-button btn-ghost normal-case">
<Link to={routes[titleTo]()}>{title}</Link>
</h1>
</div>
<Link to={routes[buttonTo]()} className="rw-button btn-success">
<div className="rw-button-icon">+</div> {buttonLabel}
</Link>

View File

@ -22,7 +22,7 @@ const ForgotPasswordPage = () => {
}, [])
const onSubmit = async (data: { email: string }) => {
const response = await forgotPassword(data.email)
const response = await forgotPassword(data.email.toLowerCase())
if (response.error) {
toast.error(response.error)

View File

@ -1,19 +1,27 @@
import { Link, routes } from '@redwoodjs/router'
import { SortMethod, SortOrder } from 'types/graphql'
import { MetaTags } from '@redwoodjs/web'
const HomePage = () => {
import PartsCell from 'src/components/PartsCell'
interface Props {
page: number
sort: SortMethod
order: SortOrder
}
const HomePage = ({ page = 1, sort = 'id', order = 'ascending' }: Props) => {
return (
<>
<MetaTags title="Home" description="Home page" />
<h1>HomePage</h1>
<p>
Find me in <code>./web/src/pages/HomePage/HomePage.tsx</code>
</p>
<p>
My default route is named <code>home</code>, link to me with `
<Link to={routes.home()}>Home</Link>`
</p>
<div className="my-16 text-center font-inter md:my-24">
<h1 className="text-4xl font-extrabold tracking-wide md:text-6xl">
Arduino Parts Inventory
</h1>
<p className="pt-4 text-xl">Only take what you need</p>
</div>
<PartsCell page={page} sort={sort} order={order} />
</>
)
}

View File

@ -30,7 +30,7 @@ const LoginPage = () => {
const onSubmit = async (data: Record<string, string>) => {
const response = await logIn({
username: data.email,
username: data.email.toLowerCase(),
password: data.password,
})

View File

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

View File

@ -0,0 +1,14 @@
import { render } from '@redwoodjs/testing/web'
import PartPage from './PartPage'
// Improve this test with help from the Redwood Testing Doc:
// https://redwoodjs.com/docs/testing#testing-pages-layouts
describe('PartPage', () => {
it('renders successfully', () => {
expect(() => {
render(<PartPage />)
}).not.toThrow()
})
})

View File

@ -0,0 +1,21 @@
import { MetaTags } from '@redwoodjs/web'
import PartDetailsCell from 'src/components/PartDetailsCell'
interface Props {
id: number
}
const PartPage = ({ id }: Props) => {
return (
<>
<MetaTags title="Part" description="Part page" />
<div className="m-8">
<PartDetailsCell id={id} />
</div>
</>
)
}
export default PartPage

View File

@ -31,7 +31,7 @@ const SignupPage = () => {
const onSubmit = async (data: Record<string, string>) => {
const response = await signUp({
username: data.email,
username: data.email.toLowerCase(),
password: data.password,
firstName: data.firstName,
lastName: data.lastName,
@ -114,6 +114,7 @@ const SignupPage = () => {
message: 'Email is not valid',
},
}}
inputMode="email"
/>
<FieldError name="email" className="rw-field-error pb-3" />

View File

@ -107,7 +107,7 @@
@apply input input-bordered w-full max-w-xs font-inter text-base-content;
}
.rw-textarea {
@apply textarea textarea-bordered font-inter max-w-xs w-full text-base-content;
@apply textarea textarea-bordered text-base font-inter max-w-xs w-full text-base-content;
}
.rw-check-radio-items {
@apply flex justify-items-center;