Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed: Issues #1 to #8 #70

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

146 changes: 95 additions & 51 deletions src/Components/Cart.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/outline";
import React, { Fragment } from "react";
import { Dialog, Transition } from '@headlessui/react'
import { XIcon, ShoppingCartIcon } from '@heroicons/react/outline'
import React, { Fragment } from 'react'

export default function Cart({ open, setOpen, cart, updateCart }) {
const subTotal = cart.reduce((s, p) => p.price * p.quantity + s, 0)

return (
<Transition.Root show={open} as={Fragment}>
<Transition.Root
show={open}
as={Fragment}
>
<Dialog
as="div"
className="fixed inset-0 overflow-hidden z-10"
onClose={() => {
setOpen;
setOpen
}}
>
<div className="absolute inset-0 overflow-hidden">
@@ -21,6 +26,7 @@ export default function Cart({ open, setOpen, cart, updateCart }) {
leave="ease-in-out duration-500"
leaveFrom="opacity-100"
leaveTo="opacity-0"
onClick={() => setOpen(false)}
>
<Dialog.Overlay className="absolute inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
@@ -39,75 +45,112 @@ export default function Cart({ open, setOpen, cart, updateCart }) {
<div className="flex h-full flex-col overflow-y-scroll bg-white shadow-xl">
<div className="flex-1 overflow-y-auto py-6 px-4 sm:px-6">
<div className="flex items-start justify-between">
<Dialog.Title className="text-lg font-medium text-gray-900"> Shopping cart </Dialog.Title>
<Dialog.Title className="text-lg font-medium text-gray-900">
{' '}
Shopping cart{' '}
</Dialog.Title>
<div className="ml-3 flex h-7 items-center">
<button
type="button"
className="-m-2 p-2 text-gray-400 hover:text-gray-500"
onClick={() => setOpen(false)}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
<XIcon
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
</div>

<div className="mt-8">
<div
className={
cart.length === 0
? 'mt-8 flex h-full items-center justify-center'
: 'mt-8'
}
>
<div className="flow-root">
<ul role="list" className="-my-6 divide-y divide-gray-200">
{cart.map((product) => (
<li key={product.id} className="flex py-6">
<div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
<img
src={product.imageSrc}
alt={product.imageAlt}
className="h-full w-full object-cover object-center"
/>
</div>
{cart.length === 0 ? (
<div className="flex flex-col items-center">
<ShoppingCartIcon
className="w-12"
aria-hidden="true"
/>
<span className="pt-2">Your Cart is Empty.</span>
</div>
) : (
<ul
role="list"
className="-my-6 divide-y divide-gray-200"
>
{cart.map((product) => (
<li
key={product.id}
className="flex py-6"
>
<div className="h-24 w-24 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
<img
src={product.imageSrc}
alt={product.imageAlt}
className="h-full w-full object-cover object-center"
/>
</div>

<div className="ml-4 flex flex-1 flex-col">
<div>
<div className="flex justify-between text-base font-medium text-gray-900">
<h3>{product.name}</h3>
<p className="ml-4">${product.price}</p>
<div className="ml-4 flex flex-1 flex-col">
<div>
<div className="flex justify-between text-base font-medium text-gray-900">
<h3>{product.name}</h3>
<p className="ml-4">${product.price}</p>
</div>
</div>
</div>
<div className="flex flex-1 items-end justify-between text-sm">
<p className="text-gray-500">Qty {product.quantity}</p>
<div className="flex flex-1 items-end justify-between text-sm">
<p className="text-gray-500">
Qty {product.quantity}
</p>

<div className="flex">
<button
onClick={() => {
let newCart = cart.filter((p) => {
if (p.id === product.id) {
p.quantity -= 1;
}
<div className="flex">
<button
onClick={() => {
let newCart = cart.filter((p) => {
if (p.id === product.id) {
p.quantity -= 1
}

return p.quantity > 0;
});
updateCart(newCart);
}}
type="button"
className="font-medium text-gray-500 hover:text-black"
>
Remove
</button>
return p.quantity > 0
})
updateCart(newCart)
// Issue #2
localStorage.setItem(
'cart',
JSON.stringify(newCart)
)
}}
type="button"
className="font-medium text-gray-500 hover:text-black"
>
Remove
</button>
</div>
</div>
</div>
</div>
</li>
))}
</ul>
</li>
))}
</ul>
)}
</div>
</div>
</div>

<div className="border-t border-gray-200 py-6 px-4 sm:px-6">
<div className="flex justify-between text-base font-medium text-gray-900">
<p>Subtotal</p>
<p>$262.00</p>
<p>${subTotal.toFixed(2)}</p>
</div>
<p className="mt-0.5 text-sm text-gray-500">Shipping and taxes calculated at checkout.</p>
<p className="mt-0.5 text-sm text-gray-500">
Shipping and taxes calculated at checkout.
</p>
<div className="mt-6">
<a
href="#"
@@ -118,13 +161,14 @@ export default function Cart({ open, setOpen, cart, updateCart }) {
</div>
<div className="mt-6 flex justify-center text-center text-sm text-gray-500">
<p>
or{" "}
or{' '}
<button
type="button"
className="font-medium text-gray-700 hover:text-black"
onClick={() => setOpen(false)}
>
Continue Shopping<span aria-hidden="true"> &rarr;</span>
Continue Shopping
<span aria-hidden="true"> &rarr;</span>
</button>
</p>
</div>
@@ -136,5 +180,5 @@ export default function Cart({ open, setOpen, cart, updateCart }) {
</div>
</Dialog>
</Transition.Root>
);
)
}
26 changes: 18 additions & 8 deletions src/Components/NavBar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ShoppingBagIcon } from "@heroicons/react/outline";
import React from "react";
import { ShoppingBagIcon } from '@heroicons/react/outline'
import React from 'react'

export default function NavBar({ setOpen }) {
export default function NavBar({ setOpen, productsInCart }) {
return (
<div className="bg-white">
<header className="relative">
@@ -23,7 +23,10 @@ export default function NavBar({ setOpen }) {
</div>

{/* Logo (lg-) */}
<a href="#" className="lg:hidden">
<a
href="#"
className="lg:hidden"
>
<span className="sr-only">Workflow</span>
<img
src="https://tailwindui.com/img/logos/workflow-mark.svg?color=black&shade=600"
@@ -36,13 +39,20 @@ export default function NavBar({ setOpen }) {
<div className="flex items-center lg:ml-8">
{/* Cart Icon */}
<div className="ml-4 flow-root lg:ml-8">
<button onClick={() => setOpen(true)} className="group -m-2 p-2 flex items-center">
<button
onClick={() => setOpen(true)}
className="group -m-2 p-2 flex items-center"
>
<ShoppingBagIcon
className="flex-shrink-0 h-6 w-6 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
<span className="ml-2 text-sm font-medium text-gray-700 group-hover:text-gray-800">0</span>
<span className="sr-only">items in cart, view bag</span>
<span className="ml-2 text-sm font-medium text-gray-700 group-hover:text-gray-800">
{productsInCart}
</span>
<span className="sr-only">
items in cart, view bag
</span>
</button>
</div>
</div>
@@ -54,5 +64,5 @@ export default function NavBar({ setOpen }) {
</nav>
</header>
</div>
);
)
}
110 changes: 91 additions & 19 deletions src/Components/ProductFilters.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ChevronDownIcon, FilterIcon } from "@heroicons/react/solid";
import React, { Fragment } from "react";
import { Disclosure, Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon, FilterIcon } from '@heroicons/react/solid'
import React, { Fragment, useEffect, useState } from 'react'

function classNames(...classes) {
return classes.filter(Boolean).join(" ");
return classes.filter(Boolean).join(' ')
}

export default function ProductFilters({ filterOptions, setFilterOptions, sortOptions, setSortOptions }) {
export default function ProductFilters({
filterOptions,
setFilterOptions,
sortOptions,
setSortOptions,
handleClear,
}) {
function handleFilterChange(event) {
const filter = event.target.value

if (event.target.name.startsWith('color')) {
// Color filter changed
const newColorArray = filterOptions.color.map((c) =>
c.value === filter ? { ...c, checked: !c.checked } : c
)
setFilterOptions({ ...filterOptions, color: newColorArray })
} else {
// Price filter changed
const newPriceArray = filterOptions.price.map((p) =>
p.minValue === Number(filter) ? { ...p, checked: !p.checked } : p
)
setFilterOptions({ ...filterOptions, price: newPriceArray })
}
}
function countFilter() {
return (
filterOptions.price.reduce((s, p) => Number(p.checked) + s, 0) +
filterOptions.color.reduce((s, c) => Number(c.checked) + s, 0)
)
}

return (
<Disclosure
as="section"
aria-labelledby="filter-heading"
className="relative z-10 border-gray-200 grid items-center"
>
<h2 id="filter-heading" className="sr-only">
<h2
id="filter-heading"
className="sr-only"
>
Filters
</h2>
<div className="relative col-start-1 row-start-1 py-4">
@@ -24,11 +57,15 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
className="flex-none w-5 h-5 mr-2 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
0 Filters
{countFilter()} Filters
</Disclosure.Button>
</div>
<div className="pl-6">
<button type="button" className="text-gray-500">
<button
type="button"
className="text-gray-500"
onClick={handleClear}
>
Clear all
</button>
</div>
@@ -41,16 +78,23 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
<legend className="block font-medium">Price</legend>
<div className="pt-6 space-y-6 sm:pt-4 sm:space-y-4">
{filterOptions.price.map((option, optionIdx) => (
<div key={option.minValue} className="flex items-center text-base sm:text-sm">
<div
key={option.minValue}
className="flex items-center text-base sm:text-sm"
>
<input
id={`price-${optionIdx}`}
name="price[]"
defaultValue={option.minValue}
type="checkbox"
className="flex-shrink-0 h-4 w-4 border-gray-300 rounded text-black focus:ring-black"
defaultChecked={option.checked}
checked={option.checked}
onChange={handleFilterChange}
/>
<label htmlFor={`price-${optionIdx}`} className="ml-3 min-w-0 flex-1 text-gray-600">
<label
htmlFor={`price-${optionIdx}`}
className="ml-3 min-w-0 flex-1 text-gray-600"
>
{option.label}
</label>
</div>
@@ -61,16 +105,24 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
<legend className="block font-medium">Color</legend>
<div className="pt-6 space-y-6 sm:pt-4 sm:space-y-4">
{filterOptions.color.map((option, optionIdx) => (
<div key={option.value} className="flex items-center text-base sm:text-sm">
<div
key={option.value}
className="flex items-center text-base sm:text-sm"
>
<input
id={`color-${optionIdx}`}
name="color[]"
defaultValue={option.value}
type="checkbox"
className="flex-shrink-0 h-4 w-4 border-gray-300 rounded text-black focus:ring-black"
defaultChecked={option.checked}
checked={option.checked}
onChange={handleFilterChange}
/>
<label htmlFor={`color-${optionIdx}`} className="ml-3 min-w-0 flex-1 text-gray-600">

<label
htmlFor={`color-${optionIdx}`}
className="ml-3 min-w-0 flex-1 text-gray-600"
>
{option.label}
</label>
</div>
@@ -82,7 +134,10 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
</Disclosure.Panel>
<div className="col-start-1 row-start-1 py-4">
<div className="flex justify-end max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Menu as="div" className="relative inline-block">
<Menu
as="div"
className="relative inline-block"
>
<div className="flex">
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
Sort
@@ -110,11 +165,28 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
<button
onClick={() => {
// TODO
const clikedOption = sortOptions.find(
(o) => o.name === option.name
)
const otherOption = sortOptions.find(
(o) => o.name !== option.name
)
if (!clikedOption.current) {
clikedOption.current = true
if (otherOption.current) {
otherOption.current = false
}
} else {
clikedOption.current = false
}
setSortOptions([clikedOption, otherOption])
}}
className={classNames(
option.current ? "font-medium text-gray-900" : "text-gray-500",
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm"
option.current
? 'font-medium text-gray-900'
: 'text-gray-500',
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm'
)}
>
{option.name}
@@ -129,5 +201,5 @@ export default function ProductFilters({ filterOptions, setFilterOptions, sortOp
</div>
</div>
</Disclosure>
);
)
}
142 changes: 103 additions & 39 deletions src/Components/ProductTable.jsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,119 @@
import React, { useEffect, useState } from "react";
import ProductFilters from "./ProductFilters";
import React, { useEffect, useState } from 'react'
import ProductFilters from './ProductFilters'

const getDefaultFilterOptions = () => {
return {
price: [
{ minValue: 0, maxValue: 25, label: "$0 - $25", checked: false },
{ minValue: 25, maxValue: 50, label: "$25 - $50", checked: false },
{ minValue: 50, maxValue: 75, label: "$50 - $75", checked: false },
{ minValue: 75, maxValue: Number.MAX_VALUE, label: "$75+", checked: false },
{ minValue: 0, maxValue: 25, label: '$0 - $25', checked: false },
{ minValue: 25, maxValue: 50, label: '$25 - $50', checked: false },
{ minValue: 50, maxValue: 75, label: '$50 - $75', checked: false },
{
minValue: 75,
maxValue: Number.MAX_VALUE,
label: '$75+',
checked: false,
},
],
color: [
{ value: "beige", label: "Beige", checked: false },
{ value: "green", label: "Green", checked: false },
{ value: "white", label: "White", checked: false },
{ value: "black", label: "Black", checked: false },
{ value: "gray", label: "Gray", checked: false },
{ value: "teal", label: "Teal", checked: false },
{ value: 'beige', label: 'Beige', checked: false },
{ value: 'green', label: 'Green', checked: false },
{ value: 'white', label: 'White', checked: false },
{ value: 'black', label: 'Black', checked: false },
{ value: 'gray', label: 'Gray', checked: false },
{ value: 'teal', label: 'Teal', checked: false },
],
};
};
}
}

const getDefaultSortOptions = () => {
return [
{ name: "Price", current: false },
{ name: "Newest", current: false },
];
};
{ name: 'Price', current: false },
{ name: 'Newest', current: false },
]
}

export default function ProductTable({ cart, updateCart }) {
let [products, setProducts] = useState([]);
let [products, setProducts] = useState([])

const [filterOptions, setFilterOptions] = useState(getDefaultFilterOptions());
const [sortOptions, setSortOptions] = useState(getDefaultSortOptions());
const [filterOptions, setFilterOptions] = useState(getDefaultFilterOptions())
const [sortOptions, setSortOptions] = useState(getDefaultSortOptions())

useEffect(() => {
let fetchProducts = async () => {
console.info("Fetching Products...");
let res = await fetch("http://localhost:3001/products");
let body = await res.json();
setProducts(body);
};
fetchProducts();
});
console.info('Fetching Products...')
let res = await fetch('http://localhost:3001/products')
let body = await res.json()

setProducts(body)
}
fetchProducts()
}, [])

// Filter products
const filterProducts = () => {
let filteredProducts = products
const colorFilter = filterOptions.color
.filter((c) => c.checked)
.map((c) => c.value)
const priceFilter = filterOptions.price
.filter((p) => p.checked)
.map((p) => {
return { minF: p.minValue, maxF: p.maxValue }
})

if (colorFilter.length > 0)
filteredProducts = filteredProducts.filter((p) =>
colorFilter.includes(p.color)
)
if (priceFilter.length > 0) {
filteredProducts = filteredProducts.filter((p) =>
priceFilter.some(({ minF, maxF }) => p.price >= minF && p.price <= maxF)
)
}
return filteredProducts
}

// Sort products
const sortProducts = (p) => {
const price = sortOptions.find((o) => o.name === 'Price')
const newest = sortOptions.find((o) => o.name === 'Newest')

if (price.current) {
return p.sort((a, b) => a.price - b.price)
} else if (newest.current) {
return p.sort((a, b) => a.releaseDate - b.releaseDate)
} else {
return p
}
}

function handleClear(event) {
setFilterOptions(getDefaultFilterOptions())
}

const filteredProducts = filterProducts()
const productsToShow = sortProducts(filteredProducts)

return (
<div className="bg-white">
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:max-w-7xl lg:px-8">
<h2 className="sr-only">Products</h2>
<ProductFilters {...{ filterOptions, setFilterOptions, sortOptions, setSortOptions }} />
<ProductFilters
{...{
filterOptions,
setFilterOptions,
sortOptions,
setSortOptions,
handleClear,
}}
/>

<div className="grid grid-cols-1 gap-y-10 sm:grid-cols-2 gap-x-6 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
{products.map((product) => (
<a key={product.id} className="group">
{productsToShow.map((product) => (
<a
key={product.id}
className="group"
>
<div className="w-full aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden xl:aspect-w-7 xl:aspect-h-8">
<img
src={product.imageSrc}
@@ -62,31 +124,33 @@ export default function ProductTable({ cart, updateCart }) {
type="button"
className="hidden group-hover:block group-hover:opacity-50 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-black"
onClick={() => {
let newCart = cart.slice();
let newCart = cart.slice()

if (!newCart.includes(product)) {
product.quantity = 1;
newCart.push(product);
product.quantity = 1
newCart.push(product)
} else {
newCart.map((p) => {
if (p.id === product.id) {
p.quantity += 1;
p.quantity += 1
}
});
})
}

updateCart(newCart);
updateCart(newCart)
}}
>
Add To Cart
</button>
</div>
<h3 className="mt-4 text-sm text-gray-700">{product.name}</h3>
<p className="mt-1 text-lg font-medium text-gray-900">${product.price}</p>
<p className="mt-1 text-lg font-medium text-gray-900">
${product.price}
</p>
</a>
))}
</div>
</div>
</div>
);
)
}
32 changes: 23 additions & 9 deletions src/Pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import React, { useState } from "react";
import Cart from "../Components/Cart";
import NavBar from "../Components/NavBar";
import ProductTable from "../Components/ProductTable";
import React, { useEffect, useState } from 'react'
import Cart from '../Components/Cart'
import NavBar from '../Components/NavBar'
import ProductTable from '../Components/ProductTable'

function Home() {
const [open, setOpen] = useState(false);
const [cart, updateCart] = useState([]);
const [open, setOpen] = useState(false)
const [cart, updateCart] = useState([])

// Issue #2
useEffect(() => {
const storedCart = localStorage.getItem('cart')
if (storedCart) {
updateCart(JSON.parse(storedCart))
}
}, [])

useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart))
}, [cart])

const productsInCart = cart.reduce((s, p) => p.quantity + s, 0)

return (
<main>
<NavBar {...{ setOpen }} />
<NavBar {...{ setOpen, productsInCart }} />
<Cart {...{ open, setOpen, cart, updateCart }} />
<ProductTable {...{ cart, updateCart }} />
</main>
);
)
}

export default Home;
export default Home
1 change: 1 addition & 0 deletions todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#1 fix that only one option is active