├── public ├── favicon.ico ├── images │ ├── 65341 │ │ ├── CheeseSlice.webp │ │ ├── GarlicKnots.webp │ │ ├── GrandmaSlice.webp │ │ └── MargheritaSlice.webp │ ├── 98441 │ │ ├── VadaPav.webp │ │ ├── PaniPuri.webp │ │ ├── PapdiChaat.webp │ │ ├── MiniSamosas.webp │ │ └── PaneerTikkaMasala.webp │ ├── 120985 │ │ ├── Papito.webp │ │ ├── YucaFrita.webp │ │ ├── BajaFishTaco.webp │ │ ├── ChicharronPollo.webp │ │ └── MangoPassionfruitAgua.webp │ ├── 334624 │ │ ├── YumSumO.webp │ │ ├── YumTawai.webp │ │ ├── MassamanGae.webp │ │ └── PorkSquidJowl.webp │ ├── 1243431 │ │ ├── 12course.webp │ │ ├── sakePairing.webp │ │ ├── BuckwheatCustard.webp │ │ └── ChickenSkinCrisps.webp │ ├── 7498723 │ │ ├── GarlicFries.webp │ │ ├── SuperBurger.webp │ │ ├── TruffleBurger.webp │ │ ├── VeggieBurger.webp │ │ └── FriedChickenSandwich.webp │ ├── 12356667 │ │ ├── KaiJiew.webp │ │ ├── KaoSoi.webp │ │ ├── GaiGraPao.webp │ │ ├── MussamunNeur.webp │ │ ├── ThaiIcedTea.webp │ │ └── GuayTiowKuaGai.webp │ ├── 18764431 │ │ ├── SaagPaneer.webp │ │ ├── BananaTurmericLassi.webp │ │ ├── CardamomMangoLassi.webp │ │ ├── CayenneTamarindLassi.webp │ │ ├── ChicknTikkaStreetWrap.webp │ │ └── PepperCornBerryLassi.webp │ ├── 986234892 │ │ ├── Matcha.webp │ │ ├── CaffeLatte.webp │ │ ├── PoundCake.webp │ │ ├── OvernightOats.webp │ │ └── NewOrleansIcedCoffee.webp │ ├── Blur.webp │ ├── Sushi.webp │ ├── batch_webp.sh~ │ ├── NariThai.webp │ ├── CurryUpNow.webp │ ├── DosaByDosa.webp │ ├── RosasPizza.webp │ ├── SuperDuper.webp │ ├── CholitaLinda.webp │ ├── BirdAndBuffalo.webp │ ├── BlueBottleCoffee.webp │ └── batch_webp.sh ├── fonts │ └── TTNorms │ │ ├── TTNorms-Bold.woff2 │ │ ├── TTNorms-Medium.woff2 │ │ ├── TTNorms-Regular.woff2 │ │ └── TTNorms-ExtraBold.woff2 ├── vercel.svg ├── thirteen.svg └── next.svg ├── README-supporting └── StateManagement.png ├── styles ├── StoreLayout.module.css ├── Home.module.css ├── globals.css └── GlobalStyles.tsx ├── app-redux ├── hooks.tsx ├── features │ ├── delivery │ │ └── deliverySlice.tsx │ ├── item │ │ └── itemSlice.tsx │ ├── data │ │ └── dataSlice.tsx │ └── cart │ │ └── cartSlice.tsx └── store.tsx ├── components ├── Layouts │ ├── HomeLayout.tsx │ └── StoreLayout.tsx ├── RestaurantCarousel │ ├── CarouselTitle.tsx │ ├── RestaurantCarousel.tsx │ └── RestaurantCard │ │ └── RestaurantCard.tsx ├── Icons │ ├── MinusIcon.tsx │ ├── PlusIcon.tsx │ ├── GarbageCanIcon.tsx │ ├── XIcon.tsx │ ├── MagnifyingGlassIcon.tsx │ ├── CarrotRightIcon.tsx │ ├── CarrotDownIcon.tsx │ ├── BackArrowIcon.tsx │ ├── InformationIcon.tsx │ ├── DashPassLabel.tsx │ ├── ClockIcon.tsx │ ├── MinusCircleIcon.tsx │ ├── CouponIcon.tsx │ ├── PlusCircleIcon.tsx │ ├── StarIcon.tsx │ ├── HeartIcon.tsx │ ├── ThumbsUpIcon.tsx │ ├── CartIcon.tsx │ ├── GroupOfPeopleIcon.tsx │ └── DashPassIcon.tsx ├── StoreComponents │ ├── QuickActionsComponent │ │ └── QuickActions.tsx │ ├── CheckoutButton │ │ └── CheckoutButton.tsx │ ├── CartItem │ │ └── CartItem.tsx │ ├── ItemOverlay │ │ ├── ItemOverlay.tsx │ │ └── ItemCustomizationPanel │ │ │ ├── ModalInputStepper │ │ │ └── ModalInputStepper.tsx │ │ │ └── ItemCustomizationPanel.tsx │ ├── HeroComponent │ │ ├── DeliveryTile │ │ │ └── DeliveryTile.tsx │ │ ├── AuxOptions │ │ │ └── AuxOptions.tsx │ │ └── HeroComponent.tsx │ ├── MenuSection │ │ └── MenuSection.tsx │ ├── InputStepper │ │ └── InputStepper.tsx │ ├── MenuItem │ │ └── MenuItem.tsx │ └── CartOverviewComponent │ │ └── CartOverview.tsx ├── Navigation │ ├── AddressButtonToggle.tsx │ ├── HamburgerButton.tsx │ ├── ShoppingCartButton.tsx │ ├── HomeLogoLink.tsx │ ├── SearchBar.tsx │ └── Navbar.tsx ├── Placeholders │ └── Shimmer.tsx ├── CartSheet │ ├── CartSheetBackground.tsx │ └── CartSheet.tsx ├── FilterButtonRow │ ├── FilterButton.tsx │ └── FilterButtonRow.tsx ├── GithubBadge │ └── GithubBadge.tsx ├── data.tsx └── SVGgraphics │ └── TableOfFood.tsx ├── next.config.js ├── hooks └── useDebounce.tsx ├── .gitignore ├── tsconfig.json ├── pages ├── api │ └── hello.ts ├── pickup.tsx ├── index.tsx ├── _document.tsx ├── store │ └── [slug].tsx └── _app.tsx ├── package.json ├── global.d.ts └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/Blur.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/Blur.webp -------------------------------------------------------------------------------- /public/images/Sushi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/Sushi.webp -------------------------------------------------------------------------------- /public/images/batch_webp.sh~: -------------------------------------------------------------------------------- 1 | for file in *.png 2 | do cwebp -q 50 "$file" -o "${file%.png}.webp" 3 | done 4 | -------------------------------------------------------------------------------- /public/images/NariThai.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/NariThai.webp -------------------------------------------------------------------------------- /public/images/CurryUpNow.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/CurryUpNow.webp -------------------------------------------------------------------------------- /public/images/DosaByDosa.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/DosaByDosa.webp -------------------------------------------------------------------------------- /public/images/RosasPizza.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/RosasPizza.webp -------------------------------------------------------------------------------- /public/images/SuperDuper.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/SuperDuper.webp -------------------------------------------------------------------------------- /public/images/120985/Papito.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/120985/Papito.webp -------------------------------------------------------------------------------- /public/images/98441/VadaPav.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/98441/VadaPav.webp -------------------------------------------------------------------------------- /public/images/CholitaLinda.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/CholitaLinda.webp -------------------------------------------------------------------------------- /public/images/120985/YucaFrita.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/120985/YucaFrita.webp -------------------------------------------------------------------------------- /public/images/12356667/KaiJiew.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/KaiJiew.webp -------------------------------------------------------------------------------- /public/images/12356667/KaoSoi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/KaoSoi.webp -------------------------------------------------------------------------------- /public/images/1243431/12course.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/1243431/12course.webp -------------------------------------------------------------------------------- /public/images/334624/YumSumO.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/334624/YumSumO.webp -------------------------------------------------------------------------------- /public/images/334624/YumTawai.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/334624/YumTawai.webp -------------------------------------------------------------------------------- /public/images/98441/PaniPuri.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/98441/PaniPuri.webp -------------------------------------------------------------------------------- /public/images/98441/PapdiChaat.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/98441/PapdiChaat.webp -------------------------------------------------------------------------------- /public/images/986234892/Matcha.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/986234892/Matcha.webp -------------------------------------------------------------------------------- /public/images/BirdAndBuffalo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/BirdAndBuffalo.webp -------------------------------------------------------------------------------- /public/images/BlueBottleCoffee.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/BlueBottleCoffee.webp -------------------------------------------------------------------------------- /README-supporting/StateManagement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/README-supporting/StateManagement.png -------------------------------------------------------------------------------- /public/images/12356667/GaiGraPao.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/GaiGraPao.webp -------------------------------------------------------------------------------- /public/images/334624/MassamanGae.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/334624/MassamanGae.webp -------------------------------------------------------------------------------- /public/images/65341/CheeseSlice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/65341/CheeseSlice.webp -------------------------------------------------------------------------------- /public/images/65341/GarlicKnots.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/65341/GarlicKnots.webp -------------------------------------------------------------------------------- /public/images/65341/GrandmaSlice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/65341/GrandmaSlice.webp -------------------------------------------------------------------------------- /public/images/98441/MiniSamosas.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/98441/MiniSamosas.webp -------------------------------------------------------------------------------- /public/fonts/TTNorms/TTNorms-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/fonts/TTNorms/TTNorms-Bold.woff2 -------------------------------------------------------------------------------- /public/images/120985/BajaFishTaco.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/120985/BajaFishTaco.webp -------------------------------------------------------------------------------- /public/images/12356667/MussamunNeur.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/MussamunNeur.webp -------------------------------------------------------------------------------- /public/images/12356667/ThaiIcedTea.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/ThaiIcedTea.webp -------------------------------------------------------------------------------- /public/images/1243431/sakePairing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/1243431/sakePairing.webp -------------------------------------------------------------------------------- /public/images/18764431/SaagPaneer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/SaagPaneer.webp -------------------------------------------------------------------------------- /public/images/334624/PorkSquidJowl.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/334624/PorkSquidJowl.webp -------------------------------------------------------------------------------- /public/images/65341/MargheritaSlice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/65341/MargheritaSlice.webp -------------------------------------------------------------------------------- /public/images/7498723/GarlicFries.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/7498723/GarlicFries.webp -------------------------------------------------------------------------------- /public/images/7498723/SuperBurger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/7498723/SuperBurger.webp -------------------------------------------------------------------------------- /public/images/7498723/TruffleBurger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/7498723/TruffleBurger.webp -------------------------------------------------------------------------------- /public/images/7498723/VeggieBurger.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/7498723/VeggieBurger.webp -------------------------------------------------------------------------------- /public/images/986234892/CaffeLatte.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/986234892/CaffeLatte.webp -------------------------------------------------------------------------------- /public/images/986234892/PoundCake.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/986234892/PoundCake.webp -------------------------------------------------------------------------------- /public/fonts/TTNorms/TTNorms-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/fonts/TTNorms/TTNorms-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/TTNorms/TTNorms-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/fonts/TTNorms/TTNorms-Regular.woff2 -------------------------------------------------------------------------------- /public/images/120985/ChicharronPollo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/120985/ChicharronPollo.webp -------------------------------------------------------------------------------- /public/images/12356667/GuayTiowKuaGai.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/12356667/GuayTiowKuaGai.webp -------------------------------------------------------------------------------- /public/images/98441/PaneerTikkaMasala.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/98441/PaneerTikkaMasala.webp -------------------------------------------------------------------------------- /public/images/986234892/OvernightOats.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/986234892/OvernightOats.webp -------------------------------------------------------------------------------- /public/fonts/TTNorms/TTNorms-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/fonts/TTNorms/TTNorms-ExtraBold.woff2 -------------------------------------------------------------------------------- /public/images/1243431/BuckwheatCustard.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/1243431/BuckwheatCustard.webp -------------------------------------------------------------------------------- /public/images/1243431/ChickenSkinCrisps.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/1243431/ChickenSkinCrisps.webp -------------------------------------------------------------------------------- /styles/StoreLayout.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | justify-content: center; 4 | min-height: 100vh; 5 | margin-top: 64px; 6 | } -------------------------------------------------------------------------------- /public/images/120985/MangoPassionfruitAgua.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/120985/MangoPassionfruitAgua.webp -------------------------------------------------------------------------------- /public/images/18764431/BananaTurmericLassi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/BananaTurmericLassi.webp -------------------------------------------------------------------------------- /public/images/18764431/CardamomMangoLassi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/CardamomMangoLassi.webp -------------------------------------------------------------------------------- /public/images/7498723/FriedChickenSandwich.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/7498723/FriedChickenSandwich.webp -------------------------------------------------------------------------------- /public/images/18764431/CayenneTamarindLassi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/CayenneTamarindLassi.webp -------------------------------------------------------------------------------- /public/images/18764431/ChicknTikkaStreetWrap.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/ChicknTikkaStreetWrap.webp -------------------------------------------------------------------------------- /public/images/18764431/PepperCornBerryLassi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/18764431/PepperCornBerryLassi.webp -------------------------------------------------------------------------------- /public/images/986234892/NewOrleansIcedCoffee.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theericzhang/doordash-clone/HEAD/public/images/986234892/NewOrleansIcedCoffee.webp -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | min-height: 100vh; 6 | margin-top: 64px; 7 | } -------------------------------------------------------------------------------- /app-redux/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import { RootState, AppDispatch } from './store'; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; 6 | -------------------------------------------------------------------------------- /components/Layouts/HomeLayout.tsx: -------------------------------------------------------------------------------- 1 | import FilterButtonRow from '../FilterButtonRow/FilterButtonRow'; 2 | 3 | type THomeLayout = { 4 | children: JSX.Element; 5 | }; 6 | 7 | export default function HomeLayout({ children }: THomeLayout) { 8 | return ( 9 | <> 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | compiler: { 5 | styledComponents: true 6 | }, 7 | images: { 8 | formats: [ 9 | // consider avif for future iteration 10 | // 'image/avif', 11 | 'image/webp' 12 | ], 13 | }, 14 | } 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /public/images/batch_webp.sh: -------------------------------------------------------------------------------- 1 | # batch_webp.sh 2 | for file in ./*.webp; do 3 | cwebp "$file" -q 40 -o "${file%}.redo.webp"; 4 | done 5 | 6 | for file in ./*.png; do 7 | cwebp "$file" -q 40 -o "${file%}.redo.webp"; 8 | done 9 | 10 | for file in ./*.jpeg; do 11 | cwebp "$file" -q 40 -o "${file%}.redo.webp"; 12 | done 13 | 14 | for file in ./*.JPEG; do 15 | cwebp "$file" -q 40 -o "${file%}.redo.webp"; 16 | done 17 | 18 | 19 | -------------------------------------------------------------------------------- /hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export default function useDebounce(value: any, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(); 5 | 6 | useEffect(() => { 7 | const debounceTimer = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(debounceTimer); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | -------------------------------------------------------------------------------- /components/RestaurantCarousel/CarouselTitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const CarouselTitleHeader = styled.h2` 4 | font-size: 24px; 5 | font-weight: 500; 6 | color: var(--primary-black); 7 | `; 8 | 9 | type TCarouselTitle = { 10 | carouselName: string; 11 | }; 12 | 13 | export default function CarouselTitle({ carouselName }: TCarouselTitle) { 14 | return ( 15 | 16 | {carouselName} 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/Icons/MinusIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function Minus() { 2 | return ( 3 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # eslint config 39 | .eslintrc.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /app-redux/features/delivery/deliverySlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | interface IDeliveryState { 4 | isDelivery: boolean; 5 | } 6 | 7 | const initialState: IDeliveryState = { 8 | isDelivery: true, 9 | }; 10 | 11 | const deliverySlice = createSlice({ 12 | name: 'delivery', 13 | initialState, 14 | reducers: { 15 | toggleDeliveryState: (state) => { 16 | state.isDelivery = !state.isDelivery; 17 | } 18 | } 19 | }); 20 | 21 | export const { toggleDeliveryState } = deliverySlice.actions; 22 | export default deliverySlice.reducer; 23 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | import { restaurantList, restaurantCarousels } from '../../components/datav2'; 4 | 5 | const restaurantListData = restaurantList; 6 | const restaurantCarouselsData = restaurantCarousels; 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | // future: use req.body to specify a restaurant to fetch in the future? 13 | res.status(200).json({ restaurantListData, restaurantCarouselsData }); 14 | } 15 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function Plus() { 2 | return ( 3 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app-redux/store.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import cartReducers from './features/cart/cartSlice'; 3 | import deliveryReducers from './features/delivery/deliverySlice'; 4 | import itemReducers from './features/item/itemSlice'; 5 | import dataReducers from './features/data/dataSlice'; 6 | 7 | export const store = configureStore({ 8 | reducer: { 9 | // Slice and reducer, slice: reducer 10 | cartSlice: cartReducers, 11 | deliverySlice: deliveryReducers, 12 | itemSlice: itemReducers, 13 | dataSlice: dataReducers, 14 | } 15 | }); 16 | 17 | export type AppDispatch = typeof store.dispatch; 18 | export type RootState = ReturnType; 19 | -------------------------------------------------------------------------------- /pages/pickup.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import styles from '../styles/Home.module.css'; 3 | 4 | export default function Pickup() { 5 | return ( 6 | <> 7 | 8 | Create Next App 9 | 13 | 17 | 18 | 19 |
20 | Hi! More pages 21 |
22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/StoreComponents/QuickActionsComponent/QuickActions.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | import styled from 'styled-components'; 3 | import MenuSection from '../MenuSection/MenuSection'; 4 | 5 | const QuickActionsWrapper = styled.section` 6 | display: flex; 7 | flex-direction: column; 8 | max-width: 928px; 9 | width: 100%; 10 | margin: 0 auto; 11 | position: relative; 12 | row-gap: 50px; 13 | 14 | @media screen and (max-width: 1300px) { 15 | padding: 0 16px; 16 | margin: 0; 17 | max-width: 100%; 18 | } 19 | 20 | @media screen and (max-width: 1185px) { 21 | width: calc(928px + 16px * 2); 22 | } 23 | `; 24 | 25 | export default function QuickActions() { 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/Icons/GarbageCanIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function GarbageCan() { 2 | return ( 3 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/Icons/XIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function X() { 2 | return ( 3 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Icons/MagnifyingGlassIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function MagnifyingGlass() { 2 | return ( 3 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/Icons/CarrotRightIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CarrotRight() { 2 | return ( 3 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app-redux/features/item/itemSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { TStoreItem } from '../../../global'; 3 | 4 | interface IItemState { 5 | isModalOpen: boolean; 6 | itemData: TStoreItem; 7 | } 8 | 9 | const initialState: IItemState = { 10 | isModalOpen: false, 11 | itemData: {} as TStoreItem, 12 | }; 13 | 14 | const itemSlice = createSlice({ 15 | name: 'item', 16 | initialState, 17 | reducers: { 18 | toggleIsModalOpen: (state) => { 19 | state.isModalOpen = !state.isModalOpen; 20 | }, 21 | setIsModalOpenFalse: (state) => { 22 | state.isModalOpen = false; 23 | }, 24 | setModalData: (state, action: PayloadAction) => { 25 | state.itemData = action.payload; 26 | } 27 | } 28 | }); 29 | 30 | export const { toggleIsModalOpen, setIsModalOpenFalse, setModalData } = itemSlice.actions; 31 | export default itemSlice.reducer; 32 | -------------------------------------------------------------------------------- /components/Icons/CarrotDownIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CarrotDown() { 2 | return ( 3 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/Icons/BackArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function BackArrow() { 2 | return ( 3 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Icons/InformationIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function Information() { 2 | return ( 3 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app-redux/features/data/dataSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import type { TRestaurantList, TRestaurantCarousels } from '../../../global'; 3 | 4 | const initialState = { 5 | restaurantListData: {}, 6 | restaurantCarousels: {}, 7 | }; 8 | 9 | const dataSlice = createSlice({ 10 | name: 'restaurantData', 11 | initialState, 12 | // todo: validate restarantListData and restaurantCarousels. also validate global import types 13 | reducers: { 14 | setRestaurantListData: (state, action: PayloadAction) => { 15 | state.restaurantListData = action.payload; 16 | console.log(state.restaurantListData); 17 | }, 18 | setRestaurantCarouselData: (state, action: PayloadAction) => { 19 | state.restaurantCarousels = action.payload; 20 | console.log(state.restaurantCarousels); 21 | } 22 | } 23 | }); 24 | 25 | export const { setRestaurantListData, setRestaurantCarouselData } = dataSlice.actions; 26 | export default dataSlice.reducer; 27 | -------------------------------------------------------------------------------- /components/Icons/DashPassLabel.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import DashPassIcon from './DashPassIcon'; 3 | 4 | type TDashPass = { 5 | isFull: boolean; 6 | }; 7 | 8 | const DashPassLabelWrapper = styled.div` 9 | display: flex; 10 | justify-content: flex-start; 11 | align-items: center; 12 | column-gap: 2px; 13 | `; 14 | 15 | const DashPassLabelSpan = styled.span` 16 | font-weight: 500; 17 | font-size: 14px; 18 | color: var(--primary-teal); 19 | `; 20 | 21 | export default function DashPassLabel({ isFull }: TDashPass) { 22 | return ( 23 | isFull ? 24 | 25 | 29 | 30 | DashPass 31 | 32 | 33 | : 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Navigation/AddressButtonToggle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React from 'react'; 3 | 4 | const NavbarButtonAddress = styled.button` 5 | padding: 10px 0; 6 | margin: 0 14px; 7 | border: none; 8 | background-color: transparent; 9 | 10 | &:hover { 11 | cursor: pointer; 12 | } 13 | 14 | @media screen and (max-width: 960px) { 15 | display: none; 16 | } 17 | `; 18 | 19 | const NavbarLabel = styled.h4` 20 | font-family: var(--primary-font-family); 21 | font-size: var(--nav-label-font-size); 22 | font-weight: var(--nav-label-font-weight); 23 | `; 24 | 25 | type TAddressButtonToggle = { 26 | setIsAddressButtonToggled: React.Dispatch>; 27 | }; 28 | 29 | export default function AddressButtonToggle({ setIsAddressButtonToggled } : TAddressButtonToggle) { 30 | return ( 31 | setIsAddressButtonToggled((prevToggleState) => !prevToggleState)} 33 | > 34 | 23 Maple Dr 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /components/Icons/ClockIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function ClockIcon() { 2 | return ( 3 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/Icons/MinusCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | type TMinusCircle = { 2 | isEnabled: boolean; 3 | }; 4 | 5 | export default function MinusCircle({ isEnabled }: TMinusCircle) { 6 | return ( 7 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/Placeholders/Shimmer.tsx: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | const ShimmerKeyframes = ({ width }: TShimmer) => keyframes` 4 | 0% { 5 | background-position: -${width}px; 6 | } 7 | 8 | 100% { 9 | background-position: ${width}px; 10 | } 11 | `; 12 | 13 | type TShimmer = { 14 | width: number; 15 | }; 16 | 17 | const ShimmerPlaceholder = styled.div` 18 | background-color: #F0F3F4; 19 | background: #f6f7f8; 20 | background-image: linear-gradient(to right, var(--secondary-gray) 0%, var(--primary-gray) 40%, var(--secondary-gray) 80%, var(--secondary-gray) 100%); 21 | background-repeat: no-repeat; 22 | background-size: 100%; 23 | display: inline-block; 24 | position: relative; 25 | height: 100%; 26 | width: 100%; 27 | z-index: 2; 28 | 29 | /* animation parameters */ 30 | animation-duration: 0.7s; 31 | animation-fill-mode: forwards; 32 | animation-iteration-count: infinite; 33 | animation-name: ${ShimmerKeyframes}; 34 | animation-timing-function: linear; 35 | `; 36 | 37 | export default function Shimmer({ width }: TShimmer) { 38 | return ( 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doordash-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "^13.1.1", 13 | "@reduxjs/toolkit": "^1.9.1", 14 | "@types/node": "18.11.18", 15 | "@types/react": "18.0.26", 16 | "@types/react-dom": "18.0.10", 17 | "eslint-config-next": "13.1.1", 18 | "focus-trap-react": "^10.0.2", 19 | "next": "^13.1.2", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-redux": "^8.0.5", 23 | "react-transition-group": "^4.4.5", 24 | "styled-components": "^5.3.6", 25 | "typescript": "4.9.4" 26 | }, 27 | "devDependencies": { 28 | "@next/eslint-plugin-next": "^13.1.5", 29 | "@types/react-transition-group": "^4.4.5", 30 | "@types/styled-components": "^5.1.26", 31 | "@typescript-eslint/eslint-plugin": "^5.49.0", 32 | "@typescript-eslint/parser": "^5.49.0", 33 | "eslint": "^8.32.0", 34 | "eslint-config-airbnb": "^19.0.4", 35 | "eslint-config-airbnb-typescript": "^17.0.0", 36 | "eslint-plugin-jsx-a11y": "^6.7.1", 37 | "eslint-plugin-styled-components-a11y": "^0.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /components/Icons/CouponIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function Coupon() { 2 | return ( 3 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | max-width: 100vw; 10 | overflow-x: clip; 11 | } 12 | 13 | button { 14 | border: none; 15 | padding: none; 16 | } 17 | 18 | a { 19 | color: inherit; 20 | text-decoration: none; 21 | } 22 | 23 | /* System Font imports */ 24 | 25 | @font-face { 26 | font-family: TTNorms; 27 | font-style: normal; 28 | font-weight: 600; 29 | src: url("/fonts/TTNorms/TTNorms-ExtraBold.woff2") format("woff2"); 30 | font-display: block; 31 | } 32 | 33 | @font-face { 34 | font-family: TTNorms; 35 | font-style: normal; 36 | font-weight: 500; 37 | src: url("/fonts/TTNorms/TTNorms-Bold.woff2") format("woff2"); 38 | font-display: block; 39 | } 40 | 41 | @font-face { 42 | font-family: TTNorms; 43 | font-style: normal; 44 | font-weight: 400; 45 | src: url("/fonts/TTNorms/TTNorms-Medium.woff2") format("woff2"); 46 | font-display: block; 47 | } 48 | 49 | @font-face { 50 | font-family: TTNorms; 51 | font-style: normal; 52 | font-weight: 300; 53 | src: url("/fonts/TTNorms/TTNorms-Regular.woff2") format("woff2"); 54 | font-display: block; 55 | } 56 | 57 | /* @media (prefers-color-scheme: dark) { 58 | html { 59 | color-scheme: dark; 60 | } 61 | } */ 62 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | export type TRestaurantList = { 2 | [key: number]: { 3 | restaurantData: TRestaurantDataPrimary; 4 | storefrontData: TStorefrontData; 5 | }, 6 | }; 7 | 8 | export type TRestaurantCarousels = { 9 | restaurantCarouselsData: { 10 | carouselName: string; 11 | selectedRestaurantIDs: number[]; 12 | }[]; 13 | }; 14 | 15 | export type TRestaurantDataPrimary = { 16 | restaurantName: string; 17 | restaurantImage: { 18 | src: string; 19 | alt: string; 20 | }; 21 | distance: string; 22 | deliveryTime: string; 23 | pickupTime: string; 24 | isDashPass: boolean; 25 | }; 26 | 27 | export type TStorefrontData = { 28 | shortDescription?: string; 29 | averageRating?: number; 30 | ratingCount?: number; 31 | priceRating?: number; 32 | operationHours: { 33 | openHour: number; 34 | openMinute: number; 35 | closeHour: number; 36 | closeMinute: number; 37 | }[]; 38 | items: TStoreItem[]; 39 | }; 40 | 41 | export type TStoreItem = { 42 | itemID: number; 43 | image: { 44 | src: string; 45 | alt: string; 46 | }; 47 | itemName: string; 48 | price: number; 49 | description?: string; 50 | lastOrdered?: string; 51 | ratingPercentage: number; 52 | ratingCount: number; 53 | }; 54 | -------------------------------------------------------------------------------- /components/Icons/PlusCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | type TPlusCircle = { 2 | isEnabled?: boolean; 3 | }; 4 | 5 | export default function PlusCircle({ isEnabled }: TPlusCircle) { 6 | return ( 7 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Icons/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | type TStar = { 2 | color?: string; 3 | isInlineReview?: boolean; 4 | }; 5 | 6 | export default function Star({ color, isInlineReview }: TStar) { 7 | return ( 8 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/Navigation/HamburgerButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const HamburgerButtonWrapper = styled.button` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | margin: 0 17px; 8 | background-color: white; 9 | `; 10 | 11 | export default function HamburgerButton() { 12 | return ( 13 | 14 | 21 | 30 | 39 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import styled from 'styled-components'; 3 | import styles from '../styles/Home.module.css'; 4 | import HomeLayout from '../components/Layouts/HomeLayout'; 5 | import RestaurantCarousel from '../components/RestaurantCarousel/RestaurantCarousel'; 6 | import { restaurantCarousels } from '../components/datav2'; 7 | 8 | const RestaurantCarouselSection = styled.section` 9 | width: 100%; 10 | `; 11 | 12 | export default function Home() { 13 | const arrayOfCarousels = restaurantCarousels.map((carousel, index) => ( 14 | 19 | )); 20 | 21 | return ( 22 | <> 23 | 24 | DoorDash Food Delivery 25 | 29 | 33 | 34 | 35 | 36 |
37 | 38 | {arrayOfCarousels} 39 | 40 |
41 |
42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /styles/GlobalStyles.tsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyles = createGlobalStyle` 4 | :root { 5 | /* System Colors Variables */ 6 | --primary-black: rgba(25, 25, 25, 1); 7 | --primary-black-transparency-75: rgba(25, 25, 25, 0.75); 8 | --secondary-black: rgba(73, 73, 73, 1); 9 | 10 | --primary-gray: rgba(231, 231, 231, 1); 11 | --secondary-gray: rgba(247, 247, 247, 1); 12 | --tertiary-gray: rgba(96, 96, 96, 1); 13 | --quaternary-gray: rgba(214, 214, 214, 1); 14 | --quinary-gray: rgba(118, 118, 118, 1); 15 | 16 | --primary-white: rgba(255, 255, 255, 1); 17 | 18 | --primary-red: rgba(255, 48, 8, 1); 19 | --secondary-red: rgba(235, 23, 0, 1); 20 | --tertiary-red: rgba(217, 20, 0, 1); 21 | --quaternary-red: rgba(183, 16, 0, 1); 22 | 23 | --primary-teal: rgba(0, 131, 138, 1); 24 | 25 | --primary-green: rgba(0, 135, 47, 1); 26 | 27 | --primary-gold: rgba(161, 108, 0, 1); 28 | 29 | /* System Font Family Variable */ 30 | --primary-font-family: 'TTNorms'; 31 | 32 | /* Nav Label Font Option Variables */ 33 | --nav-label-font-size: 14px; 34 | --nav-label-font-weight: 500; 35 | } 36 | 37 | * { 38 | /* Setting default font family and disabling ligatures */ 39 | font-family: var(--primary-font-family); 40 | font-variant-ligatures: no-common-ligatures; 41 | -webkit-font-smoothing: antialiased; 42 | -moz-osx-font-smoothing: grayscale; 43 | } 44 | `; 45 | 46 | export default GlobalStyles; 47 | -------------------------------------------------------------------------------- /components/Icons/HeartIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function Heart() { 2 | return ( 3 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/StoreComponents/CheckoutButton/CheckoutButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { useAppSelector } from '../../../app-redux/hooks'; 4 | 5 | const CheckoutButtonWrapper = styled.button` 6 | width: 100%; 7 | height: 40px; 8 | display: flex; 9 | align-items: center; 10 | background-color: var(--secondary-red); 11 | border-radius: 20px; 12 | transition: ease 0.15s; 13 | transition-property: background-color; 14 | 15 | &:hover { 16 | background-color: var(--tertiary-red); 17 | transition: ease 0.15s; 18 | transition-property: background-color; 19 | cursor: pointer; 20 | } 21 | 22 | &:active { 23 | transition: 0.15s ease; 24 | transition-property: background-color; 25 | background-color: var(--quaternary-red); 26 | } 27 | `; 28 | 29 | const CheckoutButtonTextWrapper = styled.div` 30 | display: flex; 31 | justify-content: space-between; 32 | padding: 0 16px; 33 | width: 100%; 34 | `; 35 | 36 | const CheckoutButtonCheckoutLabel = styled.span` 37 | font-size: 16px; 38 | font-weight: 500; 39 | color: var(--primary-white); 40 | `; 41 | 42 | const CheckoutButtonPriceLabel = styled.span` 43 | font-size: 16px; 44 | font-weight: 500; 45 | color: var(--primary-white); 46 | `; 47 | 48 | export default function CheckoutButton() { 49 | const cartTotalValue = useAppSelector((state) => state.cartSlice.totalValue); 50 | const priceFormatter = new Intl.NumberFormat('en-US', { 51 | style: 'currency', 52 | currency: 'USD', 53 | }); 54 | 55 | return ( 56 | 57 | 58 | Checkout 59 | {priceFormatter.format(cartTotalValue)} 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /components/Icons/ThumbsUpIcon.tsx: -------------------------------------------------------------------------------- 1 | type TThumbsUp = { 2 | size?: number; 3 | }; 4 | 5 | export default function ThumbsUp({ size }: TThumbsUp) { 6 | return ( 7 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/Layouts/StoreLayout.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React from 'react'; 3 | import styles from '../../styles/StoreLayout.module.css'; 4 | import ItemOverlay from '../StoreComponents/ItemOverlay/ItemOverlay'; 5 | 6 | const StorePage = styled.article` 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | margin: 16px 0; 11 | width: calc(100% - 340px); 12 | left: 0; 13 | position: absolute; 14 | row-gap: 22px; 15 | 16 | @media screen and (max-width: 1185px) { 17 | width: 100%; 18 | } 19 | `; 20 | 21 | const StorePageDivider = styled.hr` 22 | width: 928px; 23 | margin: 0 auto; 24 | border: none; 25 | border-bottom: 1px solid var(--primary-gray); 26 | 27 | @media screen and (max-width: 1300px) { 28 | margin: 0 16px; 29 | max-width: calc(100% - 32px); 30 | width: 100%; 31 | } 32 | 33 | @media screen and (max-width: 1185px) { 34 | margin: 0; 35 | width: calc(928px); 36 | } 37 | `; 38 | 39 | type TStoreLayout = { 40 | children: JSX.Element[]; 41 | }; 42 | 43 | export default function StoreLayout({ children }: TStoreLayout) { 44 | const mainContent = children.slice(0, -1); 45 | return ( 46 | <> 47 | 48 |
49 | 50 | {/* {eslint-disable-next-line arrow-body-style} */} 51 | {mainContent.map((child, index) => ( 52 | // eslint-disable-next-line react/no-array-index-key 53 | 54 | {child} 55 | {index !== mainContent.length - 1 ? : null} 56 | 57 | ))} 58 | 59 | {/* NAV */} 60 | {children[children.length - 1]} 61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/CartSheet/CartSheetBackground.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { useCallback, useEffect } from 'react'; 3 | import { useAppDispatch } from '../../app-redux/hooks'; 4 | import { setFalseIsOpenFromCartSheet, toggleIsOpenFromCartSheet } from '../../app-redux/features/cart/cartSlice'; 5 | 6 | const CartSheetBackgroundWrapper = styled.dialog<{ isStoreCartSheet?: boolean }>` 7 | width: 100vw; 8 | height: 100vh; 9 | background-color: transparent; 10 | z-index: 3; 11 | position: fixed; 12 | top: 0; 13 | left: 0; 14 | border: none; 15 | display: ${(props) => (props.isStoreCartSheet ? 'none' : 'flex')}; 16 | 17 | @media screen and (max-width: 1185px) { 18 | 19 | } 20 | `; 21 | 22 | type TCartSheetBackground = { 23 | isStoreCartSheet?: boolean; 24 | }; 25 | 26 | export default function CartSheetBackground({ isStoreCartSheet }: TCartSheetBackground) { 27 | const dispatch = useAppDispatch(); 28 | 29 | // function keyDownHandler(e: KeyboardEvent) { 30 | // if (e.key === 'Escape') { 31 | // dispatch(setFalseIsOpenFromCartSheet()); 32 | // } 33 | // } 34 | 35 | const keyDownHandler = useCallback((e: React.KeyboardEvent) => { 36 | if (e.key === 'Escape') { 37 | dispatch(setFalseIsOpenFromCartSheet()); 38 | } 39 | }, [dispatch]); 40 | 41 | useEffect(() => { 42 | window.addEventListener('keydown', keyDownHandler as any); 43 | return () => { 44 | window.removeEventListener('keydown', keyDownHandler as any); 45 | }; 46 | }, [keyDownHandler]); 47 | 48 | return ( 49 | // eslint-disable-next-line styled-components-a11y/no-noninteractive-element-interactions 50 | { dispatch(toggleIsOpenFromCartSheet()); }} 52 | onKeyDown={keyDownHandler} 53 | isStoreCartSheet={isStoreCartSheet} 54 | aria-modal="true" 55 | /> 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/FilterButtonRow/FilterButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import CarrotDown from '../Icons/CarrotDownIcon'; 3 | 4 | const FilterButtonWrapper = styled.button` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | column-gap: 2px; 9 | background-color: var(--primary-gray); 10 | transition: 0.15s ease; 11 | transition-property: background-color; 12 | height: 32px; 13 | width: fit-content; 14 | padding: 0 13px; 15 | border-radius: 16px; 16 | 17 | &:hover { 18 | cursor: pointer; 19 | transition: 0.15s ease; 20 | transition-property: background-color; 21 | background-color: var(--secondary-gray); 22 | } 23 | 24 | // equivalent to onMouseDown 25 | &:active { 26 | transition: 0.15s ease; 27 | transition-property: background-color; 28 | background-color: var(--quaternary-gray); 29 | } 30 | `; 31 | 32 | const FilterButtonButtonLabel = styled.h5` 33 | font-size: 14px; 34 | font-weight: 500; 35 | color: var(--primary-black); 36 | `; 37 | 38 | const FilterButtonVerticalDivider = styled.hr` 39 | border: 1px solid var(--quaternary-gray); 40 | height: 16px; 41 | border-top: none; 42 | border-bottom: none; 43 | border-left: none; 44 | `; 45 | 46 | type TFilterButton = { 47 | buttonData: { 48 | buttonLabel: string | null; 49 | leftLogo: JSX.Element | null; 50 | rightLogo: JSX.Element | null; 51 | hasDivider: boolean; 52 | hasDropdown: boolean; 53 | } 54 | }; 55 | 56 | export default function FilterButton({ buttonData }: TFilterButton) { 57 | return ( 58 | 59 | {buttonData.leftLogo} 60 | {buttonData.buttonLabel} 61 | {buttonData.rightLogo} 62 | {buttonData.hasDivider ? 63 | 64 | : 65 | null} 66 | {buttonData.hasDropdown ? 67 | 68 | : 69 | null} 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/GithubBadge/GithubBadge.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Link from 'next/link'; 3 | 4 | const OctoSVG = styled.svg` 5 | fill:#151513; 6 | color:#fff; 7 | width: 80px; 8 | height: 80px; 9 | 10 | @media screen and (max-width: 1185px) { 11 | width: 50px; 12 | height: 50px; 13 | } 14 | 15 | @media screen and (max-width: 1050px) { 16 | width: 40px; 17 | height: 40px; 18 | } 19 | `; 20 | 21 | const GithubBadgeLink = styled(Link)` 22 | position: fixed; 23 | top: 0; 24 | border: 0; 25 | right: 0; 26 | z-index: 99999; 27 | 28 | &:hover .octo-arm { 29 | animation: octocat-wave 560ms ease-in-out; 30 | @keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}} 31 | } 32 | `; 33 | 34 | export default function GithubBadge() { 35 | return ( 36 | 41 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/Icons/CartIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function CartIcon() { 2 | return ( 3 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/Navigation/ShoppingCartButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import CartIcon from '../Icons/CartIcon'; 3 | 4 | import { useAppSelector, useAppDispatch } from '../../app-redux/hooks'; 5 | import { toggleIsOpenFromCartSheet } from '../../app-redux/features/cart/cartSlice'; 6 | 7 | const ShoppingCartButtonWrapper = styled.button<{ isShoppingCartToggleable?: boolean }>` 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | column-gap: 12px; 12 | min-width: 68px; 13 | height: 32px; 14 | background-color: var(--secondary-red); 15 | transition: ease 0.15s; 16 | transition-property: background-color; 17 | border-radius: 17px; 18 | border: none; 19 | 20 | &:hover { 21 | background-color: var(--tertiary-red); 22 | transition: ease 0.15s; 23 | transition-property: background-color; 24 | cursor: pointer; 25 | } 26 | 27 | &:active { 28 | transition: 0.15s ease; 29 | transition-property: background-color; 30 | background-color: var(--quaternary-red); 31 | } 32 | 33 | @media screen and (min-width: 1185px) { 34 | pointer-events: ${(props) => (props.isShoppingCartToggleable ? 'all' : 'none')}; 35 | } 36 | `; 37 | 38 | const ShoppingCartButtonLabel = styled.h4` 39 | color: var(--primary-white); 40 | font-weight: 500; 41 | font-size: 14px; 42 | `; 43 | 44 | type TShoppingCartButton = { 45 | isShoppingCartToggleable: boolean; 46 | }; 47 | 48 | export default function ShoppingCartButton({ isShoppingCartToggleable }: TShoppingCartButton) { 49 | // calculate total number of items in cart 50 | const cart = useAppSelector((state) => state.cartSlice.cart); 51 | let cartCount = 0; 52 | cart.forEach((item) => { 53 | cartCount += item.quantity; 54 | }); 55 | 56 | const dispatch = useAppDispatch(); 57 | 58 | return ( 59 | { dispatch(toggleIsOpenFromCartSheet()); }} 61 | isShoppingCartToggleable={isShoppingCartToggleable} 62 | aria-label={`Shopping cart, ${cartCount} items`} 63 | > 64 | 65 | 66 | {cartCount} 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, Head, Main, NextScript, DocumentContext 3 | } from 'next/document'; 4 | import { ServerStyleSheet } from 'styled-components'; 5 | 6 | export default class MyDocument extends Document { 7 | static async getInitialProps(ctx: DocumentContext) { 8 | const sheet = new ServerStyleSheet(); 9 | const originalRenderPage = ctx.renderPage; 10 | 11 | try { 12 | ctx.renderPage = () => originalRenderPage({ 13 | enhanceApp: (App) => (props) => sheet.collectStyles(), 14 | }); 15 | 16 | const initialProps = await Document.getInitialProps(ctx); 17 | return { 18 | ...initialProps, 19 | styles: [initialProps.styles, sheet.getStyleElement()], 20 | }; 21 | } finally { 22 | sheet.seal(); 23 | } 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 30 | 37 | 44 | 51 | 58 | 59 | 60 |
61 | 62 | 63 | 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/Icons/GroupOfPeopleIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function GroupOfPeople() { 2 | return ( 3 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /pages/store/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext } from 'next'; 2 | import Head from 'next/head'; 3 | import { createContext } from 'react'; 4 | import { restaurantList } from '../../components/datav2'; 5 | import { TRestaurantDataPrimary, TStorefrontData, TStoreItem } from '../../global'; 6 | 7 | import StoreLayout from '../../components/Layouts/StoreLayout'; 8 | import HeroComponent from '../../components/StoreComponents/HeroComponent/HeroComponent'; 9 | import CartOverview from '../../components/StoreComponents/CartOverviewComponent/CartOverview'; 10 | // eslint-disable-next-line import/no-cycle 11 | import QuickActions from '../../components/StoreComponents/QuickActionsComponent/QuickActions'; 12 | 13 | import { useAppDispatch } from '../../app-redux/hooks'; 14 | import { setPageViewingStoreID } from '../../app-redux/features/cart/cartSlice'; 15 | 16 | type TServerSideProps = { 17 | restaurant: { 18 | restaurantData: TRestaurantDataPrimary; 19 | storefrontData: TStorefrontData; 20 | }; 21 | storeID: string; 22 | }; 23 | 24 | export const StoreItemsContext = createContext(null); 25 | 26 | export default function Store({ restaurant, storeID }: TServerSideProps) { 27 | const dispatch = useAppDispatch(); 28 | dispatch(setPageViewingStoreID(Number(storeID))); 29 | return ( 30 | <> 31 | 32 | {restaurant.restaurantData.restaurantName} 33 | 37 | 41 | 42 | 43 | 44 | 48 | {/* Insert Rest of the Store's components */} 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | export async function getServerSideProps(ctx: GetServerSidePropsContext) { 59 | // fetch data pertinent to store here 60 | // Always note that the last property ctx.params?.slug MUST be the same as the name [slug], without square brackets. 61 | const storeID = ctx.params?.slug; 62 | const restaurant = restaurantList[Number(storeID) as keyof typeof restaurantList]; 63 | return { 64 | props: { restaurant, storeID }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /components/StoreComponents/CartItem/CartItem.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import styled from 'styled-components'; 3 | import InputStepper from '../InputStepper/InputStepper'; 4 | 5 | const CartItemWrapper = styled.a` 6 | width: 100%; 7 | display: flex; 8 | justify-content: space-between; 9 | column-gap: 20px; 10 | align-items: center; 11 | padding: 16px 0; 12 | padding-left: 5px; 13 | padding-right: 16px; 14 | border-bottom: 1px solid var(--primary-gray); 15 | background-color: transparent; 16 | text-align: left; 17 | `; 18 | 19 | const CartItemImageWrapper = styled.div` 20 | width: 60px; 21 | height: 60px; 22 | border-radius: 5px; 23 | overflow: hidden; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | position: relative; 28 | `; 29 | 30 | const CartItemImage = styled(Image)` 31 | object-fit: cover; 32 | `; 33 | 34 | const CartItemDescriptionWrapper = styled.div` 35 | display: flex; 36 | flex-direction: column; 37 | row-gap: 12px; 38 | margin-right: 9.5px; 39 | max-width: 98px; 40 | `; 41 | 42 | const CartItemDescriptionItem = styled.span` 43 | max-width: 100%; 44 | font-size: 16px; 45 | font-weight: 400; 46 | color: var(--primary-black); 47 | line-height: 22px; 48 | text-overflow: ellipsis; 49 | overflow: hidden; 50 | -webkit-line-clamp: 2; 51 | display: -webkit-box; 52 | -webkit-box-orient: vertical; 53 | `; 54 | 55 | const CartItemDescriptionPrice = styled.span` 56 | font-size: 14px; 57 | font-weight: 400; 58 | color: var(--primary-black); 59 | `; 60 | 61 | type TCartItem = { 62 | imageSrc: string; 63 | imageAlt: string; 64 | itemName: string; 65 | price: number; 66 | itemID: number; 67 | }; 68 | 69 | export default function CartItem({ 70 | imageSrc, imageAlt, itemName, price, itemID 71 | }: TCartItem) { 72 | const priceFormatter = new Intl.NumberFormat('en-US', { 73 | style: 'currency', 74 | currency: 'USD', 75 | }); 76 | 77 | return ( 78 | 83 | 84 | 90 | 91 | 92 | 93 | {itemName} 94 | 95 | 96 | {priceFormatter.format(price)} 97 | 98 | 99 | 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /components/StoreComponents/ItemOverlay/ItemOverlay.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable styled-components-a11y/no-noninteractive-element-interactions */ 2 | import styled from 'styled-components'; 3 | import { Transition, TransitionStatus } from 'react-transition-group'; 4 | import React, { useCallback, useEffect, useRef } from 'react'; 5 | import FocusTrap from 'focus-trap-react'; 6 | import { useAppSelector, useAppDispatch } from '../../../app-redux/hooks'; 7 | import { setIsModalOpenFalse, toggleIsModalOpen } from '../../../app-redux/features/item/itemSlice'; 8 | import ItemCustomizationPanel from './ItemCustomizationPanel/ItemCustomizationPanel'; 9 | 10 | interface ItemModalWrapperProps { 11 | state: TransitionStatus; 12 | isModalOpen: boolean; 13 | } 14 | 15 | const ItemModalWrapper = styled.dialog` 16 | width: 100%; 17 | height: 100%; 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | background-color: var(--primary-black-transparency-75); 22 | z-index: 9; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | border: none; 27 | opacity: ${(props) => (props.state === 'entering' 28 | ? 29 | 0 : props.state === 'entered' 30 | ? 31 | 1 : props.state === 'exiting' 32 | ? 0 33 | : 0)}; 34 | transition: opacity 225ms ease; 35 | 36 | @media screen and (max-width: 770px) { 37 | padding: 0 16px; 38 | } 39 | 40 | @media screen and (max-width: 480px) { 41 | padding: 0; 42 | } 43 | `; 44 | 45 | export default function ItemModal() { 46 | const isModalOpen = useAppSelector((state) => state.itemSlice.isModalOpen); 47 | const dispatch = useAppDispatch(); 48 | const nodeRef = useRef(null); 49 | 50 | const keyDownHandler = useCallback((e: React.KeyboardEvent) => { 51 | if (e.key === 'Escape') { 52 | dispatch(setIsModalOpenFalse()); 53 | } 54 | }, [dispatch]); 55 | 56 | useEffect(() => { 57 | window.addEventListener('keydown', keyDownHandler as any); 58 | return () => { 59 | window.removeEventListener('keydown', keyDownHandler as any); 60 | }; 61 | }, [keyDownHandler]); 62 | 63 | return ( 64 | 70 | {(state) => ( 71 | 72 | dispatch(toggleIsModalOpen())} 76 | onKeyDown={(e) => keyDownHandler(e as React.KeyboardEvent)} 77 | ref={nodeRef} 78 | > 79 | 83 | 84 | 85 | )} 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /components/RestaurantCarousel/RestaurantCarousel.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import CarouselTitle from './CarouselTitle'; 3 | import RestaurantCard from './RestaurantCard/RestaurantCard'; 4 | import { restaurantList } from '../datav2'; 5 | 6 | const RestaurantCarouselArticle = styled.article` 7 | display: flex; 8 | flex-direction: column; 9 | margin: 30px auto; 10 | row-gap: 19px; 11 | width: 1152px; 12 | 13 | @media screen and (max-width: 1280px) { 14 | width: unset; 15 | max-width: 100%; 16 | margin: 30px 64px; 17 | } 18 | 19 | @media screen and (max-width: 1185px) { 20 | margin: 30px 48px; 21 | } 22 | 23 | @media screen and (max-width: 960px) { 24 | margin: 30px 32px; 25 | } 26 | 27 | @media screen and (max-width: 770px) { 28 | margin: 30px 0; 29 | } 30 | 31 | @media screen and (max-width: 480px) { 32 | margin: 15px 0; 33 | row-gap: 15px; 34 | } 35 | `; 36 | 37 | const RestaurantCarouselTopRow = styled.div` 38 | display: flex; 39 | justify-content: space-between; 40 | width: 100%; 41 | 42 | @media screen and (max-width: 770px) { 43 | padding-left: 32px; 44 | } 45 | 46 | @media screen and (max-width: 480px) { 47 | padding-left: 16px; 48 | } 49 | `; 50 | 51 | const RestaurantCarouselCarousel = styled.div` 52 | display: flex; 53 | max-width: 100%; 54 | column-gap: 15px; 55 | overflow-x: scroll; 56 | scroll-snap-type: x mandatory; 57 | 58 | // hide scrollbar style 59 | -ms-overflow-style: none; // IE and Edge 60 | scrollbar-width: none; // Firefox 61 | &::-webkit-scrollbar { 62 | display: none; 63 | } 64 | 65 | @media screen and (max-width: 770px) { 66 | overflow-x: scroll; 67 | padding: 0 32px; 68 | scroll-snap-type: none; 69 | column-gap: 12px; 70 | } 71 | 72 | @media screen and (max-width: 480px) { 73 | overflow-x: scroll; 74 | padding: 0 16px; 75 | scroll-snap-type: none; 76 | column-gap: 8px; 77 | } 78 | `; 79 | 80 | type TRestaurauntCarousel = { 81 | carouselData: { 82 | carouselName: string, 83 | selectedRestaurantIDs: number[]; 84 | }; 85 | }; 86 | 87 | export default function RestaurantCarousel({ carouselData } : TRestaurauntCarousel) { 88 | const arrayOfRestaurantCards = carouselData.selectedRestaurantIDs.map(((restaurantID, index) => ( 89 | 97 | ))); 98 | 99 | return ( 100 | 101 | 102 | 103 | {/* add buttons to navigate through carousel */} 104 | 105 | 106 | {/* Add custom restaurant cards here */} 107 | {arrayOfRestaurantCards} 108 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/StoreComponents/HeroComponent/DeliveryTile/DeliveryTile.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import DashPassIcon from '../../../Icons/DashPassIcon'; 3 | import Information from '../../../Icons/InformationIcon'; 4 | import { useAppSelector } from '../../../../app-redux/hooks'; 5 | 6 | const DeliveryTileWrapper = styled.div` 7 | display: flex; 8 | align-items: center; 9 | height: 62px; 10 | border-radius: 8px; 11 | border: 1px solid var(--primary-gray); 12 | margin: 10px 0; 13 | `; 14 | 15 | const DeliveryTileHalfWrapper = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | align-items: center; 19 | justify-content: center; 20 | width: 170px; 21 | height: 100%; 22 | position: relative; 23 | `; 24 | 25 | const DeliveryTileDashPassWrapper = styled.div<{ isDelivery: boolean }>` 26 | position: absolute; 27 | left: ${(props) => (props.isDelivery ? '34px' : '28px')}; 28 | top: 10px; 29 | `; 30 | 31 | const DeliveryTileDivider = styled.div` 32 | height: 28px; 33 | border-left: 1px solid var(--primary-gray); 34 | `; 35 | 36 | const DeliveryTimeTextStrongTeal = styled.span` 37 | font-size: 16px; 38 | font-weight: 500; 39 | color: var(--primary-teal); 40 | letter-spacing: -0.4px; 41 | margin-bottom: -1px; 42 | `; 43 | 44 | const DeliveryTimeTextStrong = styled.span` 45 | font-size: 16px; 46 | font-weight: 500; 47 | color: var(--primary-black); 48 | letter-spacing: -0.4px; 49 | margin-bottom: -1px; 50 | `; 51 | 52 | const DeliveryTimeTextRegular = styled.span` 53 | font-size: 14px; 54 | font-weight: 400; 55 | color: var(--quinary-gray); 56 | `; 57 | 58 | const DeliveryTimeInformationWrapper = styled.div` 59 | position: absolute; 60 | right: 31px; 61 | bottom: 13px; 62 | `; 63 | 64 | type TDeliveryTime = { 65 | deliveryTime: string; 66 | pickupTime: string; 67 | }; 68 | 69 | export default function DeliveryTile({ deliveryTime, pickupTime }: TDeliveryTime) { 70 | const isDelivery = useAppSelector((state) => state.deliverySlice.isDelivery); 71 | 72 | return ( 73 | 74 | 75 | 76 | 77 | 78 | 79 | {isDelivery ? '$0.00' : '5% back'} 80 | 81 | 82 | {isDelivery ? 'delivery fee' : 'and $0 fees'} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | {isDelivery ? deliveryTime : pickupTime} 92 | 93 | 94 | {isDelivery ? 'delivery time' : 'ready for pickup'} 95 | 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /components/Navigation/HomeLogoLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styled from 'styled-components'; 3 | 4 | const HomeLogoLinkStyle = styled.a` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | gap: 12px; 9 | `; 10 | 11 | const DoorDashLogoText = styled.div` 12 | display: flex; 13 | align-items: center; 14 | 15 | @media screen and (max-width: 960px) { 16 | display: none; 17 | } 18 | `; 19 | 20 | export default function HomeLogoLink() { 21 | return ( 22 | 23 | 24 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | import { useEffect } from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import GlobalStyles from '../styles/GlobalStyles'; 6 | import Home from './index'; 7 | import Store from './store/[slug]'; 8 | import Navbar from '../components/Navigation/Navbar'; 9 | import { store } from '../app-redux/store'; 10 | // import { useAppDispatch, useAppSelector } from '../app-redux/hooks'; 11 | // import { setRestaurantListData, setRestaurantCarouselData } from '../app-redux/features/data/dataSlice'; 12 | import CartSheet from '../components/CartSheet/CartSheet'; 13 | import GithubBadge from '../components/GithubBadge/GithubBadge'; 14 | 15 | export default function App({ Component, pageProps }: AppProps) { 16 | useEffect(() => { 17 | console.log('%chttps://anericzhang.com', 'background: orange; font-size: 16px'); 18 | // Prints: my website to the console 19 | }, []); 20 | 21 | // const dispatch = useAppDispatch(); 22 | // const restaurantData = useAppSelector((state) => state.dataSlice.restaurantListData); 23 | // const restaurantCarouselData = useAppSelector((state) => state.dataSlice.restaurantCarousels); 24 | // fetch restaurant data 25 | useEffect(() => { 26 | // Set fetchData for later. 27 | 28 | // async function fetchData() { 29 | // try { 30 | // const res = await fetch('/api/hello'); 31 | // if (!res.ok) { 32 | // console.log(`fetch failed, error code ${res.status} - ${res.statusText}`); 33 | // } else { 34 | // const data = await res.json(); 35 | // // dispatch(setRestaurantListData(data.restaurantListData)); 36 | // // dispatch(setRestaurantCarouselData(data.restaurantCarouselsData)); 37 | // // setRestaurantData(data.restaurantListData); 38 | // // setRestaurantCarouselsData(data.restaurantCarouselsData); 39 | // } 40 | // } catch (e) { 41 | // console.error(e); 42 | // console.log('fetch failed!'); 43 | // } 44 | // } 45 | 46 | // fetchData(); 47 | }, []); 48 | 49 | // useEffect(() => { 50 | // console.log(restaurantData, restaurantCarouselData); 51 | // }, [restaurantData, restaurantCarouselData]); 52 | 53 | return ( 54 | 55 | 56 | {/** 57 | * Check if the child component pages (Component prop) are either Home or Store. 58 | * If they are, render a 59 | * */} 60 | {Component === Home || Component === Store ? ( 61 | Component !== Store ? ( 62 | 63 | ) : ( 64 | 65 | ) 66 | ) : null} 67 | {/* {(Component !== Store) ? : null} */} 68 | {/* Consider passing a cartSheet with property isStoreCartSheet for 69 | Component === store? So we can use this boolean to hide/show the cart sheet */} 70 | {Component === Home ? ( 71 | 72 | ) : Component === Store ? ( 73 | 74 | ) : null} 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/StoreComponents/MenuSection/MenuSection.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | import styled from 'styled-components'; 3 | import { Fragment, useContext } from 'react'; 4 | import MenuItem from '../MenuItem/MenuItem'; 5 | import { StoreItemsContext } from '../../../pages/store/[slug]'; 6 | 7 | const MenuSectionSection = styled.section` 8 | display: flex; 9 | flex-direction: column; 10 | `; 11 | 12 | const MenuSectionHeaderWrapper = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | row-gap: 3.5px; 16 | 17 | @media screen and (max-width: 770px) { 18 | padding: 0 16px; 19 | } 20 | 21 | @media screen and (max-width: 480px) { 22 | padding: 0; 23 | } 24 | `; 25 | 26 | // reserved for larger headers, like feature items section 27 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 28 | const MenuSectionHeaderLarge = styled.h2` 29 | font-size: 24px; 30 | font-weight: 500; 31 | letter-spacing: -0.6px; 32 | color: var(--primary-black); 33 | `; 34 | 35 | const MenuSectionHeader = styled.h2` 36 | font-size: 22px; 37 | font-weight: 500; 38 | letter-spacing: -0.7px; 39 | color: var(--primary-black); 40 | `; 41 | 42 | const MenuSectionSubheader = styled.span` 43 | font-size: 14px; 44 | font-weight: 400; 45 | color: var(--quinary-gray); 46 | `; 47 | 48 | const MenuSectionItemsWrapper = styled.div` 49 | display: flex; 50 | column-gap: 14px; 51 | row-gap: 16px; 52 | flex-wrap: wrap; 53 | width: 100%; 54 | margin: 18px 0; 55 | overflow: hidden; 56 | 57 | @media screen and (max-width: 1300px) { 58 | justify-content: space-between; 59 | column-gap: unset; 60 | } 61 | 62 | @media screen and (max-width: 770px) { 63 | padding: 0 18px; 64 | } 65 | 66 | @media screen and (max-width: 480px) { 67 | padding: 0; 68 | } 69 | `; 70 | 71 | const MenuSectionItemsDivider = styled.hr` 72 | display: none; 73 | 74 | @media screen and (max-width: 770px) { 75 | display: block; 76 | width: 100%; 77 | border-top: none; 78 | border-left: none; 79 | border-right: none; 80 | border-bottom: 1px solid var(--primary-gray); 81 | } 82 | `; 83 | 84 | export default function MenuSection() { 85 | const restaurantMenu = useContext(StoreItemsContext); 86 | 87 | return ( 88 | 89 | 90 | 91 | Popular Items 92 | 93 | 94 | The most commonly ordered items and dishes from this store 95 | 96 | 97 | 98 | {restaurantMenu?.map((item, index) => ( 99 | 100 | 110 | 111 | 112 | ))} 113 | 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /components/StoreComponents/ItemOverlay/ItemCustomizationPanel/ModalInputStepper/ModalInputStepper.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | import styled from 'styled-components'; 3 | import MinusCircle from '../../../../Icons/MinusCircleIcon'; 4 | import PlusCircle from '../../../../Icons/PlusCircleIcon'; 5 | 6 | const ModalInputStepperWrapper = styled.div` 7 | display: flex; 8 | align-items: center; 9 | height: 100%; 10 | width: fit-content; 11 | column-gap: 16px; 12 | 13 | @media screen and (max-width: 480px) { 14 | column-gap: 9px; 15 | } 16 | `; 17 | 18 | const ModalInputStepperButton = styled.button` 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | background-color: transparent; 23 | padding: 10px; 24 | 25 | &:hover { 26 | cursor: pointer; 27 | } 28 | `; 29 | 30 | const ModalInputStepperInput = styled.input` 31 | height: 100%; 32 | width: 64px; 33 | display: flex; 34 | justify-content: center; 35 | text-align: center; 36 | font-size: 16px; 37 | background-color: var(--secondary-gray); 38 | color: var(--primary-black); 39 | border-radius: 7px; 40 | border: none; 41 | 42 | &:focus { 43 | outline-color: var(--primary-black); 44 | } 45 | 46 | &::placeholder { 47 | font-size: 16px; 48 | text-align: center; 49 | color: var(--primary-black); 50 | } 51 | 52 | &::-webkit-outer-spin-button, 53 | &::-webkit-inner-spin-button { 54 | -webkit-appearance: none; 55 | margin: 0; 56 | } 57 | `; 58 | 59 | type TModalInputStepper = { 60 | itemCounter: number; 61 | setItemCounter: React.Dispatch>; 62 | }; 63 | 64 | export default function ModalInputStepper({ itemCounter, setItemCounter }: TModalInputStepper) { 65 | function incrementCounter() { 66 | setItemCounter((prevItemCounter) => prevItemCounter + 1); 67 | } 68 | 69 | function decrementCounter() { 70 | if (itemCounter > 1) { 71 | setItemCounter((prevItemCounter) => prevItemCounter - 1); 72 | } 73 | } 74 | 75 | // this is pretty important. Without this, the text cursor will be 76 | // placed anywhere along existing text, but you can't delete it. 77 | // by selecting the entire field on click, we can ensure that the input will be properly reset. 78 | function setInputFocus() { 79 | (document.getElementById('ModalInputStepperInput') as HTMLInputElement).select(); 80 | } 81 | 82 | return ( 83 | 84 | 88 | 1} /> 89 | 90 | { 95 | setItemCounter(Number(e.target.value)); 96 | }} 97 | id="ModalInputStepperInput" 98 | onFocus={setInputFocus} 99 | aria-label={`Quantity: ${itemCounter}`} 100 | aria-live="polite" 101 | aria-atomic="true" 102 | /> 103 | 107 | 108 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /components/FilterButtonRow/FilterButtonRow.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import FilterButton from './FilterButton'; 3 | import DashPass from '../Icons/DashPassIcon'; 4 | import Coupon from '../Icons/CouponIcon'; 5 | import Star from '../Icons/StarIcon'; 6 | 7 | const FilterButtonRowWrapper = styled.div` 8 | display: flex; 9 | align-items: center; 10 | width: 100%; 11 | height: 62px; 12 | background-color: var(--primary-white); 13 | position: sticky; 14 | top: 64px; 15 | border-bottom: 1px solid var(--primary-gray); 16 | z-index: 1; 17 | 18 | @media screen and (max-width: 770px) { 19 | height: 56px; 20 | width: unset; 21 | } 22 | `; 23 | 24 | const FilterButtonRowResizer = styled.div` 25 | 26 | width: 1152px; 27 | margin-left: auto; 28 | margin-right: auto; 29 | 30 | @media screen and (max-width: 1280px) { 31 | width: unset; 32 | max-width: 100%; 33 | margin: 30px 64px; 34 | } 35 | 36 | @media screen and (max-width: 1185px) { 37 | margin: 30px 48px; 38 | } 39 | 40 | @media screen and (max-width: 960px) { 41 | margin: 30px 32px; 42 | } 43 | 44 | @media screen and (max-width: 770px) { 45 | margin: 30px 0; 46 | overflow-x: scroll; 47 | // hide scrollbar style 48 | -ms-overflow-style: none; // IE and Edge 49 | scrollbar-width: none; // Firefox 50 | &::-webkit-scrollbar { 51 | display: none; 52 | } 53 | } 54 | `; 55 | 56 | const FilterButtonRowList = styled.fieldset` 57 | display: flex; 58 | column-gap: 8px; 59 | border: none; 60 | 61 | @media screen and (max-width: 770px) { 62 | padding: 0 32px; 63 | width: max-content; 64 | } 65 | 66 | @media screen and (max-width: 480px) { 67 | padding: 0 16px; 68 | } 69 | `; 70 | 71 | export default function FilterButtonRow() { 72 | const filterButtonData = [ 73 | { 74 | buttonLabel: 'DashPass', 75 | leftLogo: , 76 | rightLogo: null, 77 | hasDivider: false, 78 | hasDropdown: false, 79 | }, 80 | { 81 | buttonLabel: 'Offers', 82 | leftLogo: , 83 | rightLogo: null, 84 | hasDivider: false, 85 | hasDropdown: false, 86 | }, 87 | { 88 | buttonLabel: 'Pickup', 89 | leftLogo: null, 90 | rightLogo: null, 91 | hasDivider: false, 92 | hasDropdown: false, 93 | }, 94 | { 95 | buttonLabel: 'Over 4.5', 96 | leftLogo: null, 97 | rightLogo: , 98 | hasDivider: true, 99 | hasDropdown: true, 100 | }, 101 | { 102 | buttonLabel: 'Under 30 min', 103 | leftLogo: null, 104 | rightLogo: null, 105 | hasDivider: false, 106 | hasDropdown: false, 107 | }, 108 | { 109 | buttonLabel: 'Price', 110 | leftLogo: null, 111 | rightLogo: null, 112 | hasDivider: false, 113 | hasDropdown: true, 114 | }, 115 | ]; 116 | 117 | return ( 118 | 119 | 120 | 121 | {filterButtonData.map((buttonData) => ( 122 | 126 | ))} 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /components/StoreComponents/InputStepper/InputStepper.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | import styled from 'styled-components'; 3 | import Minus from '../../Icons/MinusIcon'; 4 | import Plus from '../../Icons/PlusIcon'; 5 | import GarbageCan from '../../Icons/GarbageCanIcon'; 6 | 7 | // redux global state 8 | import { useAppDispatch, useAppSelector } from '../../../app-redux/hooks'; 9 | import { addItemToCart, deleteItemFromCart } from '../../../app-redux/features/cart/cartSlice'; 10 | 11 | const InputStepperWrapper = styled.div` 12 | display: flex; 13 | align-items: center; 14 | background-color: var(--secondary-gray); 15 | border-radius: 17px; 16 | box-shadow: rgb(0 0 0 / 20%) 0px 2px 8px; 17 | margin-left: auto; 18 | `; 19 | 20 | const InputStepperButton = styled.button` 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | width: 32px; 25 | height: 32px; 26 | border-radius: 500px; 27 | background-color: var(--primary-white); 28 | color: inherit; 29 | 30 | &:hover { 31 | cursor: pointer; 32 | } 33 | 34 | &:visited { 35 | color: inherit; 36 | } 37 | `; 38 | 39 | const InputStepperLabelWrapper = styled.div` 40 | display: flex; 41 | justify-content: center; 42 | min-width: 32px; 43 | `; 44 | 45 | const InputStepperLabel = styled.span` 46 | font-weight: 500; 47 | font-size: 14px; 48 | color: var(--primary-black); 49 | `; 50 | 51 | type TInputStepper = { 52 | itemID: number; 53 | }; 54 | 55 | export default function InputStepper({ itemID }: TInputStepper) { 56 | const stateCart = useAppSelector((state) => state.cartSlice.cart); 57 | let quantityCart = 0; 58 | 59 | stateCart.forEach((item) => { 60 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 61 | itemID === item.itemID ? quantityCart = item.quantity : null; 62 | }); 63 | 64 | /** 65 | * TODO: Values need to be debounced before they are dispatched to global store. 66 | * REVISION: Values can be directly dispatched to global store. the reducer itself will take care of 67 | * debouncing since it will post to the server then. 68 | */ 69 | 70 | const dispatch = useAppDispatch(); 71 | 72 | function handleClickIncrement() { 73 | const cartPayload = { 74 | itemID, 75 | quantity: 1, 76 | }; 77 | dispatch(addItemToCart(cartPayload)); 78 | } 79 | 80 | function handleClickDecrement() { 81 | if (quantityCart > 1) { 82 | dispatch(deleteItemFromCart(itemID)); 83 | } else if (quantityCart === 1) { 84 | // delete item and remove from list if stepperCount reaches 0. 85 | dispatch(deleteItemFromCart(itemID)); 86 | } 87 | } 88 | 89 | return ( 90 | 93 | 97 | {quantityCart === 1 ? 98 | 99 | : 100 | } 101 | 102 | 103 | 108 | {quantityCart} 109 | {' '} 110 | × 111 | 112 | 113 | 117 | 118 | 119 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /components/Navigation/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import React, { useCallback } from 'react'; 3 | import BackArrow from '../Icons/BackArrowIcon'; 4 | import MagnifyingGlass from '../Icons/MagnifyingGlassIcon'; 5 | 6 | type TSearchBar = { 7 | isSearchBarToggled: boolean; 8 | setIsSearchBarToggled: React.Dispatch>; 9 | }; 10 | 11 | // Note that if CSS styles are dependent on TS, you need to invoke the 12 | // styled function this way - announcing the prop ahead, at style declaration 13 | 14 | const SearchBarWrapper = styled('label')<{ isSearchBarToggled: boolean }>` 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | column-gap: 10px; 19 | height: 40px; 20 | padding: ${(props) => (props.isSearchBarToggled ? '0 11px' : '0 13px')}; 21 | width: 428px; 22 | border-radius: 23px; 23 | border: 2px solid var(--primary-black); 24 | border: ${(props) => (props.isSearchBarToggled ? '2px solid var(--primary-black)' : 'none')}; 25 | background-color: var(--secondary-gray); 26 | 27 | &:hover { 28 | cursor: text; 29 | } 30 | 31 | @media screen and (max-width: 1280px) { 32 | 33 | } 34 | 35 | @media screen and (max-width: 1185px) { 36 | /* width: fit-content; */ 37 | width: 300px; 38 | } 39 | 40 | @media screen and (max-width: 960px) { 41 | width: 100%; 42 | max-width: 460px; 43 | } 44 | 45 | @media screen and (max-width: 770px) { 46 | width: 100%; 47 | max-width: 550px; 48 | } 49 | `; 50 | 51 | const SearchBarInput = styled.input` 52 | width: 91%; 53 | max-height: 24px; 54 | border: none; 55 | background-color: transparent; 56 | color: var(--primary-black); 57 | font-weight: 400; 58 | font-size: 16px; 59 | letter-spacing: 0ch; 60 | 61 | &:focus { 62 | outline: none; 63 | } 64 | 65 | &::placeholder { 66 | font-size: 16px; 67 | color: var(--tertiary-gray); 68 | text-overflow: ellipsis; 69 | } 70 | `; 71 | 72 | export default function SearchBar({ isSearchBarToggled, setIsSearchBarToggled }: TSearchBar) { 73 | // function searchBarOnClick() { 74 | // setIsSearchBarToggled(true); 75 | // } 76 | 77 | const searchBarOnClick = useCallback(() => { 78 | setIsSearchBarToggled(true); 79 | }, [setIsSearchBarToggled]); 80 | 81 | // function searchBarOnBlur() { 82 | // setIsSearchBarToggled(false); 83 | // } 84 | 85 | const searchBarOnBlur = useCallback(() => { 86 | setIsSearchBarToggled(false); 87 | }, [setIsSearchBarToggled]); 88 | 89 | return ( 90 | // disabled this rule as we need to trigger focus when the entire wrapper is clicked 91 | // eslint-disable-next-line styled-components-a11y/no-noninteractive-element-interactions 92 | { 97 | if (e.key === 'Enter') { 98 | // eslint-disable-next-line no-alert 99 | alert('Search has not been implemented, yet!'); 100 | } else if (e.key === 'Escape') { 101 | setIsSearchBarToggled(false); 102 | } 103 | }} 104 | htmlFor="SearchBar__ID" 105 | aria-label="Begin typing to search for stores available on DoorDash." 106 | > 107 | { 108 | isSearchBarToggled ? 109 | // Show back button 110 | 111 | : 112 | // Shows default search icon 113 | 114 | } 115 | 122 | 123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /components/Icons/DashPassIcon.tsx: -------------------------------------------------------------------------------- 1 | type TDashPass = { 2 | size?: number; 3 | color?: string; 4 | }; 5 | 6 | export default function DashPassIcon({ size, color }: TDashPass) { 7 | return (size || color) ? 8 | 21 | : 22 | ; 35 | } 36 | -------------------------------------------------------------------------------- /components/RestaurantCarousel/RestaurantCard/RestaurantCard.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { useState } from 'react'; 5 | import { TRestaurantDataPrimary } from '../../../global'; 6 | import DashPassIcon from '../../Icons/DashPassIcon'; 7 | import Shimmer from '../../Placeholders/Shimmer'; 8 | 9 | const RestaurantCardArticle = styled.article` 10 | min-width: 374px; 11 | height: 245px; 12 | scroll-snap-align: start; 13 | 14 | 15 | @media screen and (max-width: 1280px) { 16 | flex: 0 0 calc((100% - 32px) / 3); 17 | min-width: unset; 18 | } 19 | 20 | @media screen and (max-width: 960px) { 21 | flex: 0 0 calc((100% - 16px) / 2); 22 | } 23 | 24 | @media screen and (max-width: 770px) { 25 | flex: 0 0 346px; 26 | } 27 | 28 | @media screen and (max-width: 480px) { 29 | flex: 0 0 293px; 30 | } 31 | `; 32 | 33 | const RestaurantCardLink = styled.a` 34 | display: flex; 35 | row-gap: 14px; 36 | flex-direction: column; 37 | text-decoration: none; 38 | `; 39 | 40 | const RestaurantCardImageWrapper = styled.div` 41 | width: 100%; 42 | height: 172px; 43 | border-radius: 4px; 44 | overflow: hidden; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | position: relative; 49 | 50 | @media screen and (max-width: 480px) { 51 | height: 144px; 52 | } 53 | `; 54 | 55 | const RestaurantCardImage = styled(Image)` 56 | object-fit: cover; 57 | transition: 0.3s; 58 | `; 59 | 60 | const RestaurantCardBottom = styled.div` 61 | display: flex; 62 | `; 63 | 64 | const RestaurantCardText = styled.div` 65 | display: flex; 66 | flex-direction: column; 67 | row-gap: 4px; 68 | `; 69 | 70 | const RestaurantCardRestaurantNameLabel = styled.div` 71 | display: flex; 72 | align-items: center; 73 | column-gap: 6px; 74 | margin: 0 2px; 75 | `; 76 | 77 | const RestaurantCardRestaurantName = styled.span` 78 | font-size: 16px; 79 | font-weight: 500; 80 | `; 81 | 82 | const RestaurantCardAuxInfo = styled.span` 83 | font-size: 14px; 84 | font-weight: 400; 85 | color: var(--quinary-gray); 86 | `; 87 | 88 | type TRestaurantCard = { 89 | restaurantID: number, 90 | restaurantData: TRestaurantDataPrimary; 91 | index: number; 92 | }; 93 | 94 | export default function RestaurantCard({ restaurantID, restaurantData, index } : TRestaurantCard) { 95 | const [isImageLoading, setIsImageLoading] = useState(true); 96 | 97 | return ( 98 | 99 | 100 | 101 | 102 | {isImageLoading ? : null} 103 | { setIsImageLoading(false); }} 113 | /> 114 | 115 | 116 | 117 | 118 | 119 | 120 | {restaurantData.restaurantName} 121 | 122 | 123 | 132 | 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /components/CartSheet/CartSheet.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import FocusTrap from 'focus-trap-react'; 3 | import { useRef } from 'react'; 4 | import { Transition, TransitionStatus } from 'react-transition-group'; 5 | import CartOverview from '../StoreComponents/CartOverviewComponent/CartOverview'; 6 | import { useAppSelector, useAppDispatch } from '../../app-redux/hooks'; 7 | import { toggleIsOpenFromCartSheet } from '../../app-redux/features/cart/cartSlice'; 8 | import X from '../Icons/XIcon'; 9 | import CartSheetBackground from './CartSheetBackground'; 10 | 11 | const CartSheetWrapper = styled.aside` 12 | display: flex; 13 | position: fixed; 14 | // width of the wrapper + box shadow = 340px + 24px ≈ 370px 15 | right: ${(props) => (props.state === 'entered' ? '0px' : '-370px')}; 16 | top: 0px; 17 | pointer-events: ${(props) => (props.state === 'entered' ? 'all' : 'none')}; 18 | z-index: 4; 19 | background-color: var(--primary-white); 20 | height: 100%; 21 | width: 340px; 22 | transition: 0.225s right ease; 23 | 24 | @media screen and (min-width: 1185px) { 25 | display: ${(props) => (props.isStoreCartSheet ? 'none' : 'flex')}; 26 | } 27 | 28 | @media screen and (max-width: 1185px) { 29 | width: ${(props) => props.isStoreCartSheet && props.isOpenFromCartSheet && '50%'}; 30 | /* width: ${(props) => (props.isOpenFromCartSheet ? props.isStoreCartSheet ? '50%' : 'inherit' : 'inherit')}; */ 31 | /* right: ${(props) => (props.isOpenFromCartSheet ? '0px' : '-53%')}; */ 32 | /* inverse logic to determine the position if it's CLOSED, then if it's store or home cart */ 33 | right: ${(props) => (!(props.state === 'entered') ? props.isStoreCartSheet ? '-50vw' : '-370px' : '0px')}; 34 | } 35 | 36 | @media screen and (max-width: 770px) { 37 | display: flex; 38 | width: ${(props) => props.isStoreCartSheet && props.isOpenFromCartSheet && '375px'}; 39 | right: ${(props) => (props.state === 'entered' ? '0px' : '-394px')}; 40 | pointer-events: all; 41 | } 42 | 43 | @media screen and (max-width: 480px) { 44 | width: 100%; 45 | right: ${(props) => (props.state === 'entered' ? '0px' : '-105%')}; 46 | } 47 | `; 48 | 49 | const CartSheetButtonClose = styled.button` 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | width: 55px; 54 | height: 30px; 55 | border: none; 56 | background-color: transparent; 57 | margin: 18px 0; 58 | padding: 18px 0; 59 | color: inherit; 60 | 61 | &:hover { 62 | cursor: pointer; 63 | } 64 | 65 | &:visited { 66 | color: inherit; 67 | } 68 | `; 69 | 70 | interface ICartSheetWrapper { 71 | state: TransitionStatus; 72 | isOpenFromCartSheet: boolean; 73 | isStoreCartSheet?: boolean; 74 | } 75 | 76 | type TCartSheet = { 77 | isStoreCartSheet?: boolean; 78 | }; 79 | 80 | export default function CartSheet({ isStoreCartSheet }: TCartSheet) { 81 | const isOpenFromCartSheet = useAppSelector((state) => state.cartSlice.isOpenFromCartSheet); 82 | const dispatch = useAppDispatch(); 83 | const nodeRef = useRef(null); 84 | 85 | return ( 86 | 92 | {(state) => ( 93 | 94 |
95 | {isOpenFromCartSheet ? 96 | : isStoreCartSheet ? 97 | : null} 98 | {/* TODO: Look into how to use dynamic props to set for the wrapper. */} 99 | 106 | 107 | dispatch(toggleIsOpenFromCartSheet())} 109 | aria-label="Close cart" 110 | aria-hidden={!isOpenFromCartSheet} 111 | tabIndex={isOpenFromCartSheet ? 0 : -1} 112 | > 113 | 114 | 115 | 116 | 117 |
118 |
119 | )} 120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /components/Navigation/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styled from 'styled-components'; 3 | import { useState } from 'react'; 4 | import HamburgerButton from './HamburgerButton'; 5 | import HomeLogoLink from './HomeLogoLink'; 6 | import AddressButtonToggle from './AddressButtonToggle'; 7 | import SearchBar from './SearchBar'; 8 | import ShoppingCartButton from './ShoppingCartButton'; 9 | 10 | // Navbar styles 11 | 12 | const NavbarWrapper = styled.nav` 13 | position: fixed; 14 | top: 0; 15 | overscroll-behavior-y: none; 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: space-between; 19 | align-items: center; 20 | width: 100%; 21 | height: 64px; 22 | background-color: var(--primary-white); 23 | border: 1px solid var(--primary-gray); 24 | border-top: none; 25 | border-right: none; 26 | border-left: none; 27 | padding-left: 64px; 28 | padding-right: 144px; 29 | z-index: 3; 30 | 31 | @media screen and (max-width: 1185px) { 32 | padding-left: 0; 33 | padding-right: 48px; 34 | } 35 | 36 | @media screen and (max-width: 960px) { 37 | padding-left: 0; 38 | padding-right: 16px; 39 | } 40 | `; 41 | 42 | const NavbarSubLeft = styled.div` 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | column-gap: 2px; 47 | 48 | @media screen and (max-width: 1280px) { 49 | min-width: 496px; 50 | } 51 | 52 | @media screen and (max-width: 960px) { 53 | justify-content: flex-start; 54 | min-width: fit-content; 55 | } 56 | 57 | @media screen and (max-width: 770px) { 58 | margin-right: 30px; 59 | } 60 | `; 61 | 62 | const NavbarLinkGroup = styled.div` 63 | display: flex; 64 | justify-content: center; 65 | align-items: center; 66 | margin-left: 32px; 67 | margin-right: 16px; 68 | 69 | @media screen and (max-width: 770px) { 70 | display: none; 71 | } 72 | `; 73 | 74 | const NavbarLinkWrapper = styled.div` 75 | display: flex; 76 | column-gap: 24px; 77 | text-decoration: none; 78 | list-style: none; 79 | `; 80 | 81 | const NavbarLinkInner = styled.a` 82 | padding: 10px 0; 83 | `; 84 | 85 | const NavbarLabel = styled.span` 86 | font-family: var(--primary-font-family); 87 | font-size: var(--nav-label-font-size); 88 | font-weight: var(--nav-label-font-weight); 89 | -webkit-font-smoothing: antialiased; 90 | -moz-osx-font-smoothing: grayscale; 91 | `; 92 | 93 | const NavbarVerticalDivider = styled.hr` 94 | width: 0px; 95 | height: 24px; 96 | border: 1px solid var(--primary-gray); 97 | border-radius: 2px; 98 | 99 | @media screen and (max-width: 960px) { 100 | display: none; 101 | } 102 | `; 103 | 104 | const NavbarSubRight = styled.div` 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | column-gap: 16px; 109 | 110 | @media screen and (max-width: 1185px) { 111 | width: 100%; 112 | justify-content: flex-end; 113 | } 114 | 115 | @media screen and (max-width: 960px) { 116 | padding-right: 24px; 117 | } 118 | 119 | @media screen and (max-width: 770px) { 120 | width: 100%; 121 | } 122 | `; 123 | 124 | type TNavBar = { 125 | isShoppingCartToggleable: boolean; 126 | }; 127 | 128 | export default function Navbar({ isShoppingCartToggleable }: TNavBar) { 129 | // AddressButtonToggle state that reveals address list 130 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 131 | const [isAddressButtonToggled, setIsAddressButtonToggled] = useState(false); 132 | const [isSearchBarToggled, setIsSearchBarToggled] = useState(false); 133 | 134 | return ( 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | Delivery 145 | 146 | 147 | 148 | 149 | 150 | 151 | Pickup 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 162 | 163 | 164 | 165 | 169 | 170 | 171 | 172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /components/StoreComponents/MenuItem/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Image from 'next/image'; 3 | import { useState } from 'react'; 4 | import ThumbsUp from '../../Icons/ThumbsUpIcon'; 5 | import Shimmer from '../../Placeholders/Shimmer'; 6 | import { TStoreItem } from '../../../global'; 7 | import { useAppDispatch } from '../../../app-redux/hooks'; 8 | import { toggleIsModalOpen, setModalData } from '../../../app-redux/features/item/itemSlice'; 9 | 10 | const ItemWrapper = styled.button` 11 | width: 49.2%; 12 | height: 144px; 13 | display: flex; 14 | background-color: var(--primary-white); 15 | border: 1px solid; 16 | border-color: var(--primary-gray); 17 | border-radius: 4px; 18 | transition: 0.25s ease; 19 | transition-property: border-color; 20 | align-items: center; 21 | justify-content: space-between; 22 | overflow: hidden; 23 | 24 | &:hover { 25 | cursor: pointer; 26 | border-color: var(--quaternary-gray); 27 | transition: 0.25s ease; 28 | transition-property: border-color; 29 | } 30 | 31 | @media screen and (max-width: 1300px) { 32 | max-width: 49.1%; 33 | } 34 | 35 | @media screen and (max-width: 770px) { 36 | max-width: 100%; 37 | width: 100%; 38 | border: none; 39 | border-radius: 0; 40 | } 41 | `; 42 | 43 | const ItemTextWrapper = styled.div` 44 | display: flex; 45 | flex-direction: column; 46 | align-items: flex-start; 47 | padding: 0 16px; 48 | row-gap: 5px; 49 | max-width: 313px; 50 | 51 | @media screen and (max-width: 770px) { 52 | max-width: 580px; 53 | padding-left: 0; 54 | } 55 | `; 56 | 57 | const ItemTextName = styled.span` 58 | font-size: 16px; 59 | font-weight: 500; 60 | letter-spacing: -0.4px; 61 | color: var(--primary-black); 62 | `; 63 | 64 | const ItemTextDescription = styled.span` 65 | font-size: 14px; 66 | font-weight: 400; 67 | color: var(--quinary-gray); 68 | text-align: left; 69 | display: -webkit-box; 70 | -webkit-box-orient: vertical; 71 | -webkit-line-clamp: 2; 72 | overflow: hidden; 73 | line-height: 1.5; 74 | `; 75 | 76 | const ItemTextStatsWrapper = styled.div` 77 | display: flex; 78 | column-gap: 4px; 79 | align-items: center; 80 | `; 81 | 82 | const ItemTextPrice = styled.span` 83 | font-size: 14px; 84 | font-weight: 400; 85 | letter-spacing: 0px; 86 | color: var(--secondary-black); 87 | `; 88 | 89 | const ItemTextStats = styled.span` 90 | font-size: 14px; 91 | font-weight: 400; 92 | letter-spacing: -0.3px; 93 | color: var(--secondary-black); 94 | `; 95 | 96 | const ItemTextLastOrdered = styled.span` 97 | font-size: 14px; 98 | font-weight: 500; 99 | color: var(--primary-teal); 100 | letter-spacing: -0.4px; 101 | `; 102 | 103 | const ItemImageWrapper = styled.div` 104 | display: flex; 105 | justify-content: center; 106 | align-items: center; 107 | position: relative; 108 | min-width: 143px; 109 | height: 100%; 110 | 111 | @media screen and (max-width: 770px) { 112 | border-radius: 4px; 113 | overflow: hidden; 114 | min-width: 110px; 115 | height: 110px; 116 | } 117 | `; 118 | 119 | const ItemImage = styled(Image)` 120 | object-fit: cover; 121 | `; 122 | 123 | export default function MenuItem({ 124 | itemID, image, itemName, price, description, ratingCount, ratingPercentage, lastOrdered 125 | }: TStoreItem) { 126 | const dispatch = useAppDispatch(); 127 | const priceFormatter = new Intl.NumberFormat('en-US', { 128 | style: 'currency', 129 | currency: 'USD', 130 | }); 131 | 132 | // shimmer loading state 133 | const [isImageLoading, setIsImageLoading] = useState(true); 134 | 135 | return ( 136 | { 139 | dispatch(toggleIsModalOpen()); 140 | dispatch(setModalData({ 141 | itemID, image, itemName, price, description, ratingCount, ratingPercentage, lastOrdered 142 | })); 143 | } 144 | } 145 | > 146 | 147 | {itemName} 148 | 149 | {description} 150 | 151 | 152 | {priceFormatter.format(price)} 153 | 154 | 155 | 156 | {ratingPercentage} 157 | % ( 158 | {ratingCount} 159 | ) 160 | 161 | 162 | 163 | {lastOrdered ? `Last ordered on ${lastOrdered}` : null} 164 | 165 | 166 | 167 | {isImageLoading ? : null} 168 | setIsImageLoading(false)} 174 | /> 175 | 176 | 177 | 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /app-redux/features/cart/cartSlice.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { restaurantList } from '../../../components/datav2'; 3 | 4 | interface ICartState { 5 | storeID?: number; 6 | isOpenFromCartSheet?: boolean; 7 | pageViewingStoreID?: number; 8 | cart: ICartItem[]; 9 | totalValue: number; 10 | } 11 | 12 | interface ICartItem { 13 | itemID: number; 14 | quantity: number; 15 | } 16 | 17 | const initialState: ICartState = { 18 | // storeID: 1243431, 19 | isOpenFromCartSheet: false, 20 | cart: [ 21 | // { 22 | // itemID: 0, 23 | // quantity: 2, 24 | // }, 25 | // { 26 | // itemID: 1, 27 | // quantity: 2, 28 | // }, 29 | ], 30 | totalValue: 0, 31 | }; 32 | 33 | const restaurants = restaurantList; 34 | 35 | function calculateCartTotal() { 36 | let sum = 0; 37 | initialState.cart.forEach((item) => { 38 | sum += 39 | item.quantity * 40 | restaurants[initialState.storeID as keyof typeof restaurants] 41 | .storefrontData.items[item.itemID].price; 42 | }); 43 | return sum; 44 | } 45 | 46 | initialState.totalValue = calculateCartTotal(); 47 | 48 | function immutableCalculateCartTotal(state: ICartState) { 49 | let sum = 0; 50 | state.cart.forEach((item) => { 51 | sum += 52 | item.quantity * 53 | restaurants[state.storeID as keyof typeof restaurants] 54 | .storefrontData.items[item.itemID].price; 55 | }); 56 | state.totalValue = sum; 57 | } 58 | 59 | // cartSlice needs access to the storeID to ensure that all items in the cart are from storeID. 60 | // Without storeID validation, the cart could contain items from multiple stores, which is 61 | // disallowed in standard DD delivery 62 | // (with exception of having dasher pick up items after a delivery has been dispatched) 63 | const cartSlice = createSlice({ 64 | name: 'cart', 65 | initialState, 66 | reducers: { 67 | toggleIsOpenFromCartSheet: (state) => { 68 | state.isOpenFromCartSheet = !state.isOpenFromCartSheet; 69 | }, 70 | 71 | setFalseIsOpenFromCartSheet: (state) => { 72 | state.isOpenFromCartSheet = false; 73 | }, 74 | 75 | setPageViewingStoreID: (state, action: PayloadAction) => { 76 | state.pageViewingStoreID = action.payload; 77 | }, 78 | 79 | setStoreID: (state, action: PayloadAction) => { 80 | state.storeID = action.payload; 81 | }, 82 | 83 | // if the user adds an item from a different store, we need to 84 | // reset the storeID and cart to the storeID of the new item they were looking at 85 | resetCartNewStore: (state, action: PayloadAction) => { 86 | state.storeID = action.payload; 87 | state.cart = []; 88 | immutableCalculateCartTotal(state); 89 | }, 90 | 91 | // the user adds an item, passing the itemID 92 | addItemToCart: (state, action: PayloadAction) => { 93 | // loop through cart items to see if the itemID (action.payload) exists. 94 | // if it exists, then just increment the quantity. 95 | // if it does not exist, then add the new item to a copy of the existing cart state. 96 | 97 | let itemExists = false; 98 | const newState = { ...state }; // create a copy of the state 99 | for (let i = 0; i < newState.cart.length; i++) { 100 | if (newState.cart[i].itemID === action.payload.itemID) { 101 | newState.cart[i].quantity += action.payload.quantity; 102 | itemExists = true; 103 | break; 104 | } 105 | } 106 | 107 | if (!itemExists) { 108 | newState.cart = [ 109 | ...newState.cart, 110 | { 111 | itemID: action.payload.itemID, 112 | quantity: action.payload.quantity, 113 | }, 114 | ]; 115 | } 116 | state.cart = newState.cart; 117 | immutableCalculateCartTotal(state); 118 | }, 119 | 120 | // reduces the quantity of an item in a cart if quantity > 0 121 | // if quantity is 1 and user hits delete, item should be removed from list 122 | deleteItemFromCart: (state, action: PayloadAction) => { 123 | const newState = { ...state }; 124 | for (let i = 0; i < newState.cart.length; i++) { 125 | if ( 126 | newState.cart[i].itemID === action.payload && 127 | newState.cart[i].quantity > 1 128 | ) { 129 | newState.cart[i].quantity -= 1; 130 | break; 131 | } else if ( 132 | newState.cart[i].itemID === action.payload && 133 | newState.cart[i].quantity === 1 134 | ) { 135 | // if state.cart[i].quantity is 1, then filter out that item altogether. 136 | newState.cart = newState.cart.filter( 137 | (item) => item.itemID !== action.payload 138 | ); 139 | break; 140 | } 141 | } 142 | state.cart = newState.cart; 143 | immutableCalculateCartTotal(state); 144 | }, 145 | }, 146 | }); 147 | 148 | export const { 149 | toggleIsOpenFromCartSheet, 150 | setFalseIsOpenFromCartSheet, 151 | setPageViewingStoreID, 152 | setStoreID, 153 | resetCartNewStore, 154 | addItemToCart, 155 | deleteItemFromCart, 156 | } = cartSlice.actions; 157 | export default cartSlice.reducer; 158 | -------------------------------------------------------------------------------- /components/data.tsx: -------------------------------------------------------------------------------- 1 | export default function data() { 2 | return ( 3 | [ 4 | { 5 | carouselName: 'Now on Doordash', 6 | restaurantData: [ 7 | { 8 | restaurantID: 65341, 9 | restaurantName: "Rosa's Pizza", 10 | restaurantImage: '/images/BellissimoPizza.webp', 11 | distance: '0.6 mi', 12 | deliveryTime: '28 min' 13 | }, 14 | { 15 | restaurantID: 1243431, 16 | restaurantName: 'Omakase Sushi', 17 | restaurantImage: '/images/Sushi.webp', 18 | distance: '1.2 mi', 19 | deliveryTime: '53 min' 20 | }, 21 | { 22 | restaurantID: 18764431, 23 | restaurantName: 'dosa by DOSA', 24 | restaurantImage: '/images/DosaByDosa.jpeg', 25 | distance: '0.8 mi', 26 | deliveryTime: '40 min' 27 | }, 28 | { 29 | restaurantID: 98441, 30 | restaurantName: 'Curry Up Now', 31 | restaurantImage: '/images/CurryUpNow.jpeg', 32 | distance: '0.8 mi', 33 | deliveryTime: '40 min' 34 | }, 35 | { 36 | restaurantID: 12356667, 37 | restaurantName: 'Bird and Buffalo', 38 | restaurantImage: '/images/BirdAndBuffalo.jpeg', 39 | distance: '0.3 mi', 40 | deliveryTime: '21 min' 41 | }, 42 | 43 | ] 44 | }, 45 | { 46 | carouselName: 'Most Popular Local Restaurants', 47 | restaurantData: [ 48 | { 49 | restaurantID: 12356667, 50 | restaurantName: 'Bird and Buffalo', 51 | restaurantImage: '/images/BirdAndBuffalo.jpeg', 52 | distance: '0.3 mi', 53 | deliveryTime: '21 min' 54 | }, 55 | { 56 | restaurantID: 98441, 57 | restaurantName: 'Curry Up Now', 58 | restaurantImage: '/images/CurryUpNow.jpeg', 59 | distance: '0.8 mi', 60 | deliveryTime: '40 min' 61 | }, 62 | { 63 | restaurantID: 334624, 64 | restaurantName: 'Nari Thai', 65 | restaurantImage: '/images/NariThai.jpeg', 66 | distance: '3.6 mi', 67 | deliveryTime: '39 min' 68 | }, 69 | { 70 | restaurantID: 65341, 71 | restaurantName: "Rosa's Pizza", 72 | restaurantImage: '/images/BellissimoPizza.webp', 73 | distance: '0.6 mi', 74 | deliveryTime: '28 min' 75 | }, 76 | { 77 | restaurantID: 1243431, 78 | restaurantName: 'Omakase Sushi', 79 | restaurantImage: '/images/Sushi.webp', 80 | distance: '1.2 mi', 81 | deliveryTime: '53 min' 82 | }, 83 | { 84 | restaurantID: 18764431, 85 | restaurantName: 'dosa by DOSA', 86 | restaurantImage: '/images/DosaByDosa.jpeg', 87 | distance: '0.8 mi', 88 | deliveryTime: '40 min' 89 | }, 90 | 91 | ] 92 | }, 93 | { 94 | carouselName: 'Bring the Tailgate to You', 95 | restaurantData: [ 96 | { 97 | restaurantID: 120985, 98 | restaurantName: 'Cholita Linda', 99 | restaurantImage: '/images/CholitaLinda.jpeg', 100 | distance: '0.9 mi', 101 | deliveryTime: '23 min' 102 | }, 103 | { 104 | restaurantID: 65341, 105 | restaurantName: "Rosa's Pizza", 106 | restaurantImage: '/images/BellissimoPizza.webp', 107 | distance: '0.6 mi', 108 | deliveryTime: '28 min' 109 | }, 110 | { 111 | restaurantID: 1243431, 112 | restaurantName: 'Omakase Sushi', 113 | restaurantImage: '/images/Sushi.webp', 114 | distance: '1.2 mi', 115 | deliveryTime: '53 min' 116 | }, 117 | { 118 | restaurantID: 18764431, 119 | restaurantName: 'dosa by DOSA', 120 | restaurantImage: '/images/DosaByDosa.jpeg', 121 | distance: '0.8 mi', 122 | deliveryTime: '40 min' 123 | }, 124 | { 125 | restaurantID: 98441, 126 | restaurantName: 'Curry Up Now', 127 | restaurantImage: '/images/CurryUpNow.jpeg', 128 | distance: '0.8 mi', 129 | deliveryTime: '40 min' 130 | }, 131 | { 132 | restaurantID: 12356667, 133 | restaurantName: 'Bird and Buffalo', 134 | restaurantImage: '/images/BirdAndBuffalo.jpeg', 135 | distance: '0.3 mi', 136 | deliveryTime: '21 min' 137 | }, 138 | 139 | ] 140 | } 141 | ] 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /components/StoreComponents/CartOverviewComponent/CartOverview.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import styled from 'styled-components'; 3 | import CheckoutButton from '../CheckoutButton/CheckoutButton'; 4 | import CartItem from '../CartItem/CartItem'; 5 | import CarrotRight from '../../Icons/CarrotRightIcon'; 6 | import TableOfFood from '../../SVGgraphics/TableOfFood'; 7 | 8 | import { restaurantList } from '../../datav2'; 9 | 10 | // redux global state 11 | import { useAppSelector } from '../../../app-redux/hooks'; 12 | 13 | const CartOverviewWrapper = styled.aside<{ isInCartSheet: boolean }>` 14 | display: flex; 15 | flex-direction: column; 16 | width: ${(props) => (props.isInCartSheet ? '100%' : '340px')}; 17 | border-left: 1px solid var(--primary-gray); 18 | position: ${(props) => (props.isInCartSheet ? 'relative' : 'fixed')}; 19 | right: ${(props) => (props.isInCartSheet ? 'unset' : '0')}; 20 | height: ${(props) => (props.isInCartSheet ? '100%' : 'calc(100% - 64px)')}; 21 | box-shadow: ${(props) => (props.isInCartSheet ? 'rgb(0 0 0 / 20%) 0px 8px 24px;' : 'none')}; 22 | /* padding-top: ${(props) => (props.isInCartSheet ? '72px' : '0')}; */ 23 | overflow-y: scroll; 24 | background-color: var(--primary-white); 25 | 26 | @media screen and (max-width: 1185px) { 27 | // hide the component for now - it will show itself when the shopping cart button is triggered 28 | display: ${(props) => (props.isInCartSheet ? 'flex' : 'none')}; 29 | } 30 | `; 31 | 32 | const CartOverviewCheckoutWrapper = styled.div` 33 | display: flex; 34 | flex-direction: column; 35 | padding: 0 16px; 36 | margin-top: 18px; 37 | margin-bottom: 16px; 38 | row-gap: 17.5px; 39 | `; 40 | 41 | const CartOverviewCheckoutDescription = styled.div` 42 | display: flex; 43 | flex-direction: column; 44 | row-gap: 3px; 45 | `; 46 | 47 | const CartOverviewCheckoutHeader = styled.h6` 48 | font-size: 12px; 49 | font-weight: 500; 50 | color: var(--quinary-gray); 51 | `; 52 | 53 | const CartOverviewCheckoutLink = styled(Link)` 54 | font-size: 18px; 55 | font-weight: 500; 56 | letter-spacing: -0.5px; 57 | color: var(--primary-black); 58 | display: flex; 59 | align-items: center; 60 | `; 61 | 62 | const CartOverviewListWrapper = styled.ul` 63 | display: flex; 64 | flex-direction: column; 65 | align-self: flex-end; 66 | width: 95.5%; 67 | border-top: 1px solid var(--primary-gray); 68 | `; 69 | 70 | const CartOverviewZeroItems = styled.div` 71 | width: 100%; 72 | height: fit-content; 73 | display: flex; 74 | flex-direction: column; 75 | align-items: center; 76 | margin: 36px 0; 77 | `; 78 | 79 | const CartOverviewZeroItemsSpanWrapper = styled.div` 80 | display: flex; 81 | flex-direction: column; 82 | align-items: center; 83 | margin: 24.5px 0; 84 | row-gap: 1.5px; 85 | `; 86 | 87 | const CartOverviewZeroItemsSpan = styled.span` 88 | text-align: center; 89 | font-size: 14px; 90 | font-weight: 400; 91 | color: var(--quinary-gray); 92 | `; 93 | 94 | type TCartOverview = { 95 | isInCartSheet: boolean; 96 | children?: JSX.Element; 97 | }; 98 | 99 | export default function CartOverview({ isInCartSheet, children }: TCartOverview) { 100 | const numberofitems = useAppSelector((state) => state.cartSlice.cart[0]?.quantity); 101 | 102 | const restaurants = restaurantList; 103 | 104 | // consume store, get storeID 105 | const storeID = useAppSelector((state) => state.cartSlice.storeID) as keyof typeof restaurants; 106 | 107 | // consume store, get cart array 108 | const cart = useAppSelector((state) => state.cartSlice.cart); 109 | 110 | const arrayOfCartItems = cart.map((item) => ( 111 | 119 | )); 120 | 121 | return ( 122 | 123 | {children || null} 124 | {numberofitems > 0 125 | ? ( 126 | <> 127 | 128 | 129 | 130 | Your cart from 131 | 132 | 135 | {restaurants[storeID].restaurantData.restaurantName} 136 | 137 | 138 | 139 | 140 | 141 | 142 | {arrayOfCartItems} 143 | 144 | 145 | ) 146 | : ( 147 | 148 | 149 | 150 | 151 | Your cart is empty 152 | 153 | 154 | Add items to get started 155 | 156 | 157 | 158 | )} 159 | 160 | 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doordash Clone, a Rapid Prototype 👯 2 | 3 | 👋 Hey there! Please take a look at [this Notion](https://anericzhang.notion.site/DoorDash-Clone-Discoveries-Hurdles-Lessons-A-WIP-f2fc244b5a3441528a1a69376377170f) to gauge at technical findings, hurdles, and lessons I am uncovering while working on this project. 4 | 5 | ## 🧘 Motivations 6 | 7 | I wanted to create a baseline clone of DoorDash's home portal from an authenticated user's perspective. This challenge tested a variety of skillsets including prototyping pixel perfect, data driven, and reusable UI components. 8 | 9 | This challenge is currently undergoing active development as of January 6th 2023, so features including SSR driven merchant pages, search functionality, and checkout will be implemented along the way. Please check [Roadmap](#roadmap) for more information! 10 | 11 | Up-to-date builds are hosted on Vercel. Please take a look at the `About` section on this repository to access the build. 12 | 13 | # Table of Contents 14 | * [Getting Started](#getting-started) 15 | * [Technology](#technology) 16 | * [Application Architecture](#application-architecture) 17 | * [Roadmap](#roadmap) 18 | * [Recently Completed](#recently-completed) 19 | * [In Progress](#in-progress) 20 | * [Next Up](#next-up) 21 | * [Resources and References](#resources-and-references) 22 | * [Feedback](#feedback) 23 | 24 | ## Getting Started 25 | 26 | Install the dependencies and start the development server 27 | 28 | From the project root, 29 | ```bash 30 | npm i 31 | npx next 32 | ``` 33 | 34 | Open [http://localhost:3000](http://localhost:3000) with your browser to the application. 35 | 36 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 37 | 38 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load local fonts. 39 | 40 | ## Technology 41 | Next.js is used as the React Framework in this rapid prototype. DoorDash requires Server Side Rendering, as the content that hydrates a merchant's page is widely dynamic. 42 | 43 | [styled-components](https://styled-components.com/) is used as the CSS-in-JS framework used to generate dynamic styles based on component props. 44 | 45 | TypeScript is used as the programming language in this application. With a large range of data-dependent components, it is necessary to guard data driven UI components (and their props) with explicit types. 46 | 47 | ## Application Architecture 48 | 49 | ⚠️ Under Construction ⚠️ 50 | 51 | ⚠️ Under Construction ⚠️ 52 | 53 | State Management 54 | 55 | This clone uses Redux-Toolkit to manage state. Several items are put in global store, including the cart list. Check `cartSlice.tsx` to see reducers and `CartOverview.tsx` to see global store consumption. #5 56 | 57 | ![State Management flow for cart as of 1/16](./README-supporting/StateManagement.png) 58 | 59 | ## Roadmap 60 | 61 | ### Recently Completed 62 | * Created `` component and ``, `` subcomponents. Completed 1/6/23 63 | * Created `` layout components, that holds the `` and child elements from the `index.tsx` page. Completed 1/7/23 64 | * Created reusable & data driven `` and `` components and their subcomponents. Completed 1/8/23 65 | * Creating and Linking `` [slug] to a new store view page. Completed 1/10/23 66 | * Asset optimization. Images batched to webp format with 40% quality. Proper image caching, eager/lazy loading on qualified assets. 67 | * Created ``, ``, ``, ``, and wired them. Currently uses temporary data, needs management from global state. Completed 1/14/23 68 | * Establish global state for CartOverview & Store Items. Completed 1/19/23 https://github.com/theericzhang/doordash-clone/pull/10 69 | * Created ``, `` to complete add-to-cart logic. completed 1/19/23 https://github.com/theericzhang/doordash-clone/pull/10 70 | * Responsive styling for existing layouts & components when viewport width is under 1800px. Begin 1/19/23, Completed 1/26/23 https://github.com/theericzhang/doordash-clone/pull/13 71 | * a11y + accessibilty enhancements, screen reader testing. Begin 1/26/23, Completed 1/29/23 https://github.com/theericzhang/doordash-clone/pull/16 72 | * Add more restaurants and items Begin 1/31/23 Completed 2/1/23 https://github.com/theericzhang/doordash-clone/pull/17 73 | * Button visited effect remove, webkit mobile Begin 1/31/23 Completed 2/1/23 https://github.com/theericzhang/doordash-clone/pull/17 74 | 75 | ### In Progress 76 | * Notion updates. Begin 1/31/23 77 | * loading state for getServerSideProps 78 | * create API routes data - use fetch instead of instantiating data files directly 79 | 80 | ### Next Up 81 | * Implement Search functionality. Start with stores, then cuisines and dishes. (backlogged until e-amuse completed) 82 | * Checkout flow. (backlogged until e-amuse completed) 83 | * Garner feedback. 84 | 85 | ## Resources and References 86 | 87 | - [DoorDash Clone - Discoveries, Hurdles, Lessons (A WIP)](https://anericzhang.notion.site/DoorDash-Clone-Discoveries-Hurdles-Lessons-A-WIP-f2fc244b5a3441528a1a69376377170f) - read about my experience thus far building this application! 88 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 89 | - [styled-components](https://styled-components.com/) - learn about styled-components with an interactive button component 90 | - [DoorDash Engineering - Improving Web Page Performance at DoorDash through Server Side Rendering with Next.js](https://doordash.engineering/2022/03/29/improving-web-page-performance-at-doordash-throughserver-side-rendering-with-next-js/) - Read about DoorDash's motivation and technical decision to move to Next.js powered SSR 91 | 92 | ## Feedback 93 | 94 | I'd love to hear from you! Let me know of any feedback you have; it can be pertinent to this project, tech stack, design assets, etc! 95 | 96 | 97 | | Contact | | 98 | |----------|----------------------------------------------------------------| 99 | | Twitter | [@anericzhang](http://twitter.com/anericzhang) | 100 | | Mastodon | [@anericzhang@hachyderm.io](https://hachyderm.io/@anericzhang) | 101 | | email | [anericzhang@gmail.com](mailto:anericzhang@gmail.com) | 102 | -------------------------------------------------------------------------------- /components/StoreComponents/HeroComponent/AuxOptions/AuxOptions.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Heart from '../../../Icons/HeartIcon'; 3 | import GroupOfPeople from '../../../Icons/GroupOfPeopleIcon'; 4 | 5 | // redux global state 6 | import { useAppSelector, useAppDispatch } from '../../../../app-redux/hooks'; 7 | import { toggleDeliveryState } from '../../../../app-redux/features/delivery/deliverySlice'; 8 | 9 | const AuxOptionsWrapper = styled.div` 10 | display: flex; 11 | height: 40px; 12 | column-gap: 15px; 13 | margin-bottom: 5px; 14 | 15 | @media screen and (max-width: 480px) { 16 | align-items: center; 17 | } 18 | `; 19 | 20 | const AuxOptionsButtonPrimary = styled.button` 21 | display: flex; 22 | height: 100%; 23 | background-color: var(--primary-gray); 24 | transition: 0.15s ease; 25 | transition-property: background-color; 26 | align-items: center; 27 | border: none; 28 | border-radius: 20px; 29 | padding: 0 12px; 30 | column-gap: 10px; 31 | color: inherit; 32 | 33 | &:hover { 34 | cursor: pointer; 35 | transition: 0.15s ease; 36 | transition-property: background-color; 37 | background-color: var(--secondary-gray); 38 | } 39 | 40 | // equivalent to onMouseDown 41 | &:active { 42 | transition: 0.15s ease; 43 | transition-property: background-color; 44 | background-color: var(--quaternary-gray); 45 | } 46 | 47 | &:visited { 48 | color: inherit; 49 | background-color: inherit; 50 | } 51 | 52 | @media screen and (max-width: 480px) { 53 | padding: 0 8px; 54 | height: 32px; 55 | } 56 | `; 57 | 58 | const AuxOptionsButtonLabel = styled.span` 59 | font-size: 14px; 60 | font-weight: 500; 61 | color: var(--primary-black); 62 | 63 | @media screen and (max-width: 480px) { 64 | display: none; 65 | } 66 | `; 67 | 68 | const AuxOptionsToggleWrapper = styled.button` 69 | display: flex; 70 | align-items: center; 71 | height: 100%; 72 | background-color: var(--primary-gray); 73 | border: none; 74 | border-radius: 20px; 75 | position: relative; 76 | overflow: hidden; 77 | width: 195px; 78 | transition: 0.15s ease; 79 | transition-property: background-color; 80 | justify-content: space-around; 81 | 82 | &:hover { 83 | cursor: pointer; 84 | transition: 0.15s ease; 85 | transition-property: background-color; 86 | background-color: var(--secondary-gray); 87 | } 88 | 89 | // equivalent to onMouseDown 90 | &:active { 91 | transition: 0.15s ease; 92 | transition-property: background-color; 93 | background-color: var(--quaternary-gray); 94 | } 95 | `; 96 | 97 | const AuxOptionsToggleSlider = styled.div<{ isDelivery: boolean }>` 98 | display: flex; 99 | background-color: var(--primary-black); 100 | justify-content: center; 101 | align-items: center; 102 | height: 100%; 103 | padding: 0 24px; 104 | border-radius: 20px; 105 | transition: 0.25s ease; 106 | transition-property: background-color; 107 | position: absolute; 108 | left: ${(props) => (props.isDelivery ? '0px' : 'unset')}; 109 | right: ${(props) => (!props.isDelivery ? '0px' : 'unset')}; 110 | z-index: 1; 111 | 112 | &:hover { 113 | cursor: pointer; 114 | transition: 0.25s ease; 115 | transition-property: background-color; 116 | background-color: var(--tertiary-gray); 117 | } 118 | `; 119 | 120 | const AuxOptionsToggleLabelWrapper = styled.div` 121 | display: flex; 122 | flex-direction: column; 123 | align-items: center; 124 | row-gap: 1px; 125 | margin-bottom: 1px; 126 | `; 127 | 128 | const AuxOptionsToggleLabelWrapperStationaryLeft = styled.div` 129 | display: flex; 130 | flex-direction: column; 131 | align-items: center; 132 | row-gap: 1px; 133 | margin-bottom: 1px; 134 | `; 135 | 136 | const AuxOptionsToggleLabelWrapperStationaryRight = styled.div` 137 | display: flex; 138 | flex-direction: column; 139 | align-items: center; 140 | row-gap: 1px; 141 | margin-bottom: 1px; 142 | `; 143 | 144 | const AuxOptionsToggleLabelSmall = styled.span` 145 | font-size: 12px; 146 | color: var(--quaternary-gray); 147 | `; 148 | 149 | const AuxOptionsToggleLabel = styled.span` 150 | font-size: 14px; 151 | font-weight: 500; 152 | color: var(--primary-white); 153 | `; 154 | 155 | const AuxOptionsToggleLabelSmallStationary = styled.span` 156 | font-size: 12px; 157 | color: var(--secondary-black); 158 | `; 159 | 160 | const AuxOptionsToggleLabelStationary = styled.span` 161 | font-size: 14px; 162 | font-weight: 500; 163 | color: var(--primary-black); 164 | `; 165 | 166 | type TAuxOptions = { 167 | deliveryTime: string; 168 | pickupTime: string; 169 | }; 170 | 171 | export default function AuxOptions({ deliveryTime, pickupTime }: TAuxOptions) { 172 | const isDelivery = useAppSelector((state) => state.deliverySlice.isDelivery); 173 | const dispatch = useAppDispatch(); 174 | 175 | return ( 176 | 177 | 178 | 179 | Save 180 | 181 | 182 | 183 | 184 | Group Order 185 | 186 | 187 | { 189 | dispatch(toggleDeliveryState()); 190 | }} 191 | aria-label="Selected delivery option" 192 | role="switch" 193 | aria-checked={isDelivery ? 'true' : 'false'} 194 | > 195 | e.stopPropagation()} 198 | role="none" 199 | > 200 | 201 | 202 | {isDelivery ? 'Delivery' : 'Pickup'} 203 | 204 | 205 | {isDelivery ? deliveryTime : pickupTime} 206 | 207 | 208 | 209 | 210 | 211 | 212 | Delivery 213 | 214 | 215 | {deliveryTime} 216 | 217 | 218 | 219 | 220 | Pickup 221 | 222 | 223 | {pickupTime} 224 | 225 | 226 | 227 | 228 | ); 229 | } 230 | -------------------------------------------------------------------------------- /components/StoreComponents/ItemOverlay/ItemCustomizationPanel/ItemCustomizationPanel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-bind */ 2 | /* eslint-disable @typescript-eslint/brace-style */ 3 | /* eslint-disable @typescript-eslint/no-shadow */ 4 | import styled from 'styled-components'; 5 | import { 6 | useState, useRef, useEffect, 7 | } from 'react'; 8 | import { Transition, TransitionStatus } from 'react-transition-group'; 9 | import Image from 'next/image'; 10 | import Shimmer from '../../../Placeholders/Shimmer'; 11 | import { useAppDispatch, useAppSelector } from '../../../../app-redux/hooks'; 12 | import { toggleIsModalOpen } from '../../../../app-redux/features/item/itemSlice'; 13 | import { 14 | addItemToCart, 15 | setStoreID, 16 | resetCartNewStore, 17 | setPageViewingStoreID, 18 | } from '../../../../app-redux/features/cart/cartSlice'; 19 | 20 | import X from '../../../Icons/XIcon'; 21 | import ThumbsUp from '../../../Icons/ThumbsUpIcon'; 22 | import ModalInputStepper from './ModalInputStepper/ModalInputStepper'; 23 | 24 | type TItemCustomizationPanel = { 25 | state: TransitionStatus; 26 | isModalOpen: boolean; 27 | }; 28 | 29 | const ItemCustomizationPanelWrapper = styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | width: 560px; 33 | min-height: 200px; 34 | background-color: var(--primary-white); 35 | opacity: 1; 36 | border-radius: 16px; 37 | /* padding: 16px; */ 38 | position: relative; 39 | transform: ${(props) => (props.state === 'entering' 40 | ? 'scale(0.95)' 41 | : props.state === 'entered' 42 | ? 'scale(1)' 43 | : props.state === 'exiting' 44 | ? 'scale(0.95)' 45 | : 'scale(0.95)')}; 46 | transition: transform 225ms ease-in-out; 47 | justify-content: space-between; 48 | 49 | @media screen and (max-width: 770px) { 50 | width: 480px; 51 | } 52 | 53 | @media screen and (max-width: 480px) { 54 | width: 100%; 55 | height: 100%; 56 | border-radius: 0; 57 | } 58 | `; 59 | 60 | const ItemCustomizationPanelMainWrapper = styled.div` 61 | padding: 16px; 62 | `; 63 | 64 | const ItemCustomizationPanelButtonClose = styled.button` 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | width: 24px; 69 | height: 24px; 70 | border: none; 71 | background-color: transparent; 72 | color: inherit; 73 | 74 | &:hover { 75 | cursor: pointer; 76 | } 77 | 78 | &:visited { 79 | color: inherit; 80 | background-color: inherit; 81 | } 82 | `; 83 | 84 | const ItemCustomizationPanelContentWrapper = styled.div` 85 | display: flex; 86 | flex-direction: column; 87 | row-gap: 10px; 88 | align-items: flex-start; 89 | margin: 33px 0; 90 | `; 91 | 92 | const ItemCustomizationPanelItemName = styled.h2` 93 | font-size: 32px; 94 | font-weight: 500; 95 | color: var(--primary-black); 96 | letter-spacing: -0.45px; 97 | `; 98 | 99 | const ItemCustomizationPanelStatsWrapper = styled.div` 100 | display: flex; 101 | column-gap: 4px; 102 | align-items: center; 103 | `; 104 | 105 | const ItemCustomizationPanelStats = styled.span` 106 | font-size: 14px; 107 | font-weight: 400; 108 | letter-spacing: -0.3px; 109 | color: var(--secondary-black); 110 | `; 111 | 112 | const ItemCustomizationPanelItemDescription = styled.span` 113 | font-size: 14px; 114 | font-weight: 400; 115 | color: var(--secondary-black); 116 | line-height: 1.4; 117 | margin-top: 10px; 118 | margin-bottom: 25px; 119 | `; 120 | 121 | const ItemCustomizationPanelImageWrapper = styled.div` 122 | display: flex; 123 | justify-content: center; 124 | align-items: center; 125 | width: 100%; 126 | height: 295px; 127 | overflow: hidden; 128 | position: relative; 129 | `; 130 | 131 | const ItemCustomizationPanelImage = styled(Image)` 132 | width: 100%; 133 | object-fit: cover; 134 | `; 135 | 136 | const ItemCustomizationPanelFooter = styled.div` 137 | display: flex; 138 | align-items: center; 139 | justify-content: flex-end; 140 | padding: 16px; 141 | width: 100%; 142 | height: 72px; 143 | box-shadow: rgba(0, 0, 0, 0.2) 0px calc(-1px) 15px; 144 | column-gap: 26px; 145 | 146 | @media screen and (max-width: 480px) { 147 | column-gap: 0; 148 | justify-content: center; 149 | } 150 | `; 151 | 152 | const ItemCustomizationPanelAddToCartButton = styled.button` 153 | width: 220px; 154 | height: 40px; 155 | display: flex; 156 | justify-content: center; 157 | align-items: center; 158 | background-color: var(--secondary-red); 159 | border-radius: 20px; 160 | transition: ease 0.15s; 161 | transition-property: background-color; 162 | padding: 0 25px; 163 | color: var(--primary-white); 164 | font-weight: 500; 165 | font-size: 16px; 166 | 167 | &:hover { 168 | background-color: var(--tertiary-red); 169 | transition: ease 0.15s; 170 | transition-property: background-color; 171 | cursor: pointer; 172 | } 173 | 174 | &:active { 175 | transition: 0.15s ease; 176 | transition-property: background-color; 177 | background-color: var(--quaternary-red); 178 | } 179 | 180 | @media screen and (max-width: 480px) { 181 | width: 192px; 182 | padding: 0 5px; 183 | } 184 | `; 185 | 186 | export default function ItemCustomizationPanel({ state, isModalOpen }: TItemCustomizationPanel) { 187 | // we need this local state to talk between modalinputstepper and add to cart button 188 | const [itemCounter, setItemCounter] = useState(1); 189 | 190 | const [isImageLoading, setIsImageLoading] = useState(true); 191 | 192 | const nodeRef = useRef(null); 193 | useEffect(() => { 194 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 195 | nodeRef.current && nodeRef.current.focus(); 196 | }, []); 197 | 198 | const dispatch = useAppDispatch(); 199 | // grabbing item data that was set when the user clicks on MenuItem 200 | const itemData = useAppSelector((state) => state.itemSlice.itemData); 201 | const cartStoreID = useAppSelector((state) => state.cartSlice.storeID); 202 | const pageViewingStoreID = useAppSelector( 203 | (state) => state.cartSlice.pageViewingStoreID 204 | ); 205 | const priceFormatter = new Intl.NumberFormat('en-US', { 206 | style: 'currency', 207 | currency: 'USD', 208 | }); 209 | 210 | function addToCartClickHandler() { 211 | const cartPayload = { 212 | itemID: itemData.itemID, 213 | quantity: itemCounter, 214 | }; 215 | // if the cart matches the currently viewed page's ID 216 | if (cartStoreID === pageViewingStoreID) { 217 | dispatch(addItemToCart(cartPayload)); 218 | dispatch(toggleIsModalOpen()); 219 | } 220 | // if the cart store is not defined meaning no items in cart, set the viewingID and then add 221 | else if (cartStoreID === undefined && !!pageViewingStoreID) { 222 | dispatch(setPageViewingStoreID(pageViewingStoreID)); 223 | dispatch(setStoreID(pageViewingStoreID)); 224 | dispatch(addItemToCart(cartPayload)); 225 | dispatch(toggleIsModalOpen()); 226 | } 227 | // if the cart's storeID doesn't match the viewingID, then start a new cart. 228 | else if (cartStoreID !== pageViewingStoreID && !!pageViewingStoreID) { 229 | dispatch(resetCartNewStore(pageViewingStoreID)); 230 | dispatch(addItemToCart(cartPayload)); 231 | dispatch(toggleIsModalOpen()); 232 | } 233 | } 234 | 235 | return ( 236 | 242 | {() => ( 243 | e.stopPropagation()} 247 | ref={nodeRef} 248 | // eslint-disable-next-line styled-components-a11y/no-noninteractive-tabindex 249 | tabIndex={isModalOpen ? 0 : -1} 250 | id="ItemCustomizationPanel" 251 | role="none" 252 | > 253 | 254 | dispatch(toggleIsModalOpen())} 256 | aria-label="Close Item Customization Modal" 257 | > 258 | 259 | 260 | 261 | 262 | {itemData?.itemName} 263 | 264 | {itemData?.ratingCount ? ( 265 | 266 | 267 | 268 | {itemData.ratingPercentage} 269 | % ( 270 | {itemData.ratingCount} 271 | ) 272 | 273 | 274 | ) : null} 275 | {itemData?.description ? ( 276 | 277 | {itemData?.description} 278 | 279 | ) : null} 280 | {itemData?.image.src ? ( 281 | 282 | {isImageLoading ? : null} 283 | setIsImageLoading(false)} 289 | /> 290 | 291 | ) : null} 292 | 293 | 294 | 295 | 299 | 302 | Add to Cart - 303 | {' '} 304 | {priceFormatter.format( 305 | itemData.price * itemCounter 306 | )} 307 | 308 | 309 | 310 | )} 311 | 312 | ); 313 | } 314 | -------------------------------------------------------------------------------- /components/SVGgraphics/TableOfFood.tsx: -------------------------------------------------------------------------------- 1 | export default function TableOfFood() { 2 | return ( 3 | 4 | 16 | 20 | 33 | 46 | 54 | 62 | 74 | 86 | 98 | 110 | 122 | 134 | 146 | 158 | 162 | 174 | 186 | 199 | 211 | 223 | 236 | 245 | 258 | 262 | 273 | 284 | 295 | 306 | 307 | 308 | 320 | 332 | 333 | ); 334 | } 335 | -------------------------------------------------------------------------------- /components/StoreComponents/HeroComponent/HeroComponent.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import Image from 'next/image'; 3 | import { useState } from 'react'; 4 | import { TRestaurantDataPrimary, TStorefrontData } from '../../../global'; 5 | import DashPassLabel from '../../Icons/DashPassLabel'; 6 | import DeliveryTile from './DeliveryTile/DeliveryTile'; 7 | import Star from '../../Icons/StarIcon'; 8 | import ClockIcon from '../../Icons/ClockIcon'; 9 | import Information from '../../Icons/InformationIcon'; 10 | import AuxOptions from './AuxOptions/AuxOptions'; 11 | import Shimmer from '../../Placeholders/Shimmer'; 12 | 13 | const HeroComponentWrapper = styled.section` 14 | display: flex; 15 | flex-direction: column; 16 | width: 928px; 17 | margin: 0 auto; 18 | position: relative; 19 | row-gap: 50px; 20 | 21 | @media screen and (max-width: 1300px) { 22 | padding: 0 16px; 23 | margin: 0; 24 | max-width: 100%; 25 | width: 100%; 26 | } 27 | 28 | @media screen and (max-width: 1185px) { 29 | width: calc(928px + 16px * 2); 30 | } 31 | 32 | @media screen and (max-width: 480px) { 33 | width: 100%; 34 | padding: 0; 35 | // padding is 16px for other elements 36 | } 37 | `; 38 | 39 | const HeroComponentImagesCollection = styled.div` 40 | position: relative; 41 | `; 42 | 43 | const HeroComponentImageWrapper = styled.div` 44 | width: 100%; 45 | height: 250px; 46 | border-radius: 15px; 47 | overflow: hidden; 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | position: relative; 52 | 53 | @media screen and (max-width: 480px) { 54 | border-radius: 0; 55 | } 56 | `; 57 | 58 | const HeroComponentStoreProfileImageArea = styled.div` 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | overflow: hidden; 63 | width: 80px; 64 | height: 80px; 65 | border-radius: 100%; 66 | background-color: var(--primary-white); 67 | position: absolute; 68 | filter: drop-shadow(0px 0px 5px rgba(0, 0, 0, .2)); 69 | bottom: -38px; 70 | left: 16px; 71 | z-index: 2; 72 | `; 73 | 74 | const HeroComponentStoreProfileImageWrapper = styled.div` 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | overflow: hidden; 79 | position: relative; 80 | width: 95%; 81 | height: 95%; 82 | border-radius: 100%; 83 | `; 84 | 85 | const HeroComponenSttoreProfileImage = styled(Image)` 86 | object-fit: cover; 87 | `; 88 | 89 | const HeroComponentImage = styled(Image)` 90 | object-fit: cover; 91 | `; 92 | 93 | const HeroComponentInformation = styled.div` 94 | display: flex; 95 | flex-direction: column; 96 | justify-content: flex-start; 97 | row-gap: 4px; 98 | width: 100%; 99 | 100 | @media screen and (max-width: 480px) { 101 | padding: 0 16px; 102 | } 103 | `; 104 | 105 | const HeroComponentRestaurantName = styled.h2` 106 | font-weight: 500; 107 | font-size: 40px; 108 | color: var(--primary-black); 109 | letter-spacing: -1.1px; 110 | 111 | @media screen and (max-width: 480px) { 112 | font-size: 32px; 113 | } 114 | `; 115 | 116 | const HeroComponentInformationPrimary = styled.div` 117 | display: flex; 118 | `; 119 | 120 | const HeroComponentInformationSpan = styled.span` 121 | display: flex; 122 | font-weight: 400; 123 | color: var(--quinary-gray); 124 | column-gap: 6px; 125 | font-size: 14px; 126 | align-items: center; 127 | 128 | @media screen and (max-width: 480px) { 129 | flex-wrap: wrap; 130 | width: 70%; 131 | } 132 | `; 133 | 134 | const HeroComponentInformationBullet = styled.span` 135 | font-size: 14px; 136 | font-weight: 400; 137 | color: var(--quinary-gray); 138 | `; 139 | 140 | const HeroComponentInformationRatingWrapper = styled.div` 141 | display: flex; 142 | align-items: center; 143 | column-gap: 5px; 144 | `; 145 | 146 | const HeroComponentInformationSecondary = styled.div` 147 | display: flex; 148 | margin: 6px 0; 149 | margin-bottom: 3.5px; 150 | `; 151 | 152 | const HeroComponentInformationTimeOpen = styled.span` 153 | display: flex; 154 | color: var(--primary-green); 155 | align-items: center; 156 | column-gap: 3px; 157 | `; 158 | 159 | const HeroComponentInformationTimeClosed = styled.span` 160 | display: flex; 161 | color: var(--primary-gold); 162 | align-items: center; 163 | column-gap: 3px; 164 | `; 165 | 166 | const HeroComponentInformationTertiary = styled.div` 167 | display: flex; 168 | `; 169 | 170 | const HeroComponentInformationQuaternary = styled.div` 171 | display: flex; 172 | align-items: center; 173 | justify-content: space-between; 174 | flex-wrap: wrap; 175 | `; 176 | 177 | type TRestaurantData = { 178 | restaurantData: TRestaurantDataPrimary; 179 | storefrontData: TStorefrontData 180 | }; 181 | 182 | export default function HeroComponent({ restaurantData, storefrontData }: TRestaurantData) { 183 | // already checking to see if storefrontData?.ratingCount exists. disable this lint rule 184 | // eslint-disable-next-line no-unsafe-optional-chaining 185 | const ratingsCountLocalized = storefrontData?.ratingCount && `${(Math.floor(storefrontData?.ratingCount / 1000) * 1000).toLocaleString()}+ ratings`; 186 | // in the future, validate time (isOpen) against a ground truth source instead of machine local. 187 | const currentTime = new Date(); 188 | 189 | // create openTime to compare 190 | const openTime = new Date(currentTime); 191 | openTime.setHours(storefrontData.operationHours[0].openHour); 192 | openTime.setMinutes(storefrontData.operationHours[0].openMinute); 193 | 194 | // create closeTime to compare 195 | const closeTime = new Date(currentTime); 196 | closeTime.setHours(storefrontData.operationHours[0].closeHour); 197 | closeTime.setMinutes(storefrontData.operationHours[0].openMinute); 198 | 199 | const isOpen = currentTime > openTime && currentTime < closeTime; 200 | 201 | const [isProfileImageLoading, setIsProfileImageLoading] = useState(true); 202 | const [isHeroImageLoading, setIsHeroImageLoading] = useState(true); 203 | 204 | return ( 205 | 206 | 207 | 208 | {isHeroImageLoading ? : null} 209 | { setIsHeroImageLoading(false); }} 217 | /> 218 | 219 | 220 | 221 | {isProfileImageLoading ? : null} 222 | { setIsProfileImageLoading(false); }} 230 | /> 231 | 232 | 233 | 234 | {/* immediate info area */} 235 | 236 | 237 | {restaurantData.restaurantName} 238 | 239 | 240 | 241 | {restaurantData.isDashPass ? 242 | <> 243 | 244 | 245 | • 246 | 247 | 248 | : 249 | null} 250 | {storefrontData.shortDescription ? 251 | <> 252 | {storefrontData.shortDescription} 253 | 254 | • 255 | 256 | 257 | : 258 | null} 259 | {storefrontData.averageRating ? 260 | 261 | {storefrontData.averageRating} 262 | 266 | {ratingsCountLocalized} 267 | 268 | • 269 | 270 | 271 | : 272 | null} 273 | {restaurantData.distance ? 274 | <> 275 | {restaurantData.distance} 276 | 277 | • 278 | 279 | 280 | : 281 | null} 282 | {storefrontData.priceRating ? 283 | <> 284 | { 285 | Array(storefrontData.priceRating).fill('$') 286 | } 287 | 288 | : 289 | null} 290 | 291 | 292 | 293 | 294 | {isOpen ? 295 | 296 | 297 | Open now 298 | 299 | : 300 | 301 | 302 | Closed now 303 | } 304 | 305 | 306 | 307 | 308 | Pricing & Fees 309 | {' '} 310 | 311 | 312 | 313 | 314 | 318 | 322 | 323 | 324 | 325 | ); 326 | } 327 | --------------------------------------------------------------------------------