├── .eslintrc.json ├── app ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ ├── Poppins.woff2 │ └── GeistMonoVF.woff ├── (client) │ ├── loading.tsx │ ├── draft-mode │ │ ├── disable │ │ │ └── route.ts │ │ └── enable │ │ │ └── route.ts │ ├── page.tsx │ ├── categories │ │ └── [slug] │ │ │ └── page.tsx │ ├── search │ │ └── page.tsx │ ├── layout.tsx │ ├── orders │ │ └── page.tsx │ ├── api │ │ └── webhook │ │ │ └── route.ts │ ├── success │ │ └── page.tsx │ ├── product │ │ └── [slug] │ │ │ └── page.tsx │ └── cart │ │ └── page.tsx ├── studio │ ├── layout.tsx │ └── [[...tool]] │ │ └── page.tsx └── globals.css ├── images ├── logo.png ├── payment.png ├── emptyCart.png └── loaderImage.png ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── postcss.config.mjs ├── sanity ├── helpers │ ├── couponCodes.ts │ └── index.ts ├── lib │ ├── backendClient.ts │ ├── image.ts │ ├── client.ts │ └── live.ts ├── schemaTypes │ ├── index.ts │ ├── categoryType.ts │ ├── saleType.ts │ ├── productType.ts │ ├── blockContentType.ts │ └── orderType.ts ├── structure.ts └── env.ts ├── lib ├── utils.ts └── stripe.ts ├── .vscode └── settings.json ├── next.config.ts ├── .env ├── components ├── Title.tsx ├── ui │ ├── collapsible.tsx │ ├── input.tsx │ ├── separator.tsx │ ├── badge.tsx │ ├── tooltip.tsx │ ├── popover.tsx │ ├── scroll-area.tsx │ ├── button.tsx │ ├── card.tsx │ ├── category-selector.tsx │ ├── table.tsx │ ├── dialog.tsx │ ├── command.tsx │ └── carousel.tsx ├── Container.tsx ├── Categories.tsx ├── PriceFormatter.tsx ├── Loader.tsx ├── Footer.tsx ├── PriceView.tsx ├── DisableDraftMode.tsx ├── ProductGrid.tsx ├── ProductList.tsx ├── ProductCartBar.tsx ├── CartIcon.tsx ├── EmptyCart.tsx ├── QuantityButtons.tsx ├── NoAccessToCart.tsx ├── AddToCartButton.tsx ├── DiscountBanner.tsx ├── OrdersComponent.tsx ├── OrderDetailsDialog.tsx ├── ProductCard.tsx └── Header.tsx ├── sanity.cli.ts ├── middleware.ts ├── components.json ├── .gitignore ├── tsconfig.json ├── sanity.config.ts ├── README.md ├── package.json ├── tailwind.config.ts ├── actions └── createCheckoutSession.ts ├── store.ts ├── sanity.types.ts └── schema.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/payment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/images/payment.png -------------------------------------------------------------------------------- /images/emptyCart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/images/emptyCart.png -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/Poppins.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/app/fonts/Poppins.woff2 -------------------------------------------------------------------------------- /images/loaderImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/images/loaderImage.png -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noorjsdivs/ecommerce-app-yt-1/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /sanity/helpers/couponCodes.ts: -------------------------------------------------------------------------------- 1 | export const COUPON_CODES = { 2 | BERRY: "BERRY", 3 | NEW_YEAR: "NEW_YEAR", 4 | } as const; 5 | 6 | export type CouponCode = keyof typeof COUPON_CODES; 7 | -------------------------------------------------------------------------------- /app/(client)/loading.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "@/components/Loader"; 2 | import React from "react"; 3 | 4 | const loading = () => { 5 | return ; 6 | }; 7 | 8 | export default loading; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "clsx", 4 | "ecommerce", 5 | "lucide", 6 | "nextjs", 7 | "tailwindcss", 8 | "turbopack", 9 | "typegen" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/(client)/draft-mode/disable/route.ts: -------------------------------------------------------------------------------- 1 | import { draftMode } from "next/headers"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function GET(request: NextRequest) { 5 | await (await draftMode()).disable(); 6 | return NextResponse.redirect(new URL("/", request.url)); 7 | } 8 | -------------------------------------------------------------------------------- /lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | if (!process.env.STRIPE_SECRET_KEY) { 4 | throw new Error("STRIPE_SCERET_KEY is not set"); 5 | } 6 | 7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { 8 | apiVersion: "2024-11-20.acacia", 9 | }); 10 | 11 | export default stripe; 12 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "cdn.sanity.io", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | export default nextConfig; 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VERCEL_URL= 2 | NEXT_PUBLIC_BASE_URL= 3 | NEXT_PUBLIC_SANITY_PROJECT_ID= 4 | NEXT_PUBLIC_SANITY_DATASET= 5 | NEXT_PUBLIC_SANITY_API_VERSION= 6 | SANITY_API_TOKEN= 7 | SANITY_API_READ_TOKEN= 8 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 9 | CLERK_SECRET_KEY= 10 | STRIPE_SECRET_KEY= 11 | STRIPE_WEBHOOK_SECRET= 12 | 13 | # stripe listen --forward-to localhost:3000/webhook 14 | -------------------------------------------------------------------------------- /components/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | interface Props { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | const Title = ({ children, className }: Props) => { 8 | return ( 9 |

{children}

10 | ); 11 | }; 12 | 13 | export default Title; 14 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /components/Container.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | 3 | interface Props { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | const Container = ({ children, className }: Props) => { 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | export default Container; 17 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file lets you run `$ sanity [command]` in this folder 3 | * Go to https://www.sanity.io/docs/cli to learn more. 4 | **/ 5 | import { defineCliConfig } from 'sanity/cli' 6 | 7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID 8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET 9 | 10 | export default defineCliConfig({ api: { projectId, dataset } }) 11 | -------------------------------------------------------------------------------- /sanity/lib/backendClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "next-sanity"; 2 | 3 | import { apiVersion, dataset, projectId } from "../env"; 4 | 5 | export const backendClient = createClient({ 6 | projectId, 7 | dataset, 8 | apiVersion, 9 | useCdn: true, // Set to false if statically generating pages, using ISR or tag-based revalidation 10 | // revalidation 11 | token: process.env.SANITY_API_TOKEN, 12 | }); 13 | -------------------------------------------------------------------------------- /sanity/lib/image.ts: -------------------------------------------------------------------------------- 1 | import createImageUrlBuilder from '@sanity/image-url' 2 | import { SanityImageSource } from "@sanity/image-url/lib/types/types"; 3 | 4 | import { dataset, projectId } from '../env' 5 | 6 | // https://www.sanity.io/docs/image-url 7 | const builder = createImageUrlBuilder({ projectId, dataset }) 8 | 9 | export const urlFor = (source: SanityImageSource) => { 10 | return builder.image(source) 11 | } 12 | -------------------------------------------------------------------------------- /components/Categories.tsx: -------------------------------------------------------------------------------- 1 | import { Category } from "@/sanity.types"; 2 | import React from "react"; 3 | import CategorySelector from "./ui/category-selector"; 4 | interface Props { 5 | categories: Category[]; 6 | } 7 | 8 | const Categories = ({ categories }: Props) => { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Categories; 17 | -------------------------------------------------------------------------------- /app/studio/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import React from "react"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Ecommerce Backend", 6 | description: "Generated by create next app", 7 | }; 8 | 9 | const RootLayout = ({ children }: { children: React.ReactNode }) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default RootLayout; 18 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from "@clerk/nextjs/server"; 2 | 3 | export default clerkMiddleware(); 4 | 5 | export const config = { 6 | matcher: [ 7 | // Skip Next.js internals and all static files, unless found in search params 8 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 9 | // Always run for API routes 10 | "/(api|trpc)(.*)", 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /sanity/schemaTypes/index.ts: -------------------------------------------------------------------------------- 1 | import { type SchemaTypeDefinition } from "sanity"; 2 | 3 | import { blockContentType } from "./blockContentType"; 4 | import { categoryType } from "./categoryType"; 5 | import { productType } from "./productType"; 6 | import { orderType } from "./orderType"; 7 | import { salesType } from "./saleType"; 8 | 9 | export const schema: { types: SchemaTypeDefinition[] } = { 10 | types: [blockContentType, categoryType, productType, orderType, salesType], 11 | }; 12 | -------------------------------------------------------------------------------- /sanity/structure.ts: -------------------------------------------------------------------------------- 1 | import type { StructureResolver } from "sanity/structure"; 2 | 3 | // https://www.sanity.io/docs/structure-builder-cheat-sheet 4 | export const structure: StructureResolver = (S) => 5 | S.list() 6 | .title("Ecommerce Admin") 7 | .items([ 8 | S.documentTypeListItem("category").title("Categories"), 9 | 10 | S.divider(), 11 | ...S.documentTypeListItems().filter( 12 | (item) => item.getId() && !["category"].includes(item.getId()!) 13 | ), 14 | ]); 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /sanity/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "next-sanity"; 2 | 3 | import { apiVersion, dataset, projectId } from "../env"; 4 | 5 | export const client = createClient({ 6 | projectId, 7 | dataset, 8 | apiVersion, 9 | useCdn: true, // Set to false if statically generating pages, using ISR or tag-based revalidation 10 | stega: { 11 | studioUrl: 12 | process.env.NODE_ENV === "production" 13 | ? `https://${process.env.VERCEL_URL}/studio` 14 | : `${process.env.NEXT_PUBLIC_BASE_URL}/studio`, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /components/PriceFormatter.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | 3 | interface Props { 4 | amount: number | undefined; 5 | className?: string; 6 | } 7 | 8 | const PriceFormatter = ({ amount, className }: Props) => { 9 | const formattedPrice = new Number(amount).toLocaleString("en-US", { 10 | currency: "USD", 11 | style: "currency", 12 | minimumFractionDigits: 2, 13 | }); 14 | return ( 15 | 16 | {formattedPrice} 17 | 18 | ); 19 | }; 20 | 21 | export default PriceFormatter; 22 | -------------------------------------------------------------------------------- /sanity/env.ts: -------------------------------------------------------------------------------- 1 | export const apiVersion = 2 | process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-11-09' 3 | 4 | export const dataset = assertValue( 5 | process.env.NEXT_PUBLIC_SANITY_DATASET, 6 | 'Missing environment variable: NEXT_PUBLIC_SANITY_DATASET' 7 | ) 8 | 9 | export const projectId = assertValue( 10 | process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, 11 | 'Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID' 12 | ) 13 | 14 | function assertValue(v: T | undefined, errorMessage: string): T { 15 | if (v === undefined) { 16 | throw new Error(errorMessage) 17 | } 18 | 19 | return v 20 | } 21 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | # .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /app/(client)/draft-mode/enable/route.ts: -------------------------------------------------------------------------------- 1 | import { client } from "@/sanity/lib/client"; 2 | import { validatePreviewUrl } from "@sanity/preview-url-secret"; 3 | import { redirect } from "next/navigation"; 4 | import { draftMode } from "next/headers"; 5 | const token = process.env.SANITY_API_READ_TOKEN; 6 | export async function GET(request: Request) { 7 | const { isValid, redirectTo = "/" } = await validatePreviewUrl( 8 | client.withConfig({ token }), 9 | request.url 10 | ); 11 | 12 | if (!isValid) { 13 | return new Response("Invalid secret", { status: 400 }); 14 | } 15 | (await draftMode()).enable(); 16 | 17 | redirect(redirectTo); 18 | } 19 | -------------------------------------------------------------------------------- /app/(client)/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/components/Container"; 2 | import DiscountBanner from "@/components/DiscountBanner"; 3 | import ProductList from "@/components/ProductList"; 4 | import { getAllCategories, getAllProducts, getSale } from "@/sanity/helpers"; 5 | 6 | export default async function Home() { 7 | const products = await getAllProducts(); 8 | const categories = await getAllCategories(); 9 | const sales = await getSale(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import loaderImage from "@/images/loaderImage.png"; 2 | import Image from "next/image"; 3 | 4 | const Loader = () => { 5 | return ( 6 |
7 |
8 |
9 | 10 | loaderImage 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Loader; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/studio/[[...tool]]/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This route is responsible for the built-in authoring environment using Sanity Studio. 3 | * All routes under your studio path is handled by this file using Next.js' catch-all routes: 4 | * https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes 5 | * 6 | * You can learn more about the next-sanity package here: 7 | * https://github.com/sanity-io/next-sanity 8 | */ 9 | 10 | import { NextStudio } from 'next-sanity/studio' 11 | import config from '../../../sanity.config' 12 | 13 | export const dynamic = 'force-static' 14 | 15 | export { metadata, viewport } from 'next-sanity/studio' 16 | 17 | export default function StudioPage() { 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /sanity/schemaTypes/categoryType.ts: -------------------------------------------------------------------------------- 1 | import { TagIcon } from "@sanity/icons"; 2 | import { defineField, defineType } from "sanity"; 3 | 4 | export const categoryType = defineType({ 5 | name: "category", 6 | title: "Category", 7 | type: "document", 8 | icon: TagIcon, 9 | fields: [ 10 | defineField({ 11 | name: "title", 12 | type: "string", 13 | }), 14 | defineField({ 15 | name: "slug", 16 | type: "slug", 17 | options: { 18 | source: "title", 19 | }, 20 | }), 21 | defineField({ 22 | name: "description", 23 | type: "text", 24 | }), 25 | ], 26 | preview: { 27 | select: { 28 | title: "title", 29 | subtitle: "description", 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Container from "./Container"; 3 | import Image from "next/image"; 4 | import payment from "@/images/payment.png"; 5 | 6 | const Footer = () => { 7 | return ( 8 |
9 | 10 |
11 |

12 | Copyright © 2024{" "} 13 | reactBD all 14 | rights reserved. 15 |

16 | payment 17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Footer; 24 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /sanity/lib/live.ts: -------------------------------------------------------------------------------- 1 | // Querying with "sanityFetch" will keep content automatically updated 2 | // Before using it, import and render "" in your layout, see 3 | // https://github.com/sanity-io/next-sanity#live-content-api for more information. 4 | import { defineLive } from "next-sanity"; 5 | import { client } from "./client"; 6 | 7 | const token = process.env.SANITY_API_READ_TOKEN; 8 | 9 | if (!token) { 10 | throw new Error("Missing SANITY_API_READ_TOKEN"); 11 | } 12 | 13 | export const { sanityFetch, SanityLive } = defineLive({ 14 | client, 15 | serverToken: token, 16 | browserToken: token, 17 | fetchOptions: { 18 | revalidate: 0, 19 | }, 20 | // client: client.withConfig({ 21 | // // Live content is currently only available on the experimental API 22 | // // https://www.sanity.io/docs/api-versioning 23 | // apiVersion: "vX", 24 | // }), 25 | }); 26 | -------------------------------------------------------------------------------- /components/PriceView.tsx: -------------------------------------------------------------------------------- 1 | import { twMerge } from "tailwind-merge"; 2 | import PriceFormatter from "./PriceFormatter"; 3 | 4 | interface Props { 5 | price: number | undefined; 6 | discount: number | undefined; 7 | className?: string; 8 | label?: string; 9 | } 10 | const PriceView = ({ price, discount, label, className }: Props) => { 11 | return ( 12 |
13 |
14 | 15 | {price && discount && ( 16 | 20 | )} 21 |
22 |

{label}

23 |
24 | ); 25 | }; 26 | 27 | export default PriceView; 28 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /components/DisableDraftMode.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useDraftModeEnvironment } from "next-sanity/hooks"; 3 | import { useRouter } from "next/navigation"; 4 | import { Button } from "./ui/button"; 5 | 6 | const DisableDraftMode = () => { 7 | const environment = useDraftModeEnvironment(); 8 | const router = useRouter(); 9 | 10 | // Only show the disable draft mode button when outside of Presentation Tool 11 | if (environment !== "live" && environment !== "unknown") { 12 | return null; 13 | } 14 | 15 | const handleClick = async () => { 16 | await fetch("/draft-mode/disable"); 17 | router.refresh(); 18 | }; 19 | return ( 20 | 26 | ); 27 | }; 28 | 29 | export default DisableDraftMode; 30 | -------------------------------------------------------------------------------- /components/ProductGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Product } from "@/sanity.types"; 3 | import React from "react"; 4 | import ProductCard from "./ProductCard"; 5 | import { motion, AnimatePresence } from "framer-motion"; 6 | 7 | interface Props { 8 | products: Product[]; 9 | } 10 | 11 | const ProductGrid = ({ products }: Props) => { 12 | return ( 13 |
14 | {products?.map((product) => ( 15 | 16 | 22 | 23 | 24 | 25 | ))} 26 |
27 | ); 28 | }; 29 | 30 | export default ProductGrid; 31 | -------------------------------------------------------------------------------- /components/ProductList.tsx: -------------------------------------------------------------------------------- 1 | import { Category, Product } from "@/sanity.types"; 2 | import React from "react"; 3 | import ProductGrid from "./ProductGrid"; 4 | import Categories from "./Categories"; 5 | interface Props { 6 | products: Product[]; 7 | categories: Category[]; 8 | title?: boolean; 9 | } 10 | const ProductList = ({ products, categories, title }: Props) => { 11 | return ( 12 |
13 | 14 | {title && ( 15 |
16 |

17 | Day of the Deal 18 |

19 |

20 | Don’t wait. The time will never be just right. 21 |

22 |
23 | )} 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default ProductList; 31 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ProductCartBar.tsx: -------------------------------------------------------------------------------- 1 | import { MdFavoriteBorder } from "react-icons/md"; 2 | import { FaRegEye } from "react-icons/fa"; 3 | import { TbArrowsRightLeft } from "react-icons/tb"; 4 | import { RiShoppingBag4Line } from "react-icons/ri"; 5 | 6 | const ProductCartBar = () => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default ProductCartBar; 26 | -------------------------------------------------------------------------------- /components/CartIcon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import useCartStore from "@/store"; 3 | import Link from "next/link"; 4 | import { useEffect, useState } from "react"; 5 | import { MdOutlineShoppingCart } from "react-icons/md"; 6 | 7 | const CartIcon = () => { 8 | const [isClient, setIsClient] = useState(false); 9 | const groupedItems = useCartStore((state) => state.getGroupedItems()); 10 | useEffect(() => { 11 | setIsClient(true); 12 | }, []); 13 | if (!isClient) { 14 | return null; 15 | } 16 | return ( 17 | 21 | 22 |
23 |

24 | 25 | {groupedItems?.length ? groupedItems.length : 0}{" "} 26 | 27 | items 28 |

29 |

Cart

30 |
31 | 32 | ); 33 | }; 34 | 35 | export default CartIcon; 36 | -------------------------------------------------------------------------------- /app/(client)/categories/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/components/Container"; 2 | import ProductList from "@/components/ProductList"; 3 | import { getAllCategories, getProductsByCategory } from "@/sanity/helpers"; 4 | import React from "react"; 5 | interface Props { 6 | params: Promise<{ slug: string }>; 7 | } 8 | 9 | const CategoriesPage = async ({ params }: Props) => { 10 | const { slug } = await params; 11 | const products = await getProductsByCategory(slug); 12 | const categories = await getAllCategories(); 13 | return ( 14 |
15 | 16 |

17 | Search results for{" "} 18 | 19 | {slug 20 | .split("-") 21 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 22 | .join(" ")}{" "} 23 | Collection 24 | 25 |

26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default CategoriesPage; 33 | -------------------------------------------------------------------------------- /sanity.config.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /** 4 | * This configuration is used to for the Sanity Studio that’s mounted on the `\app\studio\[[...tool]]\page.tsx` route 5 | */ 6 | 7 | import { visionTool } from "@sanity/vision"; 8 | import { defineConfig } from "sanity"; 9 | import { structureTool } from "sanity/structure"; 10 | 11 | // Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works 12 | import { apiVersion, dataset, projectId } from "./sanity/env"; 13 | import { schema } from "./sanity/schemaTypes"; 14 | import { structure } from "./sanity/structure"; 15 | import { presentationTool } from "sanity/presentation"; 16 | 17 | export default defineConfig({ 18 | basePath: "/studio", 19 | projectId, 20 | dataset, 21 | // Add and edit the content schema in the './sanity/schemaTypes' folder 22 | schema, 23 | plugins: [ 24 | structureTool({ structure }), 25 | // Vision is for querying with GROQ from inside the Studio 26 | // https://www.sanity.io/docs/the-vision-plugin 27 | visionTool({ defaultApiVersion: apiVersion }), 28 | presentationTool({ 29 | previewUrl: { 30 | preview: "/", 31 | previewMode: { 32 | enable: "/draft-mode/enable", 33 | }, 34 | }, 35 | }), 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )) 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 33 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )) 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 34 | -------------------------------------------------------------------------------- /app/(client)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@/components/Container"; 2 | import ProductGrid from "@/components/ProductGrid"; 3 | import { searchProductsByName } from "@/sanity/helpers"; 4 | 5 | // interface Props { 6 | // searchParams: { 7 | // query: string; 8 | // }; 9 | // } 10 | 11 | const SearchPage = async ({ 12 | searchParams, 13 | }: { 14 | searchParams: Promise<{ query: string }>; 15 | }) => { 16 | const { query } = await searchParams; 17 | const products = await searchProductsByName(query); 18 | 19 | if (!products?.length) { 20 | return ( 21 |
22 |
23 |

24 | No products found for:{" "} 25 | {query} 26 |

27 |

Try searching with different keywords

28 |
29 |
30 | ); 31 | } 32 | 33 | return ( 34 |
35 | 36 |

37 | Search results for {query} 38 |

39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default SearchPage; 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /app/(client)/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "../globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import Header from "@/components/Header"; 6 | import Footer from "@/components/Footer"; 7 | import { SanityLive } from "@/sanity/lib/live"; 8 | import { Toaster } from "react-hot-toast"; 9 | import { VisualEditing } from "next-sanity"; 10 | import { draftMode } from "next/headers"; 11 | import DisableDraftMode from "@/components/DisableDraftMode"; 12 | 13 | const poppins = localFont({ 14 | src: "../fonts/Poppins.woff2", 15 | variable: "--font-poppins", 16 | weight: "400", 17 | preload: false, 18 | }); 19 | 20 | export const metadata: Metadata = { 21 | title: "Ecommerce App for Shoppers", 22 | description: "Generated by create next app", 23 | }; 24 | 25 | export default async function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | return ( 31 | 32 | 33 | 34 | {(await draftMode()).isEnabled && ( 35 | <> 36 | 37 | 38 | 39 | )} 40 |
41 | {children} 42 |