diff --git a/components/AboutSection.tsx b/components/AboutSection.tsx deleted file mode 100644 index ded06e0..0000000 --- a/components/AboutSection.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Section } from '.' - -export const AboutSection = () => { - return ( -
-

Coming soon...

-
- ) -} diff --git a/components/Datepicker.tsx b/components/Datepicker.tsx index 773458b..69cf5ad 100644 --- a/components/Datepicker.tsx +++ b/components/Datepicker.tsx @@ -4,10 +4,10 @@ import { Popover, Transition } from '@headlessui/react' import { usePopper } from 'react-popper' import classNames from 'classnames' import { Input } from './Input' -import { OrderInputs } from './OrderSection' import { getTruncatedDateStr } from '../utils' import type { Props as DayzedProps } from 'dayzed' import type { UseFormSetValue } from 'react-hook-form' +import type { OrderInputs } from '../pages/order' import type { BaseInputProps } from './Input' type RequiredInputProps = Required> @@ -17,6 +17,8 @@ type DatepickerProps = { setValue: UseFormSetValue } & Omit< > & RequiredInputProps +// TODO: fix focus within state +// TODO: fix popper offset export const Datepicker = ({ setValue, ...props }: DatepickerProps) => { const [date, setDate] = useState() @@ -53,10 +55,15 @@ export const Datepicker = ({ setValue, ...props }: DatepickerProps) => { {...attributes.popper} className='bg-white' > - setDate(date.date)} - /> + {({ close }) => ( + { + setDate(date.date) + close() + }} + /> + )} diff --git a/components/GallerySection.tsx b/components/GallerySection.tsx deleted file mode 100644 index 857602a..0000000 --- a/components/GallerySection.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from 'react' -import { MobileGallery } from './MobileGallery' -import { DesktopGallery } from './DesktopGallery' -import { Section } from '.' - -import babyPic from '../public/posts/baby.jpg' -import baby2Pic from '../public/posts/baby2.jpg' -import birthdayPic from '../public/posts/birthday.jpg' -import carsPic from '../public/posts/cars.jpg' -import cars2Pic from '../public/posts/cars2.jpg' -import marvelPic from '../public/posts/marvel.jpg' -import potterPic from '../public/posts/potter.jpg' -import potter2Pic from '../public/posts/potter2.jpg' -import potter3Pic from '../public/posts/potter3.jpg' - -export const posts = [ - babyPic, - baby2Pic, - birthdayPic, - carsPic, - cars2Pic, - marvelPic, - potterPic, - potter2Pic, - potter3Pic -] - -export type CommonGalleryProps = { - postIndex: number - goToNextPost: () => void - goToPrevPost: () => void - atFirstPost: boolean - atLastPost: boolean -} - -export type GalleryButtonProps = { - side: 'left' | 'right' - onClick: () => void - disabled: boolean -} - -export const GallerySection = () => { - const [postIndex, setPostIndex] = useState(0) - - const atFirstPost = postIndex === 0 - const atLastPost = postIndex === posts.length - 1 - - const goToNextPost = () => setPostIndex(postIndex + 1) - const goToPrevPost = () => setPostIndex(postIndex - 1) - - const commonGalleryProps = { - postIndex, - goToNextPost, - goToPrevPost, - atFirstPost, - atLastPost - } - - return ( - - ) -} diff --git a/components/Layout.tsx b/components/Layout.tsx new file mode 100644 index 0000000..332c6a3 --- /dev/null +++ b/components/Layout.tsx @@ -0,0 +1,22 @@ +import type { ReactNode, RefObject } from 'react' +import Head from 'next/head' +import { Navigation } from './Navigation' + +type Props = { + children: ReactNode + navRef?: RefObject +} + +export const Layout = ({ children, navRef }: Props) => { + return ( + <> + + Cookies by Coffey + + + + + {children} + + ) +} diff --git a/components/Main.tsx b/components/Main.tsx new file mode 100644 index 0000000..cbfb0db --- /dev/null +++ b/components/Main.tsx @@ -0,0 +1,13 @@ +import type { ReactNode } from 'react' + +type Props = { + children: ReactNode +} + +export const Main = ({ children }: Props) => { + return ( +
+
{children}
+
+ ) +} diff --git a/components/MobileGallery.tsx b/components/MobileGallery.tsx deleted file mode 100644 index d886ec9..0000000 --- a/components/MobileGallery.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import Image from 'next/image' -import classNames from 'classnames' -import ChevronLeftIcon from '@mui/icons-material/ChevronLeftOutlined' -import ChevronRightIcon from '@mui/icons-material/ChevronRightOutlined' -import { posts } from './GallerySection' -import type { CommonGalleryProps, GalleryButtonProps } from './GallerySection' - -export const MobileGallery = ({ - postIndex, - goToNextPost, - goToPrevPost, - atFirstPost, - atLastPost -}: CommonGalleryProps) => ( -
-
- - -
- Post -
-) - -const GalleryButton = ({ side, ...props }: GalleryButtonProps) => { - const classes = classNames( - 'flex items-center text-white bg-black opacity-100 bg-opacity-70 disabled:opacity-30', - side === 'left' ? 'rounded-r-md' : 'rounded-l-md' - ) - - return ( - - ) -} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index 0c8bf75..4437605 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, forwardRef } from 'react' +import Link from 'next/link' import HomeIcon from '@mui/icons-material/HomeOutlined' -import PersonIcon from '@mui/icons-material/PersonOutlined' import PhotoLibraryIcon from '@mui/icons-material/PhotoLibraryOutlined' import EmailIcon from '@mui/icons-material/EmailOutlined' import MenuIcon from '@mui/icons-material/MenuOutlined' @@ -11,44 +11,14 @@ import { useOnClickOutside } from '../hooks' import type { Icon } from '../types' type LinkName = 'Home' | 'About' | 'Gallery' | 'Order' -type Link = { name: LinkName; icon: Icon; id?: string } +type Link = { name: LinkName; icon: Icon; id: string } const links: Link[] = [ - { name: 'Home', icon: HomeIcon }, - { name: 'About', icon: PersonIcon, id: 'about' }, - { name: 'Gallery', icon: PhotoLibraryIcon, id: 'gallery' }, - { name: 'Order', icon: EmailIcon, id: 'order' } + { name: 'Home', icon: HomeIcon, id: '/' }, + { name: 'Gallery', icon: PhotoLibraryIcon, id: '/gallery' }, + { name: 'Order', icon: EmailIcon, id: '/order' } ] -function scrollToTop() { - window.scrollTo({ top: 0, behavior: 'smooth' }) -} - -function scrollToSection(id: string) { - const element = document.querySelector(`#${id}`) - const navbar = document.querySelector('#navbar') - - if (element && navbar) { - // @ts-ignore - const navbarOffset = navbar.offsetHeight + 8 - const elementPosition = element.getBoundingClientRect().top - const offsetTop = elementPosition + window.pageYOffset - navbarOffset - - window.scrollTo({ - top: offsetTop, - behavior: 'smooth' - }) - } -} - -const handleLinkClick = (linkId?: string) => { - if (!linkId) { - scrollToTop() - } else { - scrollToSection(linkId) - } -} - export const Navigation = forwardRef((_props, ref) => { const [navVisEnabled, setNavVisEnabled] = useState(false) @@ -77,17 +47,17 @@ export const Navigation = forwardRef((_props, ref) => { ) @@ -107,10 +77,15 @@ const MobileNavButton = () => { return ( <> - - {isDrawerOpen ? : - } - + {!isDrawerOpen && ( + + + + )} { leaveFrom='opacity-100' leaveTo='opacity-0' > + {isDrawerOpen && ( + + + + )}
    {links.map((link, index) => ( -
  • { - setIsDrawerOpen(false) - handleLinkClick(link.id) - }} - className='flex items-center gap-2' - > - - {link.name} +
  • + + + + {link.name} + +
  • ))}
diff --git a/components/Page.tsx b/components/Page.tsx new file mode 100644 index 0000000..61b42aa --- /dev/null +++ b/components/Page.tsx @@ -0,0 +1,18 @@ +import { Layout, Main } from '.' +import type { ReactNode } from 'react' + +type Props = { + header: string + children: ReactNode +} + +export const Page = ({ header, children }: Props) => { + return ( + +

+ {header} +

+
{children}
+
+ ) +} diff --git a/components/Section.tsx b/components/Section.tsx deleted file mode 100644 index 5b16535..0000000 --- a/components/Section.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { ReactNode } from 'react' - -type Props = { - id: string - header: string - className?: string - children: ReactNode -} - -export const Section = ({ id, header, className, children }: Props) => { - return ( -
-

- {header} -

- {children} -
- ) -} diff --git a/components/Select.tsx b/components/Select.tsx new file mode 100644 index 0000000..1e41537 --- /dev/null +++ b/components/Select.tsx @@ -0,0 +1,113 @@ +import { useState } from 'react' +import { Popover, Transition } from '@headlessui/react' +import { usePopper } from 'react-popper' +import classNames from 'classnames' +import ArrowDownIcon from '@mui/icons-material/KeyboardArrowDownOutlined' +import type { BaseInputProps } from './Input' + +type SelectProps = { + options: TOption[] | readonly TOption[] + value: TOption + handleSelect: (selectedOption: TOption) => void +} & Omit + +// TODO: fix popper offset +export const Select = ({ + label, + required, + value, + handleSelect, + options, + Icon, + errorMsg +}: SelectProps) => { + const [referenceElement, setReferenceElement] = useState() + const [popperElement, setPopperElement] = useState() + const { styles, attributes } = usePopper(referenceElement, popperElement) + + return ( + + {({ open: isOpen }) => ( + <> + + + <> + + + {value} + + + + + + + {({ close }) => ( +
    + {options.map((option, index) => { + const selected = value === option + return ( +
  • { + handleSelect(option) + close() + }} + className={classNames( + 'flex items-center gap-2 p-2 cursor-pointer rounded-md transition duration-150 lg:hover:bg-darkprimary lg:hover:text-white', + { + ['px-10 text-black']: !selected, + ['text-darkprimary']: selected + } + )} + > + {selected && } + {option} +
  • + ) + })} +
+ )} +
+
+ {errorMsg && ( + + {errorMsg} + + )} + + )} +
+ ) +} diff --git a/components/index.ts b/components/index.ts index 41d5d95..98f00d8 100644 --- a/components/index.ts +++ b/components/index.ts @@ -1,7 +1,7 @@ -export * from './AboutSection' -export * from './GallerySection' export * from './Input' +export * from './Layout' +export * from './Main' export * from './Datepicker' export * from './Navigation' -export * from './OrderSection' -export * from './Section' +export * from './Page' +export * from './Select' diff --git a/package-lock.json b/package-lock.json index 0833808..c152f90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", - "@headlessui/react": "^1.7.3", + "@headlessui/react": "^1.7.17", "@hookform/resolvers": "^2.9.8", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.0", @@ -36,6 +36,7 @@ "eslint": "8.24.0", "eslint-config-next": "12.3.1", "postcss": "^8.4.16", + "prettier": "^3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" } @@ -349,9 +350,12 @@ } }, "node_modules/@headlessui/react": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.3.tgz", - "integrity": "sha512-LGp06SrGv7BMaIQlTs8s2G06moqkI0cb0b8stgq7KZ3xcHdH3qMP+cRyV7qe5x4XEW/IGY48BW4fLesD6NQLng==", + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", + "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "dependencies": { + "client-only": "^0.0.1" + }, "engines": { "node": ">=10" }, @@ -1603,6 +1607,11 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -3949,6 +3958,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5114,10 +5138,12 @@ } }, "@headlessui/react": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.3.tgz", - "integrity": "sha512-LGp06SrGv7BMaIQlTs8s2G06moqkI0cb0b8stgq7KZ3xcHdH3qMP+cRyV7qe5x4XEW/IGY48BW4fLesD6NQLng==", - "requires": {} + "version": "1.7.17", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", + "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "requires": { + "client-only": "^0.0.1" + } }, "@hookform/resolvers": { "version": "2.9.8", @@ -5867,6 +5893,11 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", @@ -7567,6 +7598,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true + }, "prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index 8acd76f..7858480 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", - "@headlessui/react": "^1.7.3", + "@headlessui/react": "^1.7.17", "@hookform/resolvers": "^2.9.8", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.0", @@ -37,6 +37,7 @@ "eslint": "8.24.0", "eslint-config-next": "12.3.1", "postcss": "^8.4.16", + "prettier": "^3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" } diff --git a/pages/api/sendEmail.ts b/pages/api/sendEmail.ts index 49fca00..3ea4d09 100644 --- a/pages/api/sendEmail.ts +++ b/pages/api/sendEmail.ts @@ -2,6 +2,7 @@ import nodemailer from 'nodemailer' import { google } from 'googleapis' import { getTruncatedDateStr } from '../../utils' import type { NextApiRequest, NextApiResponse } from 'next' +import type { FlavorOptionType } from '../../types' const OAuth2 = google.auth.OAuth2 @@ -15,12 +16,17 @@ oauth2Client.setCredentials({ refresh_token: process.env.REFRESH_TOKEN }) +export type CookieItem = { + count: number + flavor: FlavorOptionType +} + export type EmailRequestBody = { email: string name: string phone?: string deliveryDate: string - cookieCount: number + cookieList: CookieItem[] message: string } @@ -33,7 +39,7 @@ const sendEmail = async ( res: NextApiResponse ) => { if (req.method === 'POST') { - const { email, name, phone, deliveryDate, cookieCount, message } = + const { email, name, phone, deliveryDate, cookieList, message } = req.body as EmailRequestBody try { @@ -62,7 +68,12 @@ const sendEmail = async (

Date: ${getTruncatedDateStr( new Date(deliveryDate) )}

-

Number of Cookies: ${cookieCount}

+

Cookies:

+ ${cookieList.reduce( + (total, currentItem) => + `${total}

${currentItem.count} ${currentItem.flavor}

`, + '' + )}

Message: ${message}

` }) diff --git a/components/DesktopGallery.tsx b/pages/gallery.tsx similarity index 60% rename from components/DesktopGallery.tsx rename to pages/gallery.tsx index 0977961..a09ddd8 100644 --- a/components/DesktopGallery.tsx +++ b/pages/gallery.tsx @@ -1,35 +1,85 @@ -import { useRef, useState, forwardRef } from 'react' +import { forwardRef, useRef, useState } from 'react' import Image from 'next/image' import { Transition } from '@headlessui/react' import classNames from 'classnames' +import { Page } from '../components' +import { useOnClickOutside } from '../hooks' +import CloseIcon from '@mui/icons-material/CloseOutlined' import ChevronLeftIcon from '@mui/icons-material/ChevronLeftOutlined' import ChevronRightIcon from '@mui/icons-material/ChevronRightOutlined' -import CloseIcon from '@mui/icons-material/CloseOutlined' -import { useOnClickOutside } from '../hooks' -import { posts } from '.' -import type { CommonGalleryProps, GalleryButtonProps } from './GallerySection' +import type { NextPage } from 'next' + +import babyPic from '../public/posts/baby.jpg' +import baby2Pic from '../public/posts/baby2.jpg' +import birthdayPic from '../public/posts/birthday.jpg' +import carsPic from '../public/posts/cars.jpg' +import cars2Pic from '../public/posts/cars2.jpg' +import marvelPic from '../public/posts/marvel.jpg' +import potterPic from '../public/posts/potter.jpg' +import potter2Pic from '../public/posts/potter2.jpg' +import potter3Pic from '../public/posts/potter3.jpg' + +export const posts = [ + babyPic, + baby2Pic, + birthdayPic, + carsPic, + cars2Pic, + marvelPic, + potterPic, + potter2Pic, + potter3Pic +] + +const LARGE_PAGE_SIZE = 1024 -type DesktopGalleryProps = CommonGalleryProps & { - setPostIndex: (postIndex: number) => void +export type CommonGalleryProps = { + postIndex: number + goToNextPost: () => void + goToPrevPost: () => void + atFirstPost: boolean + atLastPost: boolean } -export const DesktopGallery = (props: DesktopGalleryProps) => { +export type GalleryButtonProps = { + side: 'left' | 'right' + onClick: () => void + disabled: boolean +} + +const Gallery: NextPage = () => { + const [postIndex, setPostIndex] = useState(0) const [showFullscreenPost, setShowFullscreenPost] = useState(false) + const atFirstPost = postIndex === 0 + const atLastPost = postIndex === posts.length - 1 + + const goToNextPost = () => setPostIndex(postIndex + 1) + const goToPrevPost = () => setPostIndex(postIndex - 1) + const onPostClick = (index: number) => { - props.setPostIndex(index) + setPostIndex(index) setShowFullscreenPost(true) } + const handlePostClick = (postIndex: number) => { + if ( + window.innerWidth > LARGE_PAGE_SIZE || + window.innerWidth === LARGE_PAGE_SIZE + ) { + onPostClick(postIndex) + } + } + return ( - <> -
+ +
{posts.map((post, index) => ( Post onPostClick(index)} + className='transition duration-150 rounded-xl lg:cursor-pointer lg:hover:opacity-70' + onClick={() => handlePostClick(index)} key={index} /> ))} @@ -45,16 +95,20 @@ export const DesktopGallery = (props: DesktopGalleryProps) => { > - + ) } type FullscreenCarouselProps = { setShowFullscreenPost: (showFullscreenPost: boolean) => void -} & Omit +} & CommonGalleryProps const FullscreenCarousel = ({ postIndex, @@ -118,11 +172,16 @@ const GalleryButton = forwardRef( return ( ) } ) GalleryButton.displayName = 'GalleryButton' + +export default Gallery diff --git a/pages/index.tsx b/pages/index.tsx index ed1b4f0..fd066ff 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,15 +1,14 @@ import { useRef, useState, useEffect } from 'react' -import Head from 'next/head' import Image from 'next/image' -import { - Navigation, - AboutSection, - GallerySection, - OrderSection -} from '../components' +import Link from 'next/link' +import ArrowIcon from '@mui/icons-material/ArrowForwardOutlined' +import PhotoLibraryIcon from '@mui/icons-material/PhotoLibraryOutlined' +import EmailIcon from '@mui/icons-material/EmailOutlined' +import { Layout, Main } from '../components' import logoPic from '../public/logo.svg' import type { NextPage } from 'next' import type { RefObject } from 'react' +import type { Icon } from '../types' const getElementHeight = (ref: RefObject) => ref.current?.offsetHeight @@ -48,26 +47,56 @@ const Home: NextPage = () => { }, []) return ( - <> - - Cookies by Coffey - - - - +
Logo
-
- - - -
- +
+
+

+ Welcome to Cookies by Coffey, your favorite custom cookie boutique! + We offer custom butter sugar and chocolate sugar cookies and are + continually experimenting with new flavors and ideas. Currently, + cookies may be picked up in the Greater St. Louis area, but we + expect to start shipping to the rest of the US soon! +

+
+ + +
+
+
+
) } +type HomePageLinkProps = { + Icon: Icon + text: string + href: string +} + +const HomePageLink = ({ Icon, text, href }: HomePageLinkProps) => ( + + + + + {text} + + + + +) + export default Home diff --git a/components/OrderSection.tsx b/pages/order.tsx similarity index 54% rename from components/OrderSection.tsx rename to pages/order.tsx index 7624a55..1cf36a0 100644 --- a/components/OrderSection.tsx +++ b/pages/order.tsx @@ -1,32 +1,37 @@ -import { useRef } from 'react' -import { useForm, SubmitHandler } from 'react-hook-form' -import { yupResolver } from '@hookform/resolvers/yup' +import { ChangeEvent, useRef, useEffect } from 'react' +import { SubmitHandler, useForm } from 'react-hook-form' import * as yup from 'yup' +import { yupResolver } from '@hookform/resolvers/yup' import PersonIcon from '@mui/icons-material/PersonOutlined' import EmailIcon from '@mui/icons-material/EmailOutlined' import LocalPhoneIcon from '@mui/icons-material/LocalPhoneOutlined' import CalendarMonthIcon from '@mui/icons-material/CalendarMonthOutlined' -import TagIcon from '@mui/icons-material/TagOutlined' +import NumbersIcon from '@mui/icons-material/NumbersOutlined' +import CookieIcon from '@mui/icons-material/CookieOutlined' +import DeleteIcon from '@mui/icons-material/DeleteOutlined' +import AddIcon from '@mui/icons-material/AddOutlined' import ChatIcon from '@mui/icons-material/ChatOutlined' import SendIcon from '@mui/icons-material/SendOutlined' import SyncIcon from '@mui/icons-material/SyncOutlined' import WarningIcon from '@mui/icons-material/WarningOutlined' import CheckIcon from '@mui/icons-material/CheckOutlined' -import { Input, Section, Datepicker } from '.' -import { maskPhone, removePhoneMask, httpPost } from '../utils' -import type { ChangeEvent } from 'react' +import { Page, Input, Datepicker, Select } from '../components' +import { httpPost, maskPhone, removePhoneMask } from '../utils' +import { FlavorOptions } from '../types' import type { + CookieItem, EmailRequestBody, EmailResponseBody -} from '../pages/api/sendEmail' -import type { Icon } from '../types' +} from './api/sendEmail' +import type { NextPage } from 'next' +import type { Icon, FlavorOptionType } from '../types' export type OrderInputs = { name: string email: string phone: string deliveryDate: string - cookieCount: number + cookieList: CookieItem[] message: string } @@ -54,12 +59,24 @@ const schema = yup.object({ .typeError('Please enter a delivery date.') .min(new Date(), 'Delivery date must be in the future.') .required('Please enter a delivery date.'), - cookieCount: yup - .number() - .typeError('Please enter how many cookies you would like.') - .min(1, 'Please request at least one cookie.') - .max(500, 'The number of cookies entered is too high.') - .required('Please enter how many cookies you would like.'), + cookieList: yup + .array() + .ensure() + .min(1, 'Cookie details are required to submit an order.') + .of( + yup.object({ + flavor: yup + .string() + .max(75, 'The flavor entered is too long.') + .required('Please enter a flavor.'), + count: yup + .number() + .typeError('Please enter an amount.') + .min(1, 'Please enter an amount.') + .max(500, 'That amount is too high.') + .required('Please enter an amount.') + }) + ), message: yup .string() .max(1000, 'The message entered is too long.') @@ -67,12 +84,10 @@ const schema = yup.object({ }) const EmptyGridSpace = () =>
+// @ts-ignore +const newFlavor: CookieItem = { count: null, flavor: '' } -type Props = { - className?: string -} - -export const OrderSection = ({ className }: Props) => { +const Order: NextPage = () => { const formRef = useRef(null) const { @@ -84,6 +99,30 @@ export const OrderSection = ({ className }: Props) => { reset } = useForm({ mode: 'onSubmit', resolver: yupResolver(schema) }) + const cookieListWatch = watch('cookieList') + const phoneWatch = watch('phone') + + const addFlavor = () => { + setValue( + 'cookieList', + cookieListWatch ? [...cookieListWatch, newFlavor] : [newFlavor] + ) + } + + const removeFlavor = (index: number) => { + setValue( + 'cookieList', + cookieListWatch.filter((_, i) => index !== i) + ) + } + + const handleFlavorSelect = ( + cookieItemIndex: number, + newValue: FlavorOptionType + ) => { + setValue(`cookieList.${cookieItemIndex}.flavor`, newValue) + } + const onSubmit: SubmitHandler = async data => { await httpPost('api/sendEmail', data) } @@ -92,13 +131,16 @@ export const OrderSection = ({ className }: Props) => { setTimeout(reset, 3000) } - const phoneWatch = watch('phone') + useEffect(() => { + setValue('cookieList', [newFlavor]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) return ( -
+
{ } }} /> - { setValue={setValue} required /> -
{ required />
+
+

Order Details*

+ {errors.cookieList?.message && cookieListWatch?.length < 1 && ( + + {errors.cookieList.message} + + )} + {cookieListWatch?.map((_, itemIndex) => ( +
+
+ +
+ +
+ ))} +
+ +
+
-
+ ) } @@ -171,9 +265,9 @@ type OrderButtonProps = { } const OrderButton = ({ - isSubmitting, - isSubmitSuccessful, - isSubmitted + isSubmitting, + isSubmitSuccessful, + isSubmitted }: OrderButtonProps) => { let btnContent = ButtonContentMap['default'] @@ -210,3 +304,5 @@ const ButtonContentMap: Record< error: { Icon: WarningIcon, text: 'Please Try Again.' }, success: { Icon: CheckIcon, text: 'Order Received!' } } + +export default Order diff --git a/types.ts b/types.ts index 99dc198..efb4760 100644 --- a/types.ts +++ b/types.ts @@ -2,3 +2,18 @@ import type { OverridableComponent } from '@mui/types' import type { SvgIconTypeMap } from '@mui/material' export type Icon = OverridableComponent & { muiName: string } + +const LimitedTimeOptions = [ + 'Pumpkin Spice', + 'Snickerdoodle', + 'Hot Cocoa', + 'Gingerbread' +] as const +export const FlavorOptions = [ + 'Original', + 'Organic Vanilla', + 'Chocolate', + 'Chocolate Chip', + ...LimitedTimeOptions +] as const +export type FlavorOptionType = (typeof FlavorOptions)[number]