Skip to content

Commit

Permalink
feat: add to cart (#66)
Browse files Browse the repository at this point in the history
* fix(client): fix bug in product filter

* feat(server): add to cart functionality in progress

* feat(client/server): persist cart for guest and loged in user

* feat(client): blunt implement of cart item

* feat(client): add loading modal to add to cart action

* feat(server): add channge cartItem quantity server action:

* fix: fix prettier

* feat: upgrade to nextjs14 to have stable server action
  • Loading branch information
Anh-Duy-Tran authored Jan 15, 2024
1 parent fa7db66 commit 76b1af8
Show file tree
Hide file tree
Showing 34 changed files with 3,264 additions and 2,691 deletions.
3 changes: 0 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ const nextConfig = {
images: {
domains: ["images.ctfassets.net"],
},
experimental: {
serverActions: true,
},
};

module.exports = nextConfig;
5,139 changes: 2,625 additions & 2,514 deletions package-lock.json

Large diffs are not rendered by default.

82 changes: 42 additions & 40 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,76 +19,78 @@
"predev": "yarn generate"
},
"dependencies": {
"@boiseitguru/cookie-cutter": "^0.2.1",
"@boiseitguru/cookie-cutter": "^0.2.3",
"@emotion/styled": "^11.11.0",
"@formatjs/intl-localematcher": "^0.4.2",
"@graphql-codegen/typescript-document-nodes": "^4.0.1",
"@hookform/resolvers": "^3.3.2",
"@hookform/resolvers": "^3.3.4",
"@parcel/watcher": "^2.3.0",
"@prisma/client": "^5.5.2",
"@prisma/client": "^5.8.0",
"@react-spring/web": "^9.7.3",
"@types/bcryptjs": "^2.4.5",
"@types/negotiator": "^0.6.2",
"@types/bcryptjs": "^2.4.6",
"@types/negotiator": "^0.6.3",
"@types/uuid": "^9.0.7",
"@urql/next": "^1.1.0",
"@vercel/postgres": "^0.5.0",
"@vercel/postgres": "^0.5.1",
"bcryptjs": "^2.4.3",
"graphql": "^16.8.1",
"lodash.throttle": "^4.1.1",
"negotiator": "^0.6.3",
"next": "13.5.4",
"next-auth": "^4.24.3",
"next": "14.0.4",
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"npm": "^10.2.1",
"prisma": "^5.5.2",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.47.0",
"npm": "^10.3.0",
"prisma": "^5.8.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.3",
"sharp": "^0.32.6",
"tailwind-merge": "^1.14.0",
"tailwind-variants": "^0.1.14",
"urql": "^4.0.5",
"usehooks-ts": "^2.9.1",
"yup": "^1.3.2",
"zustand": "^4.4.3"
"tailwind-variants": "^0.1.20",
"urql": "^4.0.6",
"usehooks-ts": "^2.9.4",
"uuid": "^9.0.1",
"yup": "^1.3.3",
"zustand": "^4.4.7"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/client-preset": "^4.1.0",
"@next/eslint-plugin-next": "^13.5.4",
"@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-interactions": "^7.4.6",
"@storybook/addon-links": "^7.4.6",
"@storybook/addon-onboarding": "^1.0.8",
"@next/eslint-plugin-next": "^13.5.6",
"@storybook/addon-essentials": "^7.6.8",
"@storybook/addon-interactions": "^7.6.8",
"@storybook/addon-links": "^7.6.8",
"@storybook/addon-onboarding": "^1.0.10",
"@storybook/addon-styling-webpack": "^0.0.5",
"@storybook/blocks": "^7.4.6",
"@storybook/nextjs": "^7.4.6",
"@storybook/nextjs": "^7.6.8",
"@storybook/react": "^7.4.6",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "6.1.2",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/jest": "29.5.5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@types/node": "^20.11.1",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"autoprefixer": "^10.4.16",
"css-loader": "^6.8.1",
"eslint": "^8.51.0",
"css-loader": "^6.9.0",
"eslint": "^8.56.0",
"eslint-config-next": "13.5.4",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-testing-library": "^6.1.0",
"eslint-plugin-testing-library": "^6.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.6.4",
"postcss": "^8.4.31",
"postcss-loader": "^7.3.3",
"prettier": "^3.0.3",
"storybook": "^7.4.6",
"style-loader": "^3.3.3",
"tailwindcss": "^3.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"postcss": "^8.4.33",
"postcss-loader": "^7.3.4",
"prettier": "^3.2.2",
"storybook": "^7.6.8",
"style-loader": "^3.3.4",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"utility-types": "^3.10.0"
}
}
43 changes: 30 additions & 13 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}

model User {
id String @id @default(uuid())
name String
firstName String
lastName String
email String @unique
password String
phoneNumber String @unique
prefix String
createdAt DateTime @default(now())
}
id String @id @default(uuid())
name String
firstName String
lastName String
email String @unique
password String
phoneNumber String @unique
prefix String
createdAt DateTime @default(now())
cart CartItem[]
}

model CartItem {
id String @id @default(uuid())
name String
variantName String
variantSlug String
variantRef String
sku String
size String
price Int
media String
quantity Int @default(1)
createdAt DateTime @default(now())
User User? @relation(fields: [userId], references: [id])
userId String?
}
30 changes: 30 additions & 0 deletions src/actions/addToUserCart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use server";

import prisma from "@/lib/prisma";
import { CartItem } from "@prisma/client";
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";

export async function addToUserCart(
cartItemData: Omit<CartItem, "id">,
): Promise<CartItem> {
const session = await getServerSession(options);
if (session?.user?.email) {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (user) {
const cartItem = await prisma.cartItem.create({
data: {
...cartItemData,
userId: user.id,
},
});
console.log(cartItem);
return cartItem;
} else {
throw new Error("User not found.");
}
}
throw new Error("Not authenticated");
}
42 changes: 42 additions & 0 deletions src/actions/changeCartItemQuantity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use server";

import prisma from "@/lib/prisma";
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";

export async function changeCartItemQuantity(
cartItemId: string,
delta: number,
) {
const session = await getServerSession(options);

if (session?.user?.email) {
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});

if (user) {
const cartItem = await prisma.cartItem.findFirst({
where: { id: cartItemId },
});

if (!cartItem) {
// should send message to client and not throw ?
throw new Error("User not found.");
}

const updatedCartItem = await prisma.cartItem.update({
where: {
id: cartItemId,
},
data: {
quantity: Math.max(cartItem.quantity + delta, 0),
},
});
return updatedCartItem;
} else {
throw new Error("User not found.");
}
}
throw new Error("Not authenticated");
}
26 changes: 26 additions & 0 deletions src/actions/getCart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use server";

import { CartItem } from "@prisma/client";
import { getServerSession } from "next-auth/next";
import { options } from "@/app/api/auth/[...nextauth]/options";
import prisma from "@/lib/prisma";

export async function getCart(): Promise<CartItem[] | undefined> {
const session = await getServerSession(options);

if (session?.user?.email) {
const userWithCart = await prisma.user.findFirst({
where: { email: session.user.email },
select: { cart: { orderBy: { createdAt: "desc" } } },
});

if (userWithCart && userWithCart.cart) {
return userWithCart.cart;
}
} else {
// handle this error
console.log("User not found!");
}

return [];
}
3 changes: 2 additions & 1 deletion src/app/[lang]/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { CartOverview } from "@/components/CartOverview";

export default function page() {
return <>CART</>;
return <CartOverview />;
}
42 changes: 32 additions & 10 deletions src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ import { getClient } from "@/lib/graphql";
import CategoryStoreInitializer from "@/hooks/CategoryStoreInitializer";
import { CategoriesType, useCategoryStore } from "@/context/useCategoryStore";
import { FetchCategoriesDocument } from "@/gql/graphql";
import { getServerSession } from "next-auth";
import { options } from "../api/auth/[...nextauth]/options";
import { getCart } from "@/actions/getCart";
import { useCartStore } from "@/context/useCartStore";
import CartStoreInitializer from "@/hooks/CartStoreInitializer";
import GuestCartStoreInitializer from "@/hooks/GuestCartStoreInitializer";

export const font = Montserrat({
weight: ["200", "400", "700"],
subsets: ["latin"],
});

export const metadata = {
title: "Create Next App",
title: "LAVISH",
description: "Generated by create next app",
};

Expand All @@ -32,29 +38,45 @@ export default async function RootLayout({
params,
}: RootLayoutProps) {
const { lang } = params;
const session = await getServerSession(options);

const fetchedCategories = (
await getClient().query(FetchCategoriesDocument, { lang })
).data?.categories?.categoriesCollection?.items;

useCategoryStore.setState({
categories: fetchedCategories as CategoriesType,
});

let userCart = undefined;
if (session) {
userCart = await getCart();
if (userCart) {
useCartStore.setState({ cart: userCart });
} else {
// handle this error
console.log("some thing wrong");
}
}

return (
<html lang="en">
<body>
<CategoryStoreInitializer
categories={fetchedCategories as CategoriesType}
/>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
<AuthProvider>
<main className={font.className}>
<main className={font.className}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
<AuthProvider>
<CategoryStoreInitializer
categories={fetchedCategories as CategoriesType}
/>
{session ? <CartStoreInitializer cart={userCart} /> : null}
<GuestCartStoreInitializer />
<Sidebar />
<Navbar />
{children}
<MessageModal />
</main>
</AuthProvider>
</ThemeProvider>
</AuthProvider>
</ThemeProvider>
</main>
</body>
</html>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/[lang]/user/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function layout({ children }: PageProps) {

return (
<div className="page-wrapper">
<div className="page-container flex flex-col gap-8">
<div className="page-container flex flex-col gap-8 p-5 tablet:p-0">
<div className="flex gap-3">
<Link href={"/user/orders"}>
<Button variant="outlined" size="compact">
Expand Down
Loading

0 comments on commit 76b1af8

Please sign in to comment.