?schema=public&sslmode=prefer"
5 | GITHUB_SECRET=
6 | GITHUB_ID=
7 | SECRET=
8 | NEXTAUTH_URL=
9 | NEXTAUTH_CALLBACK_URL=
10 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
11 | STRIPE_SECRET_KEY=
12 | NEXT_PUBLIC_STRIPE_SUCCESS_REDIRECT_URL=
13 | NEXT_PUBLIC_STRIPE_ERROR_REDIRECT_URL
14 |
--------------------------------------------------------------------------------
/components/products/Products.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from './Product';
2 | import { useGetProducts } from './hooks/useGetProducts';
3 |
4 | export const Products = () => {
5 | const { data: products } = useGetProducts();
6 |
7 | return (
8 |
9 | {products && products.map((product) =>
)}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/components/products/api/buyProduct.ts:
--------------------------------------------------------------------------------
1 | import type Prisma from '@prisma/client';
2 | import { fetcher } from '../../../utils/fetcher';
3 | import { stripeSessionSchema } from '../../../utils/stripe';
4 | import { transformProduct } from '../utils/transforms';
5 |
6 | export const buyProduct = async (product: Prisma.Product) => {
7 | const stripeItem = transformProduct(product);
8 |
9 | return await fetcher(`/api/checkout/products/`, {
10 | method: 'POST',
11 | body: [stripeItem],
12 | schema: stripeSessionSchema,
13 | });
14 | };
15 |
--------------------------------------------------------------------------------
/components/auth/SignInButton.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from './hooks/useAuth';
2 |
3 | export const SignInButton = () => {
4 | const { signIn } = useAuth();
5 | const handleSignIn = () => signIn('github');
6 |
7 | return (
8 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/pages/api/products/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { PrismaClient } from '@prisma/client';
3 |
4 | export default async (req: NextApiRequest, res: NextApiResponse) => {
5 | try {
6 | const prisma = new PrismaClient();
7 |
8 | const products = await prisma.product.findMany();
9 |
10 | if (products.length) {
11 | res.status(200).json(products);
12 | res.end();
13 | } else {
14 | res.status(404);
15 | res.end();
16 | }
17 | } catch {
18 | res.status(500);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/typings/next.d.ts:
--------------------------------------------------------------------------------
1 | import type { NextComponentType, NextPageContext } from 'next';
2 | import type { Session } from 'next-auth';
3 | import type { Router } from 'next/router';
4 |
5 | declare module 'next/app' {
6 | type AppProps> = {
7 | Component: NextComponentType;
8 | router: Router;
9 | __N_SSG?: boolean;
10 | __N_SSP?: boolean;
11 | pageProps: P & {
12 | /** Initial session passed in from `getServerSideProps` or `getInitialProps` */
13 | session?: Session;
14 | };
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { Header } from "./header/Header";
3 |
4 | type LayoutProps = {
5 | readonly children: ReactNode;
6 | };
7 |
8 | export const Layout = ({ children }: LayoutProps) => (
9 | <>
10 |
11 |
12 |
13 | FullStack Next.js E-commerce
14 |
15 | {children}
16 |
17 | >
18 | );
19 |
--------------------------------------------------------------------------------
/components/cart/api/checkoutCart.ts:
--------------------------------------------------------------------------------
1 | import type Prisma from "@prisma/client";
2 | import { fetcher } from "../../../utils/fetcher";
3 | import { stripeSessionSchema } from "../../../utils/stripe";
4 | import { transformProduct } from "../../products/utils/transforms";
5 |
6 | export const checkoutCart = async (products: Array) => {
7 | const stripeItems = products.map((product) => transformProduct(product));
8 |
9 | return await fetcher(`/api/checkout/products/`, {
10 | method: "POST",
11 | body: stripeItems,
12 | schema: stripeSessionSchema,
13 | });
14 | };
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/utils/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from 'stripe';
2 | import * as y from 'yup';
3 | import { getEnv } from './env';
4 | import { loadStripe } from '@stripe/stripe-js';
5 |
6 | export const redirectToCheckout = async (session: Pick) => {
7 | const stripe = await loadStripe(getEnv('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY'));
8 |
9 | return stripe!.redirectToCheckout({
10 | sessionId: session.id,
11 | });
12 | };
13 |
14 | export const stripeSessionSchema: y.SchemaOf> = y
15 | .object()
16 | .shape({
17 | id: y.string().required(),
18 | });
19 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].tsx:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 | import NextAuth from 'next-auth';
3 | import GitHubProvider from 'next-auth/providers/github';
4 | import { PrismaAdapter } from '@next-auth/prisma-adapter';
5 | import { getEnv } from '../../../utils/env';
6 |
7 | const prisma = new PrismaClient();
8 |
9 | export default NextAuth({
10 | providers: [
11 | GitHubProvider({
12 | clientId: getEnv('GITHUB_ID'),
13 | clientSecret: getEnv('GITHUB_SECRET'),
14 | }),
15 | ],
16 | pages: {
17 | signIn: '/auth/signin',
18 | },
19 | adapter: PrismaAdapter(prisma),
20 | secret: getEnv('SECRET'),
21 | });
22 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, Html, NextScript, DocumentContext } from 'next/document';
2 |
3 | export default class MyDocument extends Document {
4 | static async getInitialProps(ctx: DocumentContext) {
5 | const initialProps = await Document.getInitialProps(ctx);
6 |
7 | return { ...initialProps };
8 | }
9 | render() {
10 | return (
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pages/api/checkout/products/index.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Stripe } from 'stripe';
3 | import { getEnv } from '../../../../utils/env';
4 |
5 | const stripe = new Stripe(getEnv('STRIPE_SECRET_KEY'), {
6 | apiVersion: '2020-08-27',
7 | });
8 |
9 | export default async (req: NextApiRequest, res: NextApiResponse) => {
10 | try {
11 | const { id } = await stripe.checkout.sessions.create({
12 | mode: 'payment',
13 | submit_type: 'donate',
14 | payment_method_types: ['card'],
15 | success_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
16 | cancel_url: `${req.headers.origin}/result?session_id={CHECKOUT_SESSION_ID}`,
17 | line_items: req.body,
18 | });
19 |
20 | res.status(200).json({ id });
21 | res.end();
22 | } catch {
23 | res.status(500);
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/components/cart/context/cartContext.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ReactNode,
3 | createContext,
4 | useReducer,
5 | useContext,
6 | useMemo,
7 | useState,
8 | } from "react";
9 | import { cartReducer } from "./reducers/cartReducer";
10 | import type { Action, State } from "./types";
11 |
12 | type Dispatch = (action: Action) => void;
13 | type CartProviderProps = { readonly children: React.ReactNode };
14 |
15 | export const CartStateContext = createContext<
16 | { state: State; dispatch: Dispatch } | undefined
17 | >(undefined);
18 |
19 | const initialState: State = { products: [], totalPrice: 0, isOpen: false };
20 |
21 | export const CartProvider = ({ children }: CartProviderProps) => {
22 | const [state, dispatch] = useReducer(cartReducer, initialState);
23 |
24 | const value = useMemo(() => ({ state, dispatch }), [state]);
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import type { AppProps } from 'next/app';
3 | import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
4 | import { ReactQueryDevtools } from 'react-query/devtools';
5 | import { SessionProvider } from 'next-auth/react';
6 | import 'tailwindcss/tailwind.css';
7 | import { CartProvider } from '../components/cart/context/cartContext';
8 |
9 | export default function App({ Component, pageProps, err }: AppProps & { err: Error }) {
10 | const [queryClient] = useState(() => new QueryClient());
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/env.ts:
--------------------------------------------------------------------------------
1 | type NameToType = {
2 | readonly ENV: 'production' | 'staging' | 'development' | 'test';
3 | readonly NEXTAUTH_URL: string;
4 | readonly NODE_ENV: 'production' | 'development';
5 | readonly POSTGRES_USER: string;
6 | readonly POSTGRES_PASSWORD: string;
7 | readonly POSTGRES_DB: string;
8 | readonly DATABASE_URL: string;
9 | readonly GITHUB_SECRET: string;
10 | readonly GITHUB_ID: string;
11 | readonly SECRET: string;
12 | readonly SENTRY_DSN: string;
13 | readonly NEXTAUTH_CALLBACK_URL: string;
14 | readonly NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: string;
15 | readonly STRIPE_SECRET_KEY: string;
16 | readonly NEXT_PUBLIC_STRIPE_SUCCESS_REDIRECT_URL: string;
17 | readonly NEXT_PUBLIC_STRIPE_ERROR_REDIRECT_URL: string;
18 | readonly ABC: number;
19 | };
20 |
21 | export function getEnv(name: Env): NameToType[Env];
22 | export function getEnv(name: keyof NameToType): NameToType[keyof NameToType] {
23 | const val = process.env[name];
24 |
25 | if (!val) {
26 | throw new Error(`Cannot find environmental variable: ${name}`);
27 | }
28 |
29 | return val;
30 | }
31 |
--------------------------------------------------------------------------------
/components/layout/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth } from '../../auth/hooks/useAuth';
2 | import { Logo } from '../logo/Logo';
3 |
4 | export const Header = () => {
5 | const { session, signIn, signOut } = useAuth();
6 |
7 | return (
8 |
9 |
10 |
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { dehydrate, QueryClient } from 'react-query';
2 | import type { GetServerSideProps } from 'next';
3 | import { Products } from '../components/products/Products';
4 | import { Checkout } from '../components/cart/Checkout';
5 | import { getSession } from 'next-auth/react';
6 | import { getEnv } from '../utils/env';
7 | import { getProducts } from '../components/products/api/getProducts';
8 | import { Layout } from '../components/layout/Layout';
9 |
10 | export default function Home() {
11 | return (
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | export const getServerSideProps: GetServerSideProps = async (context) => {
20 | const queryClient = new QueryClient();
21 | const session = await getSession(context);
22 |
23 | await queryClient.prefetchQuery('products', getProducts);
24 |
25 | if (!session) {
26 | return {
27 | redirect: {
28 | destination: getEnv('NEXTAUTH_CALLBACK_URL'),
29 | permanent: false,
30 | },
31 | props: {
32 | dehydratedState: dehydrate(queryClient),
33 | },
34 | };
35 | }
36 |
37 | return {
38 | props: {
39 | session,
40 | dehydratedState: dehydrate(queryClient),
41 | },
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/pages/_error.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react";
2 | import { NextPageContext, NextPage } from "next";
3 | import NextErrorComponent, { ErrorProps as NextErrorProps } from "next/error";
4 | import * as Sentry from "@sentry/nextjs";
5 |
6 | type ErrorPageProps = {
7 | err: Error;
8 | statusCode: number;
9 | hasGetInitialPropsRun: boolean;
10 | children?: ReactElement;
11 | };
12 |
13 | type ErrorProps = {
14 | hasGetInitialPropsRun: boolean;
15 | } & NextErrorProps;
16 |
17 | function ErrorPage({ statusCode, hasGetInitialPropsRun, err }: ErrorPageProps) {
18 | if (!hasGetInitialPropsRun && err) {
19 | Sentry.captureException(err);
20 | }
21 |
22 | return ;
23 | }
24 |
25 | ErrorPage.getInitialProps = async ({ res, err, asPath }: NextPageContext) => {
26 | const errorInitialProps = (await NextErrorComponent.getInitialProps({
27 | res,
28 | err,
29 | } as NextPageContext)) as ErrorProps;
30 |
31 | errorInitialProps.hasGetInitialPropsRun = true;
32 |
33 | if (err) {
34 | Sentry.captureException(err);
35 |
36 | await Sentry.flush(2000);
37 |
38 | return errorInitialProps;
39 | }
40 |
41 | Sentry.captureException(
42 | new Error(`_error.js getInitialProps missing data at path: ${asPath}`)
43 | );
44 |
45 | await Sentry.flush(2000);
46 |
47 | return errorInitialProps;
48 | };
49 |
50 | export default ErrorPage;
51 |
--------------------------------------------------------------------------------
/components/cart/CartItem.tsx:
--------------------------------------------------------------------------------
1 | import type Prisma from "@prisma/client";
2 | import Image from "next/image";
3 | import { useCart } from "./hooks/useCart";
4 |
5 | type CartItemProps = Prisma.Product;
6 |
7 | export const CartItem = (product: CartItemProps) => {
8 | const { id, name, price, image } = product;
9 | const { dispatch } = useCart();
10 |
11 | const handleDelete = (product: Prisma.Product) => {
12 | dispatch({ type: "deleteProduct", payload: product });
13 | };
14 |
15 | return (
16 |
17 |
18 |

23 |
24 |
25 |
26 |
{name}
27 |
{price / 100}
28 |
29 |
30 |
31 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/utils/fetcher.ts:
--------------------------------------------------------------------------------
1 | import type { AnySchema, InferType } from "yup";
2 | import { ResponseError } from "./responseError";
3 | import type { HTTPMethod } from "./types";
4 |
5 | type FetcherConfig = {
6 | readonly method: HTTPMethod;
7 | readonly schema: Schema;
8 | readonly body?: object;
9 | readonly config?: RequestInit;
10 | };
11 |
12 | export async function fetcher(
13 | path: string,
14 | { method, body, config, schema }: FetcherConfig
15 | ): Promise;
16 |
17 | export async function fetcher(
18 | path: string,
19 | { method, body, config, schema }: FetcherConfig
20 | ): Promise>;
21 |
22 | export async function fetcher(
23 | path: string,
24 | { method, body, config, schema }: FetcherConfig
25 | ) {
26 | try {
27 | const response = await fetch(path, {
28 | ...config,
29 | headers: {
30 | "Content-Type": "application/json",
31 | },
32 | credentials: "include",
33 | method,
34 | ...(body && { body: JSON.stringify(body) }),
35 | });
36 | if (response.ok) {
37 | if (!schema) {
38 | return null;
39 | }
40 |
41 | const data = await response.json();
42 |
43 | return schema.cast(data);
44 | }
45 | throw new ResponseError(response.statusText, response.status);
46 | } catch (err) {
47 | if (err instanceof ResponseError) {
48 | throw err;
49 | }
50 | throw new ResponseError("Something went wrong during fetching!");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "extends": [
5 | "prettier",
6 | "plugin:react/recommended",
7 | "plugin:react-hooks/recommended",
8 | "plugin:jsx-a11y/strict",
9 | "plugin:testing-library/recommended",
10 | "plugin:jest-dom/recommended",
11 | "next",
12 | "next/core-web-vitals"
13 | ],
14 | "plugins": [
15 | "jsx-a11y",
16 | "react-app",
17 | "react-hooks",
18 | "jest-dom",
19 | "testing-library",
20 | "@typescript-eslint",
21 | "prettier"
22 | ],
23 |
24 | "env": {
25 | "es6": true,
26 | "browser": true,
27 | "jest": true,
28 | "node": true
29 | },
30 | "rules": {
31 | "react-hooks/rules-of-hooks": "error",
32 | "react-hooks/exhaustive-deps": "warn",
33 | "jsx-a11y/anchor-is-valid": 0,
34 | "@typescript-eslint/no-unused-vars": 0,
35 | "react/react-in-jsx-scope": 0,
36 | "react/display-name": 0,
37 | "react/prop-types": 0,
38 | "@typescript-eslint/explicit-member-accessibility": 0,
39 | "@typescript-eslint/indent": 0,
40 | "@typescript-eslint/member-delimiter-style": 0,
41 | "@typescript-eslint/no-var-requires": 0,
42 | "@typescript-eslint/no-use-before-define": 0,
43 | "@typescript-eslint/explicit-function-return-type": 0,
44 | "@typescript-eslint/no-explicit-any": 0,
45 | "@typescript-eslint/no-non-null-assertion": 0,
46 | "no-undef": 0,
47 | "no-unused-vars": 0,
48 | "jsx-a11y/label-has-for": 0,
49 | "jsx-a11y/no-noninteractive-tabindex": 0,
50 | "prettier/prettier": 0,
51 | "react/no-unescaped-entities": 0
52 | },
53 | "parserOptions": {
54 | "ecmaVersion": 12,
55 | "sourceType": "module"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Fullstack Next.js E-commerce
12 |
13 |
14 |
15 | ## Technologies 🔧
16 |
17 | - Next.js(React)
18 | - TypeScript
19 | - Prisma
20 | - NextAuth
21 | - Stripe
22 | - Tailwind
23 | - React Query
24 | - Sentry
25 | - Yup
26 |
27 | ## Screenshots 📸
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | ## Installation 💾
41 |
42 | ```bash
43 | git clone https://github.com/marksantiago290/eCommerce_MERN.git
44 | ```
45 |
46 | Fill your `.env` variables:
47 |
48 | ```
49 | POSTGRES_USER=
50 | POSTGRES_PASSWORD=
51 | POSTGRES_DB=
52 | DATABASE_URL="postgresql://:@:/?schema=public&sslmode=prefer"
53 | GITHUB_SECRET=
54 | GITHUB_ID=
55 | SECRET=
56 | NEXTAUTH_URL=
57 | NEXTAUTH_CALLBACK_URL=
58 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
59 | STRIPE_SECRET_KEY=
60 | NEXT_PUBLIC_STRIPE_SUCCESS_REDIRECT_URL=
61 | NEXT_PUBLIC_STRIPE_ERROR_REDIRECT_URL
62 | ```
63 |
64 | Install deps:
65 |
66 | ```bash
67 | npm install
68 | ```
69 |
70 | Generate Prisma Client:
71 |
72 | ```bash
73 | npx prisma generate
74 | ```
75 |
76 | Run docker-compose:
77 |
78 | ```bash
79 | docker-compose up -d
80 | ```
81 |
82 | Run Next dev server:
83 |
84 | ```bash
85 | npm run dev
86 | ```
87 |
--------------------------------------------------------------------------------
/components/cart/context/reducers/cartReducer.ts:
--------------------------------------------------------------------------------
1 | import type Prisma from "@prisma/client";
2 | import type { Action, State } from "../types";
3 |
4 | const calculateTotalPrice = (products: Array) => {
5 | return products.reduce((acc, curr) => acc + curr.price, 0);
6 | };
7 |
8 | export const cartReducer = (state: State, action: Action) => {
9 | switch (action.type) {
10 | case "addProduct": {
11 | const products = [...state.products];
12 | const newProduct = action.payload;
13 | const isTheNewProductInCart = products.find(
14 | (product) => product.id === newProduct.id
15 | );
16 |
17 | const newProducts = [newProduct, ...products];
18 |
19 | const totalPrice = calculateTotalPrice(newProducts);
20 |
21 | if (!isTheNewProductInCart) {
22 | return {
23 | ...state,
24 | products: newProducts,
25 | totalPrice,
26 | };
27 | }
28 | }
29 | case "deleteProduct": {
30 | const products = [...state.products];
31 | const productToDelete = action.payload;
32 |
33 | const newProducts = products.filter(
34 | (product) => product.id !== productToDelete.id
35 | );
36 |
37 | const totalPrice = calculateTotalPrice(newProducts);
38 |
39 | return {
40 | ...state,
41 | products: [...newProducts],
42 | totalPrice,
43 | };
44 | }
45 |
46 | case "openMenu": {
47 | return {
48 | ...state,
49 | isOpen: true,
50 | };
51 | }
52 | case "closeMenu": {
53 | return {
54 | ...state,
55 | isOpen: false,
56 | };
57 | }
58 |
59 | default: {
60 | throw new Error(`Unhandled action type`);
61 | }
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/components/products/Product.tsx:
--------------------------------------------------------------------------------
1 | import type Prisma from '@prisma/client';
2 | import { useBuyProduct } from './hooks/useBuyProduct';
3 | import { useCart } from '../cart/hooks/useCart';
4 |
5 | type ProductProps = Readonly;
6 |
7 | export const Product = (product: ProductProps) => {
8 | const { id, image, name, price } = product;
9 | const { mutate } = useBuyProduct();
10 | const { dispatch } = useCart();
11 |
12 | const buyProduct = () => mutate(product);
13 |
14 | const addToCart = () => {
15 | dispatch({ type: 'addProduct', payload: product });
16 | dispatch({ type: 'openMenu' });
17 | };
18 |
19 | return (
20 |
21 |
22 |

27 |
28 |
29 |
30 |
31 | {name}
32 |
33 |
{price / 100} zł
34 |
35 |
41 |
47 |
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-next",
3 | "private": true,
4 | "scripts": {
5 | "dev": "next dev",
6 | "docker": "docker-compose up -d",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "prepare": "husky install",
11 | "eslint": "eslint --ext .js,.jsx,.ts,.tsx --fix .",
12 | "tsc": "tsc --noEmit",
13 | "studio": "prisma studio",
14 | "pre-commit": "lint-staged"
15 | },
16 | "dependencies": {
17 | "@next-auth/prisma-adapter": "^0.5.2-next.19",
18 | "@prisma/client": "^3.5.0",
19 | "@sentry/nextjs": "^6.15.0",
20 | "@stripe/react-stripe-js": "^1.6.0",
21 | "@stripe/stripe-js": "^1.21.1",
22 | "@tailwindcss/forms": "^0.3.4",
23 | "next": "^12.0.4",
24 | "react": "17.0.2",
25 | "react-dom": "17.0.2",
26 | "react-query": "^3.33.1",
27 | "stripe": "^8.190.0",
28 | "yup": "^0.32.11"
29 | },
30 | "devDependencies": {
31 | "@headlessui/react": "^1.4.2",
32 | "@heroicons/react": "^1.0.5",
33 | "@tailwindcss/aspect-ratio": "^0.3.0",
34 | "@types/micro": "^7.3.6",
35 | "@types/micro-cors": "^0.1.2",
36 | "@types/node": "16.11.7",
37 | "@types/react": "17.0.35",
38 | "@types/stripe": "^8.0.417",
39 | "autoprefixer": "^10.4.0",
40 | "eslint": "^7.32.0",
41 | "eslint-config-next": "^12.0.4",
42 | "eslint-config-prettier": "^8.3.0",
43 | "eslint-plugin-jsx-a11y": "^6.5.1",
44 | "eslint-plugin-prettier": "^4.0.0",
45 | "eslint-plugin-react": "^7.27.1",
46 | "eslint-plugin-react-hooks": "^4.3.0",
47 | "husky": "^7.0.4",
48 | "lint-staged": "^12.1.2",
49 | "postcss": "^8.3.11",
50 | "prettier": "^2.5.0",
51 | "tailwindcss": "^2.2.19",
52 | "typescript": "4.5.2"
53 | },
54 | "lint-staged": {
55 | "*.{js,jsx,ts,tsx}": [
56 | "eslint --fix",
57 | "npx prettier --write"
58 | ],
59 | "*.{json,md,yaml,yml,scss,css}": [
60 | "npx prettier --write"
61 | ],
62 | "*.js": "eslint --cache --fix"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model Account {
14 | id String @id @default(cuid())
15 | userId String @map("user_id")
16 | type String
17 | provider String
18 | providerAccountId String @map("provider_account_id")
19 | refresh_token String?
20 | access_token String?
21 | expires_at Int?
22 | token_type String?
23 | scope String?
24 | id_token String?
25 | session_state String?
26 |
27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
28 |
29 | @@unique([provider, providerAccountId])
30 | @@map("accounts")
31 | }
32 |
33 | enum Role {
34 | USER
35 | ADMIN
36 | }
37 |
38 | model User {
39 | id String @id @default(cuid())
40 | role Role @default(USER)
41 | name String?
42 | email String? @unique
43 | emailVerified DateTime? @map("email_verified")
44 | image String?
45 | accounts Account[]
46 | sessions Session[]
47 |
48 | @@map("users")
49 | }
50 |
51 | model Product {
52 | id String @id
53 | description String
54 | name String
55 | price Int
56 | image String
57 | }
58 |
59 | model Session {
60 | id String @id @default(cuid())
61 | sessionToken String @unique @map("session_token")
62 | userId String @map("user_id")
63 | expires DateTime
64 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
65 |
66 | @@map("sessions")
67 | }
68 |
69 | model VerificationToken {
70 | identifier String
71 | token String @unique
72 | expires DateTime
73 |
74 | @@unique([identifier, token])
75 | @@map("verificationtokens")
76 | }
77 |
--------------------------------------------------------------------------------
/prisma/migrations/20211126172202_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
3 |
4 | -- CreateTable
5 | CREATE TABLE "accounts" (
6 | "id" TEXT NOT NULL,
7 | "user_id" TEXT NOT NULL,
8 | "type" TEXT NOT NULL,
9 | "provider" TEXT NOT NULL,
10 | "provider_account_id" TEXT NOT NULL,
11 | "refresh_token" TEXT,
12 | "access_token" TEXT,
13 | "expires_at" INTEGER,
14 | "token_type" TEXT,
15 | "scope" TEXT,
16 | "id_token" TEXT,
17 | "session_state" TEXT,
18 |
19 | CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
20 | );
21 |
22 | -- CreateTable
23 | CREATE TABLE "users" (
24 | "id" TEXT NOT NULL,
25 | "role" "Role" NOT NULL DEFAULT E'USER',
26 | "name" TEXT,
27 | "email" TEXT,
28 | "email_verified" TIMESTAMP(3),
29 | "image" TEXT,
30 |
31 | CONSTRAINT "users_pkey" PRIMARY KEY ("id")
32 | );
33 |
34 | -- CreateTable
35 | CREATE TABLE "Product" (
36 | "id" TEXT NOT NULL,
37 | "description" TEXT NOT NULL,
38 | "name" TEXT NOT NULL,
39 | "price" INTEGER NOT NULL,
40 | "image" TEXT NOT NULL,
41 |
42 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
43 | );
44 |
45 | -- CreateTable
46 | CREATE TABLE "sessions" (
47 | "id" TEXT NOT NULL,
48 | "session_token" TEXT NOT NULL,
49 | "user_id" TEXT NOT NULL,
50 | "expires" TIMESTAMP(3) NOT NULL,
51 |
52 | CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
53 | );
54 |
55 | -- CreateTable
56 | CREATE TABLE "verificationtokens" (
57 | "identifier" TEXT NOT NULL,
58 | "token" TEXT NOT NULL,
59 | "expires" TIMESTAMP(3) NOT NULL
60 | );
61 |
62 | -- CreateIndex
63 | CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id");
64 |
65 | -- CreateIndex
66 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
67 |
68 | -- CreateIndex
69 | CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token");
70 |
71 | -- CreateIndex
72 | CREATE UNIQUE INDEX "verificationtokens_token_key" ON "verificationtokens"("token");
73 |
74 | -- CreateIndex
75 | CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token");
76 |
77 | -- AddForeignKey
78 | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
79 |
80 | -- AddForeignKey
81 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/components/cart/Checkout.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, Fragment, SetStateAction } from "react";
2 | import { Dialog, Transition } from "@headlessui/react";
3 | import { XIcon } from "@heroicons/react/outline";
4 | import { CartItems } from "./CartItems";
5 | import { useCart } from "./hooks/useCart";
6 | import { useCheckout } from "./hooks/useCheckout";
7 |
8 | export const Checkout = () => {
9 | const {
10 | state: { totalPrice, products, isOpen },
11 | dispatch,
12 | } = useCart();
13 | const { mutate } = useCheckout();
14 |
15 | const handleOpenMenu = () => dispatch({ type: "openMenu" });
16 | const handleCloseMenu = () => dispatch({ type: "closeMenu" });
17 |
18 | const handleCheckout = () => mutate(products);
19 |
20 | return (
21 |
22 |
95 |
96 | );
97 | };
98 |
--------------------------------------------------------------------------------