diff --git a/package-lock.json b/package-lock.json index 2a7ffae..6b6cac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,10 @@ "name": "next-e-store", "version": "0.1.0", "dependencies": { + "@headlessui/react": "^1.7.18", "classnames": "^2.5.1", "next": "14.0.4", + "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", "react-select": "^5.8.0" @@ -1230,6 +1232,22 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@headlessui/react": { + "version": "1.7.18", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz", + "integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1876,6 +1894,31 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", + "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", + "dependencies": { + "@tanstack/virtual-core": "3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", + "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/cypress": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@testing-library/cypress/-/cypress-10.0.1.tgz", @@ -6967,6 +7010,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", + "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "peerDependencies": { + "next": "*", + "react": "*", + "react-dom": "*" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index 7dee97d..314c161 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,10 @@ "coverage-badge": "coverage-badger -r coverage/clover.xml -d coverage/" }, "dependencies": { + "@headlessui/react": "^1.7.18", "classnames": "^2.5.1", "next": "14.0.4", + "next-themes": "^0.2.1", "react": "^18", "react-dom": "^18", "react-select": "^5.8.0" diff --git a/src/app/globals.css b/src/app/globals.css index da43199..ad5855c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -13,7 +13,3 @@ button, a { @apply focus:border-black focus:outline-none focus:ring-2 focus:ring-black; } - -body { - background: var(--background); -} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0a6bae0..4c49f1b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Urbanist } from 'next/font/google'; import Header from '@/components/Header'; import { CartProvider } from '@/store'; +import { Provider as ThemeProvider } from '@/store/ThemeProvider'; import './globals.css'; @@ -23,10 +24,16 @@ export default function RootLayout({ return ( -
-
- {children} -
+ + +
+
+
+ {children} +
+
+
+
); diff --git a/src/app/page.tsx b/src/app/page.tsx index 9ab3d3a..68505c3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -24,7 +24,7 @@ export default async function Home() { return
{error}
; } return ( -
+
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index f05b1f1..b2d7c7a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -2,14 +2,26 @@ import classNames from 'classnames'; -type Props = React.ComponentPropsWithoutRef<'button'>; +type Props = React.ComponentPropsWithoutRef<'button'> & { + size?: 'small' | 'medium' | 'large'; +}; -export default function Button({ className, children, ...rest }: Props) { +export default function Button({ + className, + size = 'medium', + children, + ...rest +}: Props) { return ( +
+
+ + + {count} + + +
+
+ + + ); +} diff --git a/src/components/Color/index.tsx b/src/components/Color/index.tsx index a42eb88..d771d25 100644 --- a/src/components/Color/index.tsx +++ b/src/components/Color/index.tsx @@ -2,15 +2,26 @@ import classNames from 'classnames'; type Props = React.ComponentPropsWithoutRef<'div'> & { color: string; + size?: 'small' | 'medium' | 'large'; }; -export default function Color({ color, ...rest }: Props) { +export default function Color({ + color, + size = 'medium', + className, + ...rest +}: Props) { return (
diff --git a/src/components/ColorFilter/index.tsx b/src/components/ColorFilter/index.tsx index 407451a..5a0102e 100644 --- a/src/components/ColorFilter/index.tsx +++ b/src/components/ColorFilter/index.tsx @@ -52,15 +52,24 @@ export default function ProductFilter({ isClearable classNames={{ control: (state) => - classNames('!border-gray-300 !rounded-md', { - '!border-gray-800 !rounded-md !shadow-[0_0_0_1px_#000000]': - state.isFocused, - }), + classNames( + '!rounded-md bg-white dark:bg-gray-700 !border-gray-500 dark:!border-gray-600', + { + '!border-gray-800 dark:!border-gray-500 !rounded-md !shadow-[0_0_0_1px_#000000] dark:!shadow-[0_0_0_1px_#333]': + state.isFocused, + } + ), + placeholder: (state) => 'text-gray-700 dark:text-gray-300', + menu: (state) => '!bg-white dark:!bg-gray-900', option: (state) => - classNames('!bg-white', { - '!bg-gray-300 !text-black': state.isSelected, - '!bg-gray-200': state.isFocused, - }), + classNames( + '!bg-white dark:!bg-gray-900 !text-gray-900 dark:!text-gray-200', + { + '!bg-gray-400 !text-black': state.isSelected, + '!bg-gray-500 dark:!bg-gray-700': state.isFocused, + } + ), + singleValue: (state) => '!text-gray-900 dark:!text-gray-200', }} value={value} onChange={(option) => { diff --git a/src/components/CountDot/index.tsx b/src/components/CountDot/index.tsx new file mode 100644 index 0000000..406b7d6 --- /dev/null +++ b/src/components/CountDot/index.tsx @@ -0,0 +1,19 @@ +import classNames from 'classnames'; +import React from 'react'; + +type Props = { + count: number; + className?: string; +}; +export default function CountDot({ count, className }: Props) { + return ( + + {count} + + ); +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index bfbed78..b847b71 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -1,14 +1,21 @@ +'use client'; import Link from 'next/link'; import { Logo } from '@/components/Icons'; +import Cart from '@/components/Cart'; +import ThemeSwitch from '@/components/ThemeSwitch'; export default function Header() { return ( -
-
+
+
- +

E-Store

+
+ + +
); diff --git a/src/components/Icons/index.tsx b/src/components/Icons/index.tsx index 814859f..4803d83 100644 --- a/src/components/Icons/index.tsx +++ b/src/components/Icons/index.tsx @@ -44,18 +44,35 @@ export function Logo({ className }: IconProps) { viewBox="0 0 36.00010070884446 36" > - + - + ); } + +export function Cart({ className }: IconProps) { + return ( + + + + ); +} diff --git a/src/components/ProductItem/index.tsx b/src/components/ProductItem/index.tsx index 6343ddb..248f1b2 100644 --- a/src/components/ProductItem/index.tsx +++ b/src/components/ProductItem/index.tsx @@ -41,8 +41,8 @@ export default function ProductItem({ product, className }: Props) { className={classNames(className, 'flex flex-col')} data-testid={`product-${product.id}`} > -
-
+
+
-

+

£ {product.price.toFixed(2)} @@ -76,17 +76,15 @@ export default function ProductItem({ product, className }: Props) {

-
+
- {/*
-
-
-

Color:

-
-
-

Price:

-
-
-
- - - {count} - - -
- -
*/}
diff --git a/src/components/Products/Products.test.tsx b/src/components/Products/Products.test.tsx index 6536fff..bd36c56 100644 --- a/src/components/Products/Products.test.tsx +++ b/src/components/Products/Products.test.tsx @@ -27,7 +27,7 @@ describe('Products', () => { ); expect(screen.getByText('Products')).toBeDefined; - expect(screen.getByText('Cart Total:')).toBeDefined; + // expect(screen.getByText('Cart Total:')).toBeDefined; expect(screen.getByTestId('products-list').childNodes.length).toEqual( products.length ); diff --git a/src/components/Products/index.tsx b/src/components/Products/index.tsx index 410354a..34dc489 100644 --- a/src/components/Products/index.tsx +++ b/src/components/Products/index.tsx @@ -1,8 +1,7 @@ 'use client'; -import Cart from '@/components/Cart'; import ColorFilter from '@/components/ColorFilter'; import ProductList from '@/components/ProductList'; -import { useProductsStore } from '@/store'; +import { useCart, useProductsStore } from '@/store'; export default function Products() { const { products, filteredProducts, color, setColor } = useProductsStore(); @@ -10,7 +9,7 @@ export default function Products() { return ( <>
-

Products

+

Products

-
- +
+
+ +
); diff --git a/src/components/ThemeSwitch/index.tsx b/src/components/ThemeSwitch/index.tsx new file mode 100644 index 0000000..3d4b5ba --- /dev/null +++ b/src/components/ThemeSwitch/index.tsx @@ -0,0 +1,39 @@ +import React, { useContext } from 'react'; +import { useTheme } from 'next-themes'; +// import { ThemeContext } from '@/store/ThemeProvider'; +import Button from '@/components/Button'; + +export default function ThemeSwitch() { + // const { theme, toggleTheme } = useContext(ThemeContext); + const { theme, setTheme } = useTheme(); + return ( + + ); +} diff --git a/src/store/CartProvider.tsx b/src/store/CartProvider.tsx index 61f387c..4ea55ae 100644 --- a/src/store/CartProvider.tsx +++ b/src/store/CartProvider.tsx @@ -40,16 +40,23 @@ function updateCartItemCount( id: number, count: number ): CartStoreState['cart'] { - return state.cart.map((cartItem) => { - if (cartItem.id === id) { - return { - ...cartItem, - count: cartItem.count + count, - }; - } - return cartItem; - }); + return state.cart + .map((cartItem) => { + if (cartItem.id === id) { + return { + ...cartItem, + count: cartItem.count + count, + }; + } + return cartItem; + }) + .filter((cartItem) => cartItem.count > 0); +} + +function updateTotal(total: number, change: number): number { + return parseFloat((total + change).toFixed(2)); } + export function cartReducer( state: CartStoreState, action: CartAction @@ -61,13 +68,13 @@ export function cartReducer( if (existingItem) { return { ...state, - total: state.total + item.price, + total: updateTotal(state.total, item.price), cart: updateCartItemCount(state, item.id, 1), }; } return { ...state, - total: state.total + item.price, + total: updateTotal(state.total, item.price), cart: [...state.cart, { ...item, count: 1 }], }; case 'REMOVE_ITEM': @@ -76,7 +83,7 @@ export function cartReducer( } return { ...state, - total: state.total - existingItem.price, + total: updateTotal(state.total, -1 * item.price), cart: updateCartItemCount(state, existingItem.id, -1), }; case 'CLEAR_ITEM': @@ -85,7 +92,10 @@ export function cartReducer( } return { ...state, - total: state.total - existingItem.count * existingItem.price, + total: updateTotal( + state.total, + -1 * existingItem.count * existingItem.price + ), cart: state.cart.filter((cartItem) => cartItem.id !== existingItem.id), }; diff --git a/src/store/ThemeProvider.tsx b/src/store/ThemeProvider.tsx new file mode 100644 index 0000000..f4ce410 --- /dev/null +++ b/src/store/ThemeProvider.tsx @@ -0,0 +1,11 @@ +'use client'; +import { ThemeProvider } from 'next-themes'; +import { ThemeProviderProps } from 'next-themes/dist/types'; + +export function Provider(props: ThemeProviderProps) { + return ( + + {props.children} + + ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 66492bf..29a54d7 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,4 +1,4 @@ -import type { Config } from 'tailwindcss' +import type { Config } from 'tailwindcss'; const config: Config = { content: [ @@ -6,16 +6,17 @@ const config: Config = { './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], + darkMode: 'class', theme: { extend: { colors: { - background: "var(--background)", - grey: "var(--grey)", - 'dark-grey': "var(--dark-grey)", - 'soft-grey': "var(--soft-grey)", + background: 'var(--background)', + grey: 'var(--grey)', + 'dark-grey': 'var(--dark-grey)', + 'soft-grey': 'var(--soft-grey)', }, }, }, plugins: [], -} -export default config +}; +export default config;