diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 66d6dd3..4e2c84f 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -21,6 +21,7 @@ "@trpc/next": "^10.1.0", "@trpc/react-query": "^10.1.0", "@trpc/server": "^10.1.0", + "jotai": "^2.5.1", "next": "^13.1.6", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/apps/nextjs/src/atoms/cart.ts b/apps/nextjs/src/atoms/cart.ts new file mode 100644 index 0000000..57c1f10 --- /dev/null +++ b/apps/nextjs/src/atoms/cart.ts @@ -0,0 +1,66 @@ +import { atom } from "jotai"; +import { atomFamily, atomWithStorage, createJSONStorage } from "jotai/utils"; +import { productsAtom, singleProductAtomFamily } from "./products"; + +export const cartAtom = atomWithStorage>( + "cart", + {}, + createJSONStorage(() => localStorage), +); + +export const cartItemQuantityAtomFamily = atomFamily((id: number) => + atom( + (get) => { + return get(cartAtom)[id] ?? 0; + }, + (get, set, newValue: number) => { + const currentCart = get(cartAtom); + set(cartAtom, { ...currentCart, [id]: newValue }); + }, + ), +); + +export const cartTotalAtom = atom((get) => { + const currentCart = get(cartAtom); + const products = get(productsAtom); + + const ids = Object.keys(currentCart); + let total = 0; + + for (const pr of products) { + if (ids.includes(pr.id.toString())) { + total += (currentCart[pr.id] ?? 0) * pr.price.toNumber(); + } + } + + return total; +}); + +export const addItemToCartAtom = atomFamily( + ({ id, quantity }: { id: number; quantity: number }) => + atom(null, async (get, set) => { + const currentCart = get(cartAtom); + const currentQuantity = currentCart[id]; + const currentItemStock = + get(singleProductAtomFamily(id))?.stock.toNumber() ?? 0; + + const q = currentQuantity ? currentQuantity + quantity : quantity; + + if (q < 1) { + set(cartAtom, { ...currentCart, [id]: 1 }); + } else if (q <= currentItemStock) { + set(cartAtom, { ...currentCart, [id]: q }); + } else { + set(cartAtom, { ...currentCart, [id]: currentItemStock }); + } + }), +); + +export const deleteItemFromCartAtom = atomFamily(({ id }: { id: number }) => + atom(null, async (_, set) => { + set(cartAtom, async (prev) => { + const { [id]: toDelete, ...rest } = await prev; + return { ...rest }; + }); + }), +); diff --git a/apps/nextjs/src/atoms/products.ts b/apps/nextjs/src/atoms/products.ts new file mode 100644 index 0000000..f47a878 --- /dev/null +++ b/apps/nextjs/src/atoms/products.ts @@ -0,0 +1,16 @@ +import { AppRouter } from "@acme/api"; +import { inferProcedureOutput } from "@trpc/server"; +import { atom } from "jotai"; +import { atomFamily } from "jotai/utils"; + +export const productsAtom = atom< + inferProcedureOutput +>([]); + +export const singleProductAtomFamily = atomFamily((id: number) => + atom((get) => { + const product = get(productsAtom).filter((product) => product.id === id)[0]; + if (!product) return null; + return product; + }), +); diff --git a/apps/nextjs/src/atoms/store.ts b/apps/nextjs/src/atoms/store.ts new file mode 100644 index 0000000..41f299e --- /dev/null +++ b/apps/nextjs/src/atoms/store.ts @@ -0,0 +1,5 @@ +import { createStore } from "jotai"; + +const jotaiStore = createStore(); + +export default jotaiStore; diff --git a/apps/nextjs/src/components/ItemHeader/ItemHeader.tsx b/apps/nextjs/src/components/ItemHeader/ItemHeader.tsx index 7c3e44b..2f9d49a 100644 --- a/apps/nextjs/src/components/ItemHeader/ItemHeader.tsx +++ b/apps/nextjs/src/components/ItemHeader/ItemHeader.tsx @@ -11,9 +11,9 @@ function ItemHeader() {
    -
  • router.back()}>Home
  • -
  • Shop
  • -
  • About
  • +
  • router.push(`/`)}>Home
  • +
  • router.push(`/shoppingCart`)}>Cart
  • +
  • router.push(`/login`)}>Login
  • Contact
diff --git a/apps/nextjs/src/components/content/Content.tsx b/apps/nextjs/src/components/content/Content.tsx index d5eb4f4..97bfa64 100644 --- a/apps/nextjs/src/components/content/Content.tsx +++ b/apps/nextjs/src/components/content/Content.tsx @@ -2,6 +2,12 @@ import Aside from "../aside/Aside"; import styles from "./Content.module.css"; import { trpc } from "../../utils/trpc"; import { useRouter } from "next/router"; +import { useSetAtom } from "jotai"; +import { productsAtom } from "../../atoms/products"; +import { useEffect } from "react"; +import { inferProcedureOutput } from "@trpc/server"; +import { AppRouter } from "@acme/api"; +import { addItemToCartAtom, deleteItemFromCartAtom } from "../../atoms/cart"; interface ContentProps { filterValue: string; @@ -9,38 +15,70 @@ interface ContentProps { function Content({ filterValue }: ContentProps) { const { data } = trpc.item.all.useQuery(); - const router = useRouter(); + const setItemsAtom = useSetAtom(productsAtom); const filteredData = data?.filter((item) => item.name.toLowerCase().includes(filterValue.toLowerCase()), ) ?? []; + useEffect(() => { + setItemsAtom(data ?? []); + }, [data, setItemsAtom]); + return ( <>
{filteredData.map((item) => ( - router.push(`/item/${item.id}`)} - > - - product - -

{item.name}

-

${item.price.toFixed(2)}

- -
+ ))}
); } + +function ItemCard({ + item, +}: { + item: inferProcedureOutput[number]; +}) { + const router = useRouter(); + const addToCart = useSetAtom(addItemToCartAtom({ id: item.id, quantity: 1 })); + const deleteFromCart = useSetAtom(deleteItemFromCartAtom({ id: item.id })); + + return ( +
router.push(`/item/${item.id}`)} + > + + {" "} + {item.name} + +

{item.name}

+

${item.price.toFixed(2)}

+ + +
+ ); +} export default Content; diff --git a/apps/nextjs/src/components/loginContent/LoginContent.tsx b/apps/nextjs/src/components/loginContent/LoginContent.tsx index 4b22b7c..32361b7 100644 --- a/apps/nextjs/src/components/loginContent/LoginContent.tsx +++ b/apps/nextjs/src/components/loginContent/LoginContent.tsx @@ -1,5 +1,4 @@ import styles from "./LoginContent.module.css"; -import RegisterForm from "../RegisterForm/RegisterForm"; import { useRouter } from "next/router"; function LoginContent() { const router = useRouter(); diff --git a/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.module.css b/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.module.css index 54f2685..d934310 100644 --- a/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.module.css +++ b/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.module.css @@ -24,11 +24,14 @@ } .image { height: 100px; + width: 100px; border: 1px solid gray; display: flex; + justify-content: center; } .details h3 { font-size: large; + width: 250px; } .quantityBox { height: 30px; @@ -36,6 +39,7 @@ } .quantityInput { height: 100%; + width: 80px; padding-left: 10px; text-align: center; } diff --git a/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.tsx b/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.tsx index 553bf4c..fcabc98 100644 --- a/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.tsx +++ b/apps/nextjs/src/components/shoppingCartContent/ShoppingCartContent.tsx @@ -1,130 +1,36 @@ +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import styles from "./ShoppingCartContent.module.css"; +import { + cartAtom, + cartItemQuantityAtomFamily, + cartTotalAtom, + deleteItemFromCartAtom, +} from "../../atoms/cart"; +import { singleProductAtomFamily } from "../../atoms/products"; +import { it } from "node:test"; + function ShoppingCartContent() { + const cart = useAtomValue(cartAtom); + const total = useAtomValue(cartTotalAtom); + return ( <>

Shopping Cart

    -
  • -
    - - itemImage - - -

    Coca cola con pan y leche

    -

    price

    -
    - -
    - -
    -
    - totalPrice - - - -
    -
  • -
  • -
    - - itemImage - - -

    Coca cola con pan y leche

    -

    price

    -
    - -
    - -
    -
    - totalPrice - - - -
    -
  • -
  • -
    - - itemImage - - -

    Coca cola con pan y leche

    -

    price

    -
    - -
    - -
    -
    - totalPrice - - - -
    -
  • -
  • -
    - - itemImage - - -

    Coca cola con pan y leche

    -

    price

    -
    - -
    - -
    -
    - totalPrice - - - -
    -
  • + {Object.keys(cart).map((cartItemId) => ( +
  • + +
  • + ))}
@@ -132,4 +38,49 @@ function ShoppingCartContent() { ); } + +function CartItem({ id }: { id: number }) { + const item = useAtomValue(singleProductAtomFamily(id)); + const [quantity, setQuantity] = useAtom(cartItemQuantityAtomFamily(id)); + const deleteFromCart = useSetAtom(deleteItemFromCartAtom({ id })); + + return ( +
+ + {item?.name} + + +

{item?.name}

+

$ {item?.price.toFixed(2)}

+
+ +
+ setQuantity(Number(e.currentTarget.value))} + /> +
+
+ + $ {item?.price.toNumber() ?? 0 * quantity} + + + + +
+ ); +} export default ShoppingCartContent; diff --git a/apps/nextjs/src/pages/_app.tsx b/apps/nextjs/src/pages/_app.tsx index 6bc7a77..6d62358 100644 --- a/apps/nextjs/src/pages/_app.tsx +++ b/apps/nextjs/src/pages/_app.tsx @@ -3,11 +3,15 @@ import "../styles/globals.css"; import type { AppType } from "next/app"; import { ClerkProvider } from "@clerk/nextjs"; import { trpc } from "../utils/trpc"; +import { Provider } from "jotai"; +import jotaiStore from "../atoms/store"; const MyApp: AppType = ({ Component, pageProps: { ...pageProps } }) => { return ( - + + + ); }; diff --git a/apps/nextjs/src/pages/login.tsx b/apps/nextjs/src/pages/login.tsx index 4eb78d5..788c06f 100644 --- a/apps/nextjs/src/pages/login.tsx +++ b/apps/nextjs/src/pages/login.tsx @@ -1,7 +1,9 @@ import LoginContent from "../components/loginContent/LoginContent"; +import ItemHeader from "../components/ItemHeader/ItemHeader"; function Login() { return ( <> + ); diff --git a/apps/nextjs/src/pages/register.tsx b/apps/nextjs/src/pages/register.tsx index c661bbf..6e69996 100644 --- a/apps/nextjs/src/pages/register.tsx +++ b/apps/nextjs/src/pages/register.tsx @@ -1,8 +1,9 @@ import RegisterForm from "../components/RegisterForm/RegisterForm"; - +import ItemHeader from "../components/ItemHeader/ItemHeader"; function Register() { return ( <> + ); diff --git a/apps/nextjs/src/pages/shoppingCart.tsx b/apps/nextjs/src/pages/shoppingCart.tsx index 47a1cb1..18cc24b 100644 --- a/apps/nextjs/src/pages/shoppingCart.tsx +++ b/apps/nextjs/src/pages/shoppingCart.tsx @@ -1,6 +1,19 @@ import ShoppingCartContent from "../components/shoppingCartContent/ShoppingCartContent"; import ItemHeader from "../components/ItemHeader/ItemHeader"; +import { trpc } from "../utils/trpc"; +import { useEffect } from "react"; +import { useSetAtom } from "jotai"; +import { productsAtom } from "../atoms/products"; + function ShoppingCart() { + const { data } = trpc.item.all.useQuery(); + + const setItemsAtom = useSetAtom(productsAtom); + + useEffect(() => { + setItemsAtom(data ?? []); + }, [data, setItemsAtom]); + return ( <> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b714b8..9c25c15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,6 +194,9 @@ importers: '@trpc/server': specifier: ^10.1.0 version: 10.5.0 + jotai: + specifier: ^2.5.1 + version: 2.5.1(@types/react@18.0.26)(react@18.2.0) next: specifier: ^13.1.6 version: 13.1.6(react-dom@18.2.0)(react@18.2.0)