Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Diego Romero committed Dec 1, 2023
2 parents 9ee13f5 + 826a94c commit 77613a3
Show file tree
Hide file tree
Showing 21 changed files with 548 additions and 186 deletions.
5 changes: 4 additions & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
"@acme/db": "*",
"@acme/tailwind-config": "*",
"@clerk/nextjs": "^4.11.0",
"@hookform/resolvers": "^3.3.2",
"@tanstack/react-query": "^4.16.1",
"@trpc/client": "^10.1.0",
"@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",
"zod": "^3.18.0"
"react-hook-form": "^7.48.2",
"zod": "^3.20.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
Expand Down
66 changes: 66 additions & 0 deletions apps/nextjs/src/atoms/cart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { atom } from "jotai";
import { atomFamily, atomWithStorage, createJSONStorage } from "jotai/utils";
import { productsAtom, singleProductAtomFamily } from "./products";

export const cartAtom = atomWithStorage<Record<number, number>>(
"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 };
});
}),
);
16 changes: 16 additions & 0 deletions apps/nextjs/src/atoms/products.ts
Original file line number Diff line number Diff line change
@@ -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<AppRouter["item"]["all"]>
>([]);

export const singleProductAtomFamily = atomFamily((id: number) =>
atom((get) => {
const product = get(productsAtom).filter((product) => product.id === id)[0];
if (!product) return null;
return product;
}),
);
5 changes: 5 additions & 0 deletions apps/nextjs/src/atoms/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createStore } from "jotai";

const jotaiStore = createStore();

export default jotaiStore;
18 changes: 14 additions & 4 deletions apps/nextjs/src/components/ItemHeader/ItemHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useRouter } from "next/router";
import styles from "./Header.module.css";
import { useAuth } from "@clerk/nextjs";

function ItemHeader() {
const router = useRouter();
const { isSignedIn, signOut } = useAuth();
return (
<>
<header className={styles.header}>
Expand All @@ -11,10 +13,18 @@ function ItemHeader() {
</span>
<span className={styles.listSpan}>
<ul>
<li onClick={() => router.back()}>Home</li>
<li>Shop</li>
<li>About</li>
<li>Contact</li>
<li onClick={() => router.push(`/`)}>Home</li>
<li onClick={() => router.push(`/shoppingCart`)}>Cart</li>
<li
onClick={() =>
isSignedIn
? signOut(() => router.push(`/`))
: router.push(`/login`)
}
>
{isSignedIn ? "logout" : "login"}
</li>
<li onClick={() => router.push(`/orders`)}>Orders</li>
</ul>
</span>
</header>
Expand Down
10 changes: 10 additions & 0 deletions apps/nextjs/src/components/RegisterForm/RegisterForm.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@
.cancelbtn {
background-color: red;
}
.errorMsg {
margin-bottom: 10px;
--tw-border-opacity: 1;
border-color: rgb(248 113 113 / var(--tw-border-opacity));
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity));
padding: 0.5rem;
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}
123 changes: 85 additions & 38 deletions apps/nextjs/src/components/RegisterForm/RegisterForm.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,94 @@
import styles from "./RegisterForm.module.css";
import { useRouter } from "next/router";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useSignUp } from "@clerk/nextjs";
import { useState } from "react";

const signUpFormValidator = z.object({
email: z.string().email(),
password: z.string().min(8, "Password must be at least 8 characters"),
});

export type SignUpFormSchema = Zod.infer<typeof signUpFormValidator>;

function RegisterForm() {
const router = useRouter();

const { register, handleSubmit, setError, formState } =
useForm<SignUpFormSchema>({
resolver: zodResolver(signUpFormValidator),
mode: "onBlur",
});
const { signUp, setActive } = useSignUp();
const [isLoading, setIsLoading] = useState(false);

const onSubmit = handleSubmit(async (vals) => {
if (!signUp) return;
setIsLoading(true);

try {
const result = await signUp.create({
emailAddress: vals.email,
password: vals.password,
});

if (result?.status === "missing_requirements") {
alert("mising");
}

if (result?.status === "complete") {
setActive({ session: result.createdSessionId });
router.push("/");
}
} catch (err) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setError("root", { message: err.errors[0].message });
}
setIsLoading(false);
});

return (
<>
<form action="" method="post">
<div className={styles.container}>
<h1>Sign Up</h1>
<hr />

<label htmlFor="email">Email</label>
<input type="text" placeholder="Enter Email" name="email" required />

<label htmlFor="psw">Password</label>
<input
type="password"
placeholder="Enter Password"
name="psw"
required
/>

<label htmlFor="psw-repeat"> Repeat Password </label>
<input
type="password"
placeholder="Repeat Password"
name="psw-repeat"
required
/>
<div className={styles.buttonDiv}>
<button
type="button"
className={styles.cancelbtn}
onClick={() => router.back()}
>
Cancel
</button>
<button type="submit" className={styles.signupbtn}>
Sign Up
</button>
</div>
</div>
<main className={styles.container}>
<h1>Sign Up</h1>
<hr />
{formState.errors.root && (
<p className={styles.errorMsg}>{formState.errors.root?.message}</p>
)}

<form onSubmit={onSubmit}>
<label htmlFor="email">Email</label>
<input
type="email"
placeholder="[email protected]"
required
{...register("email")}
/>
<label htmlFor="psw">Password</label>
<input
type="password"
placeholder="Enter Password"
required
{...register("password")}
/>
{/* <div className={styles.buttonDiv}> */}
<button
type={"submit"}
className={styles.signupbtn}
disabled={isLoading}
>
Sign Up
</button>
{/* <button onClick={() => router.push(`/login`)}> Cancel </button>
</div> */}
</form>
</>

<p>
have an account? <a onClick={() => router.push(`/login`)}>Log in</a>
</p>
</main>
);
}
export default RegisterForm;
74 changes: 58 additions & 16 deletions apps/nextjs/src/components/content/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,87 @@ 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;
}

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 (
<>
<div className={styles.container}>
<Aside></Aside>
<main>
{filteredData.map((item) => (
<span
className={styles.card}
key={item.id}
onClick={() => router.push(`/item/${item.id}`)}
>
<picture>
<img
src={item.image_url != null ? item.image_url : ""}
alt="product"
/>
</picture>
<h3>{item.name}</h3>
<p>${item.price.toFixed(2)}</p>
<button type="button">Add to cart</button>
</span>
<ItemCard item={item} key={item.id} />
))}
</main>
</div>
</>
);
}

function ItemCard({
item,
}: {
item: inferProcedureOutput<AppRouter["item"]["all"]>[number];
}) {
const router = useRouter();
const addToCart = useSetAtom(addItemToCartAtom({ id: item.id, quantity: 1 }));
const deleteFromCart = useSetAtom(deleteItemFromCartAtom({ id: item.id }));

return (
<div
className={styles.card}
onClick={() => router.push(`/item/${item.id}`)}
>
<picture>
{" "}
<img src={item.image_url ? item.image_url : ""} alt={item.name} />
</picture>
<h3>{item.name}</h3>
<p>${item.price.toFixed(2)}</p>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
addToCart();
if (item.stock.lessThanOrEqualTo(0)) {
alert("Item has no stock");
}
}}
disabled={item.stock.lessThanOrEqualTo(0)}
>
Add to cart
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
deleteFromCart();
}}
>
Delete
</button>
</div>
);
}
export default Content;
Loading

0 comments on commit 77613a3

Please sign in to comment.