├── .gitignore ├── README.md ├── app ├── (auth) │ ├── forget-password │ │ └── page.jsx │ ├── layout.jsx │ ├── login │ │ └── page.jsx │ └── sign-up │ │ └── page.jsx ├── (checkout) │ ├── checkout-cod │ │ └── page.jsx │ ├── checkout-failed │ │ └── page.jsx │ └── checkout-success │ │ ├── components │ │ └── SuccessMessage.jsx │ │ └── page.jsx ├── (pages) │ ├── categories │ │ └── [categoryId] │ │ │ └── page.jsx │ ├── collections │ │ └── [collectionId] │ │ │ └── page.jsx │ ├── layout.jsx │ ├── products │ │ └── [productId] │ │ │ ├── components │ │ │ ├── AddReiveiw.jsx │ │ │ ├── Description.jsx │ │ │ ├── Details.jsx │ │ │ ├── Photos.jsx │ │ │ ├── RelatedProducts.jsx │ │ │ └── Reviews.jsx │ │ │ └── page.jsx │ └── search │ │ ├── components │ │ └── SearchBox.jsx │ │ └── page.jsx ├── (user) │ ├── account │ │ └── page.jsx │ ├── cart │ │ └── page.jsx │ ├── checkout │ │ ├── components │ │ │ └── Checkout.jsx │ │ ├── layout.jsx │ │ └── page.jsx │ ├── favorites │ │ └── page.jsx │ └── layout.jsx ├── admin │ ├── admins │ │ ├── components │ │ │ ├── Form.jsx │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── brands │ │ ├── components │ │ │ ├── Form.jsx │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── categories │ │ ├── components │ │ │ ├── Form.jsx │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── collections │ │ ├── components │ │ │ ├── Form.jsx │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── components │ │ ├── AdminLayout.jsx │ │ ├── CountMeter.jsx │ │ ├── Header.jsx │ │ ├── OrdersChart.jsx │ │ ├── RevenueChart.jsx │ │ └── Sidebar.jsx │ ├── customers │ │ ├── components │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── layout.jsx │ ├── orders │ │ ├── [orderId] │ │ │ ├── components │ │ │ │ └── ChangeStatus.jsx │ │ │ └── page.jsx │ │ ├── components │ │ │ └── ListView.jsx │ │ └── page.jsx │ ├── page.jsx │ ├── products │ │ ├── components │ │ │ └── ListView.jsx │ │ ├── form │ │ │ ├── components │ │ │ │ ├── BasicDetails.jsx │ │ │ │ ├── Description.jsx │ │ │ │ └── Images.jsx │ │ │ └── page.jsx │ │ └── page.jsx │ └── reviews │ │ ├── components │ │ └── ListView.jsx │ │ └── page.jsx ├── components │ ├── AddToCartButton.jsx │ ├── AdminButton.jsx │ ├── Brands.jsx │ ├── Categories.jsx │ ├── Collections.jsx │ ├── CustomerReviews.jsx │ ├── FavoriteButton.jsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── HeaderClientButtons.jsx │ ├── LogoutButton.jsx │ ├── MyRating.jsx │ ├── Products.jsx │ └── Sliders.jsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.js └── page.js ├── contexts └── AuthContext.jsx ├── jsconfig.json ├── lib ├── firebase.jsx ├── firebase_admin.js └── firestore │ ├── admins │ ├── read.jsx │ ├── read_server.jsx │ └── write.jsx │ ├── brands │ ├── read.jsx │ ├── read_server.jsx │ └── write.jsx │ ├── categories │ ├── read.jsx │ ├── read_server.jsx │ └── write.jsx │ ├── checkout │ └── write.jsx │ ├── collections │ ├── read.jsx │ ├── read_server.jsx │ └── write.jsx │ ├── orders │ ├── read.jsx │ ├── read_count.jsx │ └── write.jsx │ ├── products │ ├── count │ │ ├── read.jsx │ │ └── read_client.jsx │ ├── read.jsx │ ├── read_server.jsx │ └── write.jsx │ ├── reviews │ ├── read.jsx │ └── write.jsx │ └── user │ ├── read.jsx │ ├── read_count.jsx │ └── write.jsx ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── algolia.png ├── box.png ├── increase.png ├── logo.png ├── profit-up.png ├── received.png ├── svgs │ ├── Empty-pana.svg │ └── Mobile payments-rafiki.svg └── team.png └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack E-Commerce Website 2 | 3 | This is a Full Stack E-Commerce Website built with **Next.js 14**, **Tailwind CSS**, **Firebase**, **Stripe**, and **Algolia**. 4 | 5 | ## Features 6 | 7 | - User authentication with Firebase 8 | - Product browsing and searching with Algolia 9 | - Shopping cart functionality 10 | - Secure payment processing with Stripe 11 | - Admin panel for managing products, orders, and users 12 | 13 | ## Technologies Used 14 | 15 | - **Frontend**: Next.js 14, Tailwind CSS 16 | - **Backend**: Firebase (Firestore, Authentication) 17 | - **Payment**: Stripe 18 | - **Search**: Algolia 19 | 20 | ## Installation 21 | 22 | 1. Clone the repository: 23 | ```bash 24 | git clone 25 | ``` 26 | 27 | 2. Navigate to the project directory: 28 | ```bash 29 | cd 30 | ``` 31 | 32 | 3. Install the dependencies: 33 | ```bash 34 | npm install 35 | ``` 36 | 37 | 4. Create a `.env.local` file in the root directory and add the following: 38 | 39 | ```plaintext 40 | NEXT_PUBLIC_FIREBASE_API_KEY="" 41 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="" 42 | NEXT_PUBLIC_FIREBASE_PROJECT_ID="" 43 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="" 44 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="" 45 | NEXT_PUBLIC_FIREBASE_APP_ID="" 46 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID="" 47 | 48 | NEXT_PUBLIC_DOMAIN="" 49 | 50 | NEXT_PUBLIC_ALGOLIA_APP_ID="" 51 | NEXT_PUBLIC_ALGOLIA_APP_KEY="" 52 | 53 | NEXT_PUBLIC_FIREBASE_SERVICE_ACCOUNT_KEYS='{}' 54 | ``` 55 | 56 | 5. Start the development server: 57 | ```bash 58 | npm run dev 59 | ``` 60 | 61 | ## Firestore Security Rules 62 | 63 | ```plaintext 64 | service cloud.firestore { 65 | match /databases/{database}/documents { 66 | 67 | function isAdmin() { 68 | return exists(/databases/$(database)/documents/admins/$(request.auth.token.email)); 69 | } 70 | 71 | match /admins/{id} { 72 | allow read: if true; 73 | allow write: if isAdmin(); 74 | } 75 | 76 | match /brands/{id} { 77 | allow read: if true; 78 | allow write: if isAdmin(); 79 | } 80 | 81 | match /categories/{id} { 82 | allow read: if true; 83 | allow write: if isAdmin(); 84 | } 85 | 86 | match /collections/{id} { 87 | allow read: if true; 88 | allow write: if isAdmin(); 89 | } 90 | 91 | match /orders/{id} { 92 | allow read: if isAdmin() || request.auth.uid == resource.data.uid; 93 | allow write: if isAdmin(); 94 | } 95 | 96 | match /products/{id} { 97 | allow read: if true; 98 | allow write: if isAdmin(); 99 | 100 | match /reviews/{uid} { 101 | allow read: if true; 102 | allow write: if isAdmin() || request.auth.uid == uid; 103 | } 104 | } 105 | 106 | match /users/{uid} { 107 | allow read: if isAdmin() || request.auth.uid == uid; 108 | allow write: if isAdmin() || request.auth.uid == uid; 109 | 110 | match /checkout_sessions/{id} { 111 | allow read: if isAdmin() || request.auth.uid == uid; 112 | allow write: if isAdmin() || request.auth.uid == uid; 113 | } 114 | 115 | match /checkout_sessions_cod/{id} { 116 | allow read: if isAdmin() || request.auth.uid == uid; 117 | allow write: if isAdmin() || request.auth.uid == uid; 118 | } 119 | 120 | match /payments/{id} { 121 | allow read, write: if false; 122 | } 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ## Firestore Storage Security Rules 129 | 130 | ```plaintext 131 | rules_version = '2'; 132 | service firebase.storage { 133 | match /b/{bucket}/o { 134 | match /{allPaths=**} { 135 | allow read: if true; 136 | allow write: if request.auth.uid != null && firestore.exists(/databases/(default)/documents/admins/$(request.auth.token.email)); 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ## Demo 143 | 144 | You can explore the demo of the e-commerce website at [this link](https://docs-reader-store.vercel.app). 145 | 146 | To access the Admin Panel, use the following credentials: 147 | 148 | - **Email**: admin@gmail.com 149 | - **Password**: iloveyou 150 | 151 | (Note: This demo admin account has read and write permissions disabled for security purposes.) 152 | 153 | ## Conclusion 154 | 155 | This project provides a robust e-commerce solution leveraging modern technologies. Feel free to customize and extend its functionality as needed. 156 | -------------------------------------------------------------------------------- /app/(auth)/forget-password/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@/contexts/AuthContext"; 4 | import { auth } from "@/lib/firebase"; 5 | import { createUser } from "@/lib/firestore/user/write"; 6 | import { Button } from "@nextui-org/react"; 7 | import { 8 | createUserWithEmailAndPassword, 9 | sendPasswordResetEmail, 10 | updateProfile, 11 | } from "firebase/auth"; 12 | import Link from "next/link"; 13 | import { useRouter } from "next/navigation"; 14 | import { useEffect, useState } from "react"; 15 | import toast from "react-hot-toast"; 16 | 17 | export default function Page() { 18 | const { user } = useAuth(); 19 | const router = useRouter(); 20 | const [isLoading, setIsLoading] = useState(false); 21 | const [data, setData] = useState({}); 22 | 23 | const handleData = (key, value) => { 24 | setData({ 25 | ...data, 26 | [key]: value, 27 | }); 28 | }; 29 | 30 | const handleSendEmail = async () => { 31 | setIsLoading(true); 32 | try { 33 | await sendPasswordResetEmail(auth, data?.email); 34 | toast.success("Reset Link has been sent to your email!"); 35 | } catch (error) { 36 | toast.error(error?.message); 37 | } 38 | setIsLoading(false); 39 | }; 40 | 41 | return ( 42 |
43 |
44 |
45 | Logo 46 |
47 |
48 |

Forgot Password

49 |
{ 51 | e.preventDefault(); 52 | handleSendEmail(); 53 | }} 54 | className="flex flex-col gap-3" 55 | > 56 | { 63 | handleData("email", e.target.value); 64 | }} 65 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 66 | /> 67 | 68 | 76 |
77 |
78 | 79 | 82 | 83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /app/(auth)/layout.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import AuthContextProvider from "@/contexts/AuthContext"; 4 | 5 | export default function Layout({ children }) { 6 | return {children}; 7 | } 8 | -------------------------------------------------------------------------------- /app/(auth)/login/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@/contexts/AuthContext"; 4 | import { auth } from "@/lib/firebase"; 5 | import { createUser } from "@/lib/firestore/user/write"; 6 | import { Button } from "@nextui-org/react"; 7 | import { 8 | GoogleAuthProvider, 9 | signInWithEmailAndPassword, 10 | signInWithPopup, 11 | } from "firebase/auth"; 12 | import Link from "next/link"; 13 | import { useRouter } from "next/navigation"; 14 | import { useEffect, useState } from "react"; 15 | import toast from "react-hot-toast"; 16 | 17 | export default function Page() { 18 | const { user } = useAuth(); 19 | const router = useRouter(); 20 | const [isLoading, setIsLoading] = useState(false); 21 | 22 | const [data, setData] = useState({}); 23 | 24 | const handleData = (key, value) => { 25 | setData({ 26 | ...data, 27 | [key]: value, 28 | }); 29 | }; 30 | 31 | const handleLogin = async () => { 32 | setIsLoading(true); 33 | try { 34 | await signInWithEmailAndPassword(auth, data?.email, data?.password); 35 | toast.success("Logged In Successfully"); 36 | } catch (error) { 37 | toast.error(error?.message); 38 | } 39 | setIsLoading(false); 40 | }; 41 | 42 | useEffect(() => { 43 | if (user) { 44 | router.push("/account"); 45 | } 46 | }, [user]); 47 | 48 | return ( 49 |
50 |
51 |
52 | Logo 53 |
54 |
55 |

Login With Email

56 |
{ 58 | e.preventDefault(); 59 | handleLogin(); 60 | }} 61 | className="flex flex-col gap-3" 62 | > 63 | { 70 | handleData("email", e.target.value); 71 | }} 72 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 73 | /> 74 | { 81 | handleData("password", e.target.value); 82 | }} 83 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 84 | /> 85 | 93 |
94 |
95 | 96 | 99 | 100 | 101 | 104 | 105 |
106 |
107 | 108 |
109 |
110 |
111 | ); 112 | } 113 | 114 | function SignInWithGoogleComponent() { 115 | const [isLoading, setIsLoading] = useState(false); 116 | const handleLogin = async () => { 117 | setIsLoading(true); 118 | try { 119 | const credential = await signInWithPopup(auth, new GoogleAuthProvider()); 120 | const user = credential.user; 121 | await createUser({ 122 | uid: user?.uid, 123 | displayName: user?.displayName, 124 | photoURL: user?.photoURL, 125 | }); 126 | } catch (error) { 127 | toast.error(error?.message); 128 | } 129 | setIsLoading(false); 130 | }; 131 | return ( 132 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@/contexts/AuthContext"; 4 | import { auth } from "@/lib/firebase"; 5 | import { createUser } from "@/lib/firestore/user/write"; 6 | import { Button } from "@nextui-org/react"; 7 | import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth"; 8 | import Link from "next/link"; 9 | import { useRouter } from "next/navigation"; 10 | import { useEffect, useState } from "react"; 11 | import toast from "react-hot-toast"; 12 | 13 | export default function Page() { 14 | const { user } = useAuth(); 15 | const router = useRouter(); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const [data, setData] = useState({}); 18 | 19 | const handleData = (key, value) => { 20 | setData({ 21 | ...data, 22 | [key]: value, 23 | }); 24 | }; 25 | const handleSignUp = async () => { 26 | setIsLoading(true); 27 | try { 28 | const credential = await createUserWithEmailAndPassword( 29 | auth, 30 | data?.email, 31 | data?.password 32 | ); 33 | await updateProfile(credential.user, { 34 | displayName: data?.name, 35 | }); 36 | const user = credential.user; 37 | await createUser({ 38 | uid: user?.uid, 39 | displayName: data?.name, 40 | photoURL: user?.photoURL, 41 | }); 42 | toast.success("Successfully Sign Up"); 43 | router.push("/account"); 44 | } catch (error) { 45 | toast.error(error?.message); 46 | } 47 | setIsLoading(false); 48 | }; 49 | 50 | return ( 51 |
52 |
53 |
54 | Logo 55 |
56 |
57 |

Sign Up With Email

58 |
{ 60 | e.preventDefault(); 61 | handleSignUp(); 62 | }} 63 | className="flex flex-col gap-3" 64 | > 65 | { 72 | handleData("name", e.target.value); 73 | }} 74 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 75 | /> 76 | { 83 | handleData("email", e.target.value); 84 | }} 85 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 86 | /> 87 | { 94 | handleData("password", e.target.value); 95 | }} 96 | className="px-3 py-2 rounded-xl border focus:outline-none w-full" 97 | /> 98 | 106 |
107 |
108 | 109 | 112 | 113 |
114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /app/(checkout)/checkout-cod/page.jsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/app/components/Footer"; 2 | import Header from "@/app/components/Header"; 3 | import { admin, adminDB } from "@/lib/firebase_admin"; 4 | import Link from "next/link"; 5 | 6 | const fetchCheckout = async (checkoutId) => { 7 | const list = await adminDB 8 | .collectionGroup("checkout_sessions_cod") 9 | .where("id", "==", checkoutId) 10 | .get(); 11 | if (list.docs.length === 0) { 12 | throw new Error("Invalid Checkout ID"); 13 | } 14 | return list.docs[0].data(); 15 | }; 16 | 17 | const processOrder = async ({ checkout }) => { 18 | const order = await adminDB.doc(`orders/${checkout?.id}`).get(); 19 | if (order.exists) { 20 | return false; 21 | } 22 | const uid = checkout?.metadata?.uid; 23 | 24 | await adminDB.doc(`orders/${checkout?.id}`).set({ 25 | checkout: checkout, 26 | payment: { 27 | amount: checkout?.line_items?.reduce((prev, curr) => { 28 | return prev + curr?.price_data?.unit_amount * curr?.quantity; 29 | }, 0), 30 | }, 31 | uid: uid, 32 | id: checkout?.id, 33 | paymentMode: "cod", 34 | timestampCreate: admin.firestore.Timestamp.now(), 35 | }); 36 | 37 | const productList = checkout?.line_items?.map((item) => { 38 | return { 39 | productId: item?.price_data?.product_data?.metadata?.productId, 40 | quantity: item?.quantity, 41 | }; 42 | }); 43 | 44 | const user = await adminDB.doc(`users/${uid}`).get(); 45 | 46 | const productIdsList = productList?.map((item) => item?.productId); 47 | 48 | const newCartList = (user?.data()?.carts ?? []).filter( 49 | (cartItem) => !productIdsList.includes(cartItem?.id) 50 | ); 51 | 52 | await adminDB.doc(`users/${uid}`).set( 53 | { 54 | carts: newCartList, 55 | }, 56 | { merge: true } 57 | ); 58 | 59 | const batch = adminDB.batch(); 60 | 61 | productList?.forEach((item) => { 62 | batch.update(adminDB.doc(`products/${item?.productId}`), { 63 | orders: admin.firestore.FieldValue.increment(item?.quantity), 64 | }); 65 | }); 66 | 67 | await batch.commit(); 68 | return true; 69 | }; 70 | 71 | export default async function Page({ searchParams }) { 72 | const { checkout_id } = searchParams; 73 | const checkout = await fetchCheckout(checkout_id); 74 | 75 | const result = await processOrder({ checkout }); 76 | 77 | return ( 78 |
79 |
80 |
81 |
82 | 83 |
84 |

85 | Your Order Is{" "} 86 | Successfully Placed 87 |

88 |
89 | 90 | 93 | 94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /app/(checkout)/checkout-failed/page.jsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/app/components/Footer"; 2 | import Header from "@/app/components/Header"; 3 | import { adminDB } from "@/lib/firebase_admin"; 4 | import Link from "next/link"; 5 | 6 | const fetchCheckout = async (checkoutId) => { 7 | const list = await adminDB 8 | .collectionGroup("checkout_sessions") 9 | .where("id", "==", checkoutId) 10 | .get(); 11 | if (list.docs.length === 0) { 12 | throw new Error("Invalid Checkout ID"); 13 | } 14 | return list.docs[0].data(); 15 | }; 16 | 17 | export default async function Page({ searchParams }) { 18 | const { checkout_id } = searchParams; 19 | const checkout = await fetchCheckout(checkout_id); 20 | return ( 21 |
22 |
23 |
24 |
25 | 26 |
27 |

Your Payment Was Not Success

28 |
29 | 30 | 33 | 34 | 35 | 38 | 39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/(checkout)/checkout-success/components/SuccessMessage.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import confetti from "canvas-confetti"; 4 | import { useEffect } from "react"; 5 | 6 | export default function SuccessMessage() { 7 | useEffect(() => { 8 | confetti(); 9 | }, []); 10 | return <>; 11 | } 12 | -------------------------------------------------------------------------------- /app/(checkout)/checkout-success/page.jsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/app/components/Footer"; 2 | import Header from "@/app/components/Header"; 3 | import { admin, adminDB } from "@/lib/firebase_admin"; 4 | import Link from "next/link"; 5 | import SuccessMessage from "./components/SuccessMessage"; 6 | 7 | const fetchCheckout = async (checkoutId) => { 8 | const list = await adminDB 9 | .collectionGroup("checkout_sessions") 10 | .where("id", "==", checkoutId) 11 | .get(); 12 | if (list.docs.length === 0) { 13 | throw new Error("Invalid Checkout ID"); 14 | } 15 | return list.docs[0].data(); 16 | }; 17 | 18 | const fetchPayment = async (checkoutId) => { 19 | const list = await adminDB 20 | .collectionGroup("payments") 21 | .where("metadata.checkoutId", "==", checkoutId) 22 | .where("status", "==", "succeeded") 23 | .get(); 24 | if (list.docs.length === 0) { 25 | throw new Error("Invalid Checkout ID"); 26 | } 27 | return list.docs[0].data(); 28 | }; 29 | 30 | const processOrder = async ({ payment, checkout }) => { 31 | const order = await adminDB.doc(`orders/${payment?.id}`).get(); 32 | if (order.exists) { 33 | return false; 34 | } 35 | const uid = payment?.metadata?.uid; 36 | 37 | await adminDB.doc(`orders/${payment?.id}`).set({ 38 | checkout: checkout, 39 | payment: payment, 40 | uid: uid, 41 | id: payment?.id, 42 | paymentMode: "prepaid", 43 | timestampCreate: admin.firestore.Timestamp.now(), 44 | }); 45 | 46 | const productList = checkout?.line_items?.map((item) => { 47 | return { 48 | productId: item?.price_data?.product_data?.metadata?.productId, 49 | quantity: item?.quantity, 50 | }; 51 | }); 52 | 53 | const user = await adminDB.doc(`users/${uid}`).get(); 54 | 55 | const productIdsList = productList?.map((item) => item?.productId); 56 | 57 | const newCartList = (user?.data()?.carts ?? []).filter( 58 | (cartItem) => !productIdsList.includes(cartItem?.id) 59 | ); 60 | 61 | await adminDB.doc(`users/${uid}`).set( 62 | { 63 | carts: newCartList, 64 | }, 65 | { merge: true } 66 | ); 67 | 68 | const batch = adminDB.batch(); 69 | 70 | productList?.forEach((item) => { 71 | batch.update(adminDB.doc(`products/${item?.productId}`), { 72 | orders: admin.firestore.FieldValue.increment(item?.quantity), 73 | }); 74 | }); 75 | 76 | await batch.commit(); 77 | return true; 78 | }; 79 | 80 | export default async function Page({ searchParams }) { 81 | const { checkout_id } = searchParams; 82 | const checkout = await fetchCheckout(checkout_id); 83 | const payment = await fetchPayment(checkout_id); 84 | 85 | const result = await processOrder({ checkout, payment }); 86 | 87 | return ( 88 |
89 |
90 | 91 |
92 |
93 | 94 |
95 |

96 | Your Order Is{" "} 97 | Successfully Placed 98 |

99 |
100 | 101 | 104 | 105 |
106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /app/(pages)/categories/[categoryId]/page.jsx: -------------------------------------------------------------------------------- 1 | import { ProductCard } from "@/app/components/Products"; 2 | import { getCategory } from "@/lib/firestore/categories/read_server"; 3 | import { getProductsByCategory } from "@/lib/firestore/products/read_server"; 4 | 5 | export async function generateMetadata({ params }) { 6 | const { categoryId } = params; 7 | const category = await getCategory({ id: categoryId }); 8 | 9 | return { 10 | title: `${category?.name} | Category`, 11 | openGraph: { 12 | images: [category?.imageURL], 13 | }, 14 | }; 15 | } 16 | 17 | export default async function Page({ params }) { 18 | const { categoryId } = params; 19 | const category = await getCategory({ id: categoryId }); 20 | const products = await getProductsByCategory({ categoryId: categoryId }); 21 | return ( 22 |
23 |
24 |

{category.name}

25 |
26 | {products?.map((item) => { 27 | return ; 28 | })} 29 |
30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/(pages)/collections/[collectionId]/page.jsx: -------------------------------------------------------------------------------- 1 | import { ProductCard } from "@/app/components/Products"; 2 | import { getCollection } from "@/lib/firestore/collections/read_server"; 3 | import { getProduct } from "@/lib/firestore/products/read_server"; 4 | 5 | export async function generateMetadata({ params }) { 6 | const { collectionId } = params; 7 | const collection = await getCollection({ id: collectionId }); 8 | 9 | return { 10 | title: `${collection?.title} | Collection`, 11 | description: collection?.subTitle ?? "", 12 | openGraph: { 13 | images: [collection?.imageURL], 14 | }, 15 | }; 16 | } 17 | 18 | export default async function Page({ params }) { 19 | const { collectionId } = params; 20 | const collection = await getCollection({ id: collectionId }); 21 | return ( 22 |
23 |
24 |
25 | 26 |
27 |

28 | {collection.title} 29 |

30 |

{collection.subTitle}

31 |
32 | {collection?.products?.map((productId) => { 33 | return ; 34 | })} 35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | async function Product({ productId }) { 42 | const product = await getProduct({ id: productId }); 43 | return ; 44 | } 45 | -------------------------------------------------------------------------------- /app/(pages)/layout.jsx: -------------------------------------------------------------------------------- 1 | import Footer from "../components/Footer"; 2 | import Header from "../components/Header"; 3 | 4 | export default function Layout({ children }) { 5 | return ( 6 |
7 |
8 | {children} 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/(pages)/products/[productId]/components/AddReiveiw.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAuth } from "@/contexts/AuthContext"; 4 | import { addReview } from "@/lib/firestore/reviews/write"; 5 | import { useUser } from "@/lib/firestore/user/read"; 6 | import { Rating } from "@mui/material"; 7 | import { Button } from "@nextui-org/react"; 8 | import { useState } from "react"; 9 | import toast from "react-hot-toast"; 10 | 11 | export default function AddReview({ productId }) { 12 | const [isLoading, setIsLoading] = useState(false); 13 | const [rating, setRating] = useState(4); 14 | const [message, setMessage] = useState(""); 15 | const { user } = useAuth(); 16 | const { data: userData } = useUser({ uid: user?.uid }); 17 | 18 | const handleSubmit = async () => { 19 | setIsLoading(true); 20 | try { 21 | if (!user) { 22 | throw new Error("Please Logged In First"); 23 | } 24 | await addReview({ 25 | displayName: userData?.displayName, 26 | message: message, 27 | photoURL: userData?.photoURL, 28 | productId: productId, 29 | rating: rating, 30 | uid: user?.uid, 31 | }); 32 | setMessage(""); 33 | toast.success("Successfully Submitted"); 34 | } catch (error) { 35 | toast.error(error?.message); 36 | } 37 | setIsLoading(false); 38 | }; 39 | 40 | return ( 41 |
42 |

Rate This Products

43 | { 46 | setRating(newValue); 47 | }} 48 | /> 49 |