├── .nvmrc ├── .npmrc ├── .prettierrc ├── app ├── favicon.ico ├── robots.ts ├── category │ └── [categorySlug] │ │ ├── loading.tsx │ │ └── page.tsx ├── not-found.tsx ├── product │ └── [productSlug] │ │ ├── loading.tsx │ │ └── page.tsx ├── page.tsx ├── search │ └── page.tsx ├── contact │ └── page.tsx ├── cart │ ├── success │ │ └── page.tsx │ └── page.tsx ├── privacy-policy │ └── page.tsx ├── layout.tsx ├── actions.ts ├── about │ └── page.tsx ├── terms-of-service │ └── page.tsx ├── globals.css └── return-refund-policy │ └── page.tsx ├── public ├── og.webp ├── razorpay.png ├── banner │ ├── buyinbulk.webp │ ├── customize.webp │ └── Eco-Friendly.webp ├── categoryImages │ ├── pouch.webp │ ├── JUTE BAGS.webp │ ├── cotton bag.webp │ ├── denim bags.webp │ ├── canvas bags.webp │ └── file folder.webp ├── robots.txt ├── sitemap.xml ├── whatsapp_logo.svg ├── WhatsAppButtonGreenSmall.svg └── sitemap-0.xml ├── postcss.config.js ├── .env.example ├── sanity-typegen.json ├── next-sitemap.config.js ├── .eslintrc.json ├── lib ├── utils.ts ├── constants.ts └── types.ts ├── components ├── ui │ ├── aspect-ratio.tsx │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── button.tsx │ ├── card.tsx │ ├── table.tsx │ ├── use-toast.ts │ ├── sheet.tsx │ ├── form.tsx │ ├── toast.tsx │ ├── navigation-menu.tsx │ └── carousel.tsx ├── product │ ├── ProductsMarqueeWrapper.tsx │ ├── ProductsMarquee.tsx │ ├── ProductCard.tsx │ ├── ProductCarousel.tsx │ └── ProductDetailCard.tsx ├── skeletons │ ├── ProductsMarqueeWrapperSkeleton.tsx │ ├── ProductGridSkeleton.tsx │ └── ProductDetailCardSkeleton.tsx ├── Header.tsx ├── category │ ├── CategoryCard.tsx │ └── CategoryCardSection.tsx ├── MenuSheet.tsx ├── cart │ ├── AddToCartButton.tsx │ ├── CartSheet.tsx │ ├── CartProductCard.tsx │ └── CartOrderTable.tsx ├── SearchInput.tsx ├── Billboard.tsx ├── FloatingBar.tsx ├── Navbar.tsx └── Footer.tsx ├── sanity ├── lib │ ├── client.ts │ ├── image.ts │ └── queries.ts ├── env.ts ├── schemaTypes │ └── index.ts ├── types.ts └── extract.json ├── sanity.config.ts ├── sanity.cli.ts ├── components.json ├── next.config.ts ├── .gitignore ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json └── store └── useCartStore.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/og.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/og.webp -------------------------------------------------------------------------------- /public/razorpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/razorpay.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/banner/buyinbulk.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/banner/buyinbulk.webp -------------------------------------------------------------------------------- /public/banner/customize.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/banner/customize.webp -------------------------------------------------------------------------------- /public/banner/Eco-Friendly.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/banner/Eco-Friendly.webp -------------------------------------------------------------------------------- /public/categoryImages/pouch.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/pouch.webp -------------------------------------------------------------------------------- /public/categoryImages/JUTE BAGS.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/JUTE BAGS.webp -------------------------------------------------------------------------------- /public/categoryImages/cotton bag.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/cotton bag.webp -------------------------------------------------------------------------------- /public/categoryImages/denim bags.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/denim bags.webp -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SANITY_PROJECT_ID="" 2 | NEXT_PUBLIC_SANITY_DATASET="" 3 | RAZORPAY_ID="" 4 | RAZORPAY_KEY="" 5 | SITE_URL="" 6 | -------------------------------------------------------------------------------- /public/categoryImages/canvas bags.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/canvas bags.webp -------------------------------------------------------------------------------- /public/categoryImages/file folder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmanVarshney01/oxabags/HEAD/public/categoryImages/file folder.webp -------------------------------------------------------------------------------- /sanity-typegen.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./**/*.{ts,tsx,js,jsx}", 3 | "schema": "./sanity/extract.json", 4 | "generates": "./sanity/types.ts" 5 | } 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: process.env.SITE_URL, 4 | generateRobotsTxt: true, 5 | }; 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://www.oxabags.com/ 7 | 8 | # Sitemaps 9 | Sitemap: https://www.oxabags.com/sitemap.xml 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:eslint-plugin-next-on-pages/recommended" 5 | ], 6 | "plugins": ["eslint-plugin-next-on-pages"] 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://www.oxabags.com/sitemap-0.xml 4 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /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, 10 | }); 11 | -------------------------------------------------------------------------------- /sanity.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "sanity"; 2 | import { schema } from "./sanity/schemaTypes"; 3 | 4 | export default defineConfig({ 5 | projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, 6 | dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, 7 | schema, 8 | }); 9 | -------------------------------------------------------------------------------- /app/category/[categorySlug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import ProductGridSkeleton from "@/components/skeletons/ProductGridSkeleton"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /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 | const builder = createImageUrlBuilder({ projectId, dataset }); 7 | 8 | export const urlForImage = (source: SanityImageSource) => { 9 | return builder.image(source); 10 | }; 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const runtime = "edge"; 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 |

Page Not Found

9 | 10 | Go Home 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 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 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/product/[productSlug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import ProductDetailCardSkeleton from "@/components/skeletons/ProductDetailCardSkeleton"; 2 | import ProductsMarqueeWrapperSkeleton from "@/components/skeletons/ProductsMarqueeWrapperSkeleton"; 3 | 4 | export default function Loading() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev"; 2 | 3 | if (process.env.NODE_ENV === "development") { 4 | setupDevPlatform(); 5 | } 6 | 7 | import type { NextConfig } from "next"; 8 | 9 | const nextConfig: NextConfig = { 10 | images: { 11 | remotePatterns: [ 12 | { 13 | protocol: "https", 14 | hostname: "cdn.sanity.io", 15 | }, 16 | ], 17 | formats: ["image/webp"], 18 | }, 19 | experimental: { 20 | reactCompiler: true, 21 | }, 22 | }; 23 | 24 | export default nextConfig; 25 | -------------------------------------------------------------------------------- /components/product/ProductsMarqueeWrapper.tsx: -------------------------------------------------------------------------------- 1 | import ProductsMarquee from "./ProductsMarquee"; 2 | import { FeaturedProductsQueryResult } from "@/sanity/types"; 3 | 4 | export default function ProductsMarqueeWrapper({ 5 | products, 6 | }: { 7 | products: FeaturedProductsQueryResult; 8 | }) { 9 | return ( 10 |
11 |

12 | Our Most Popular Products 13 |

14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /sanity/env.ts: -------------------------------------------------------------------------------- 1 | export const apiVersion = 2 | process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-12-16' 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.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* 30 | !.env.example 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # Local Netlify folder 40 | .netlify 41 | 42 | # wrangler files 43 | .wrangler 44 | .dev.vars 45 | -------------------------------------------------------------------------------- /components/skeletons/ProductsMarqueeWrapperSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | export default function ProductsMarqueeWrapperSkeleton() { 4 | return ( 5 |
6 | 7 |
8 | {[...Array(4)].map((_, i) => ( 9 |
13 | 14 | 15 | 16 |
17 | ))} 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const SITE_NAME = "oxabags" 2 | export const SITE_URL = "https://www.oxabags.com/" 3 | export const SITE_DESCRIPTION = "Manufacturer, Wholesaler & Exporter of Jute bags, Cotton Bag for Corporate Events & Exhibitions" 4 | export const SITE_KEYWORDS = "Jute bags, Cotton Bag, Canvas Bags, File Folders, Denim Bags, Pouches" 5 | export const SITE_CATEGORY = "Shopping" 6 | export const SITE_IMAGE = "https://www.oxabags.com/og.webp" 7 | export const OWNER_NAME = "Kuldeep Gupta" 8 | export const OWNER_EMAIL = "info@oxabags.com" 9 | export const OWNER_PHONE_1 = "9868151526" 10 | export const OWNER_PHONE_2 = "9811365888" 11 | export const COMPANY_NAME = "Aman Enterprises" 12 | export const COMPANY_ADDRESS = "G-211, UPSIDC Industrial Area Phase-1 M. G. Road, Dholana GHAZIABAD -201015, UP" 13 | 14 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "imageLoader.js", 32 | "./sanity/types.ts" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const checkoutFormSchema = z.object({ 4 | name: z.string().min(2, { 5 | message: "Name must contain at least 2 characters(s)", 6 | }), 7 | email: z.string().email(), 8 | phoneNumber: z.string().min(10, { 9 | message: "Invalid phone number", 10 | }), 11 | addressLine: z.string().min(2, { 12 | message: "Address too short", 13 | }), 14 | city: z.string().min(2, { 15 | message: "Invalid city", 16 | }), 17 | state: z.string().min(2, { 18 | message: "Invalid state", 19 | }), 20 | country: z.string().optional(), 21 | zipcode: z.string().min(6, { 22 | message: "Invalid zipcode", 23 | }), 24 | }); 25 | 26 | export const invoiceSchema = checkoutFormSchema.extend({ 27 | products: z.array( 28 | z.object({ 29 | name: z.string(), 30 | price: z.number(), 31 | quantity: z.number(), 32 | }), 33 | ), 34 | }); 35 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/skeletons/ProductGridSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | function ProductCardSkeleton() { 4 | return ( 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default function ProductGridSkeleton() { 16 | return ( 17 |
18 | 19 |
20 | {[...Array(10)].map((_, i) => ( 21 | 22 | ))} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/toast" 11 | import { useToast } from "@/components/ui/use-toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Billboard from "@/components/Billboard"; 2 | import CategoryCardSection from "@/components/category/CategoryCardSection"; 3 | import ProductsMarqueeWrapper from "@/components/product/ProductsMarqueeWrapper"; 4 | import ProductsMarqueeWrapperSkeleton from "@/components/skeletons/ProductsMarqueeWrapperSkeleton"; 5 | import { client } from "@/sanity/lib/client"; 6 | import { featuredProductsQuery } from "@/sanity/lib/queries"; 7 | import { FeaturedProductsQueryResult } from "@/sanity/types"; 8 | import { Suspense } from "react"; 9 | 10 | export default async function Home() { 11 | const products = await client.fetch( 12 | featuredProductsQuery, 13 | ); 14 | 15 | return ( 16 |
17 | 18 | }> 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Aman Varshney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/product/ProductsMarquee.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Carousel, 4 | CarouselContent, 5 | CarouselItem, 6 | CarouselNext, 7 | CarouselPrevious, 8 | } from "@/components/ui/carousel"; 9 | import { 10 | FeaturedProductsQueryResult, 11 | ProductBySlugQueryResult, 12 | } from "@/sanity/types"; 13 | import ProductCard from "./ProductCard"; 14 | 15 | export default function ProductsMarquee({ 16 | products, 17 | }: { 18 | products: ProductBySlugQueryResult[] | FeaturedProductsQueryResult; 19 | }) { 20 | return ( 21 | 26 | 27 | {products?.map((product, index: number) => ( 28 | 32 | 33 | 34 | ))} 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /public/whatsapp_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /components/skeletons/ProductDetailCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; 3 | 4 | export default function ProductDetailCardSkeleton() { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | {[...Array(6)].map((_, i) => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ))} 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { SITE_NAME } from "@/lib/constants"; 2 | import { client } from "@/sanity/lib/client"; 3 | import { categoriesQuery } from "@/sanity/lib/queries"; 4 | import Link from "next/link"; 5 | import CartSheet from "./cart/CartSheet"; 6 | import MenuSheet from "./MenuSheet"; 7 | import Navbar from "./Navbar"; 8 | import SearchInput from "./SearchInput"; 9 | import { CategoriesQueryResult } from "@/sanity/types"; 10 | 11 | export default async function Header() { 12 | const categories = await client.fetch(categoriesQuery); 13 | return ( 14 |
15 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | import ProductCard from "@/components/product/ProductCard"; 3 | import { client } from "@/sanity/lib/client"; 4 | import { searchProductsQuery } from "@/sanity/lib/queries"; 5 | import { SearchProductsQueryResult } from "@/sanity/types"; 6 | 7 | export default async function SearchPage( 8 | props: { 9 | searchParams: Promise<{ [key: string]: string }>; 10 | } 11 | ) { 12 | const searchParams = await props.searchParams; 13 | const { q } = searchParams; 14 | 15 | const searchResults = await client.fetch( 16 | searchProductsQuery, 17 | { 18 | searchTerm: q, 19 | }, 20 | ); 21 | 22 | return ( 23 |
24 |
25 |

26 | Search Results for {q} 27 |

28 | {searchResults && searchResults.length > 0 ? ( 29 |
30 | {searchResults.map((product) => ( 31 | 32 | ))} 33 |
34 | ) : ( 35 |

Sorry No Products found

36 | )} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /app/contact/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | COMPANY_ADDRESS, 3 | COMPANY_NAME, 4 | OWNER_EMAIL, 5 | OWNER_NAME, 6 | OWNER_PHONE_1, 7 | OWNER_PHONE_2, 8 | } from "@/lib/constants"; 9 | import type { Metadata } from "next"; 10 | import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; 11 | 12 | export const metadata: Metadata = { 13 | title: "Contact Us", 14 | }; 15 | 16 | export default function Contact() { 17 | return ( 18 |
19 |

Contact Us

20 | 21 | 22 | 23 | Company Name: 24 | {COMPANY_NAME} 25 | 26 | 27 | Owner Name: 28 | {OWNER_NAME} 29 | 30 | 31 | Address: 32 | {COMPANY_ADDRESS} 33 | 34 | 35 | Phone Number: 36 | 37 | {OWNER_PHONE_1}, {OWNER_PHONE_2} 38 | 39 | 40 | 41 | Email: 42 | {OWNER_EMAIL} 43 | 44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /components/category/CategoryCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardDescription, 5 | CardHeader, 6 | CardTitle, 7 | } from "@/components/ui/card"; 8 | import { cn } from "@/lib/utils"; 9 | import Image, { StaticImageData } from "next/image"; 10 | import Link from "next/link"; 11 | 12 | interface CategoryCardProps { 13 | title: string; 14 | description: string; 15 | image: StaticImageData; 16 | link: string; 17 | className?: string; 18 | } 19 | 20 | export default function CategoryCard({ 21 | title, 22 | description, 23 | image, 24 | link, 25 | className, 26 | }: CategoryCardProps) { 27 | return ( 28 | 29 | 35 | 36 | 37 | {title} 38 | 39 | 40 | {description} 41 | 42 | 43 | 44 | Cotton Bag 52 | 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /sanity/lib/queries.ts: -------------------------------------------------------------------------------- 1 | import { defineQuery } from "next-sanity"; 2 | 3 | export const productBySlugQuery = 4 | defineQuery(`*[_type == "product" && slug.current == $slug][0]{ 5 | id, 6 | name, 7 | images, 8 | color, 9 | weight, 10 | size, 11 | price, 12 | slug, 13 | description, 14 | features, 15 | fabric, 16 | showOnHomePage, 17 | category->{name}, 18 | }`); 19 | 20 | export const productsByCategoryQuery = 21 | defineQuery(`*[_type == "product" && category->slug.current == $categorySlug]{ 22 | name, 23 | images[0], 24 | price, 25 | slug, 26 | category->{name} 27 | }`); 28 | 29 | export const featuredProductsQuery = 30 | defineQuery(`*[ _type == "product" && showOnHomePage == true ] { 31 | id, 32 | name, 33 | price, 34 | images[0], 35 | category->{ 36 | name 37 | }, 38 | slug, 39 | }`); 40 | 41 | export const categoriesQuery = defineQuery(`*[_type == "category"]{ 42 | name, 43 | slug 44 | }`); 45 | 46 | export const productsSlugQuery = defineQuery(`*[_type == "product"] { 47 | slug { 48 | current 49 | } 50 | }`); 51 | 52 | export const searchProductsQuery = defineQuery(`*[_type == "product" && ( 53 | name match $searchTerm 54 | || description match $searchTerm 55 | || color match $searchTerm 56 | || size match $searchTerm 57 | || weight match $searchTerm 58 | || fabric match $searchTerm 59 | || features match $searchTerm 60 | || category->name match $searchTerm 61 | )] { 62 | id, 63 | name, 64 | price, 65 | images[0], 66 | category->{ 67 | name 68 | }, 69 | slug, 70 | }`); 71 | -------------------------------------------------------------------------------- /components/product/ProductCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ProductBySlugQueryResult, 3 | ProductsByCategoryQueryResult, 4 | } from "@/sanity/types"; 5 | import { urlForImage } from "@/sanity/lib/image"; 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | import { 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardFooter, 13 | CardHeader, 14 | CardTitle, 15 | } from "@/components/ui/card"; 16 | 17 | export default function ProductCard({ 18 | product, 19 | }: { 20 | product: ProductBySlugQueryResult | ProductsByCategoryQueryResult[0]; 21 | }) { 22 | return ( 23 | 24 | 25 | 26 | {product?.name!} 34 | 35 | 36 | 37 | {product?.name} 38 | 39 | 40 | {product?.category?.name} 41 | 42 | 43 | 44 |

₹ {product?.price}

45 |
46 |
47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/product/ProductCarousel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Carousel, 4 | CarouselContent, 5 | CarouselItem, 6 | CarouselNext, 7 | CarouselPrevious, 8 | } from "@/components/ui/carousel"; 9 | import { urlForImage } from "@/sanity/lib/image"; 10 | import { ProductBySlugQueryResult } from "@/sanity/types"; 11 | import Autoplay from "embla-carousel-autoplay"; 12 | import Image from "next/image"; 13 | import { AspectRatio } from "../ui/aspect-ratio"; 14 | 15 | export default function ProductCarousel({ 16 | product, 17 | }: { 18 | product: ProductBySlugQueryResult; 19 | }) { 20 | return ( 21 |
22 | 35 | 36 | {product?.images!.map((image: any, index: number) => ( 37 | 38 | 39 | {product?.name!} 47 | 48 | 49 | ))} 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /app/cart/success/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Check } from "lucide-react"; 3 | import Link from "next/link"; 4 | 5 | export const runtime = "edge"; 6 | 7 | export default async function SuccessPage( 8 | props: { 9 | searchParams: Promise<{ 10 | name: string; 11 | email: string; 12 | phoneNumber: string; 13 | short_url: string; 14 | }>; 15 | } 16 | ) { 17 | const searchParams = await props.searchParams; 18 | const email = searchParams.email; 19 | const phoneNumber = searchParams.phoneNumber; 20 | const short_url = searchParams.short_url; 21 | return ( 22 |
23 |
24 | 25 |
26 |

27 | Thank you for your order 28 |

29 |
30 |

31 | The order details and a link to the payment has been sent to your{" "} 32 | {email} and{" "} 33 | {phoneNumber} 34 |

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /app/privacy-policy/page.tsx: -------------------------------------------------------------------------------- 1 | export default function PrivacyPolicy() { 2 | return ( 3 |
4 |

Privacy Policy

5 |
6 |

How We Use Your Information

7 |

8 | We use the information we collect to process your orders, communicate 9 | with you about your purchases, provide customer support, and improve 10 | our products and services. 11 |

12 |

13 | We do not use your personal information for promotional email 14 | campaigns or direct marketing purposes. 15 |

16 |
17 | 18 |
19 |

Information Sharing and Disclosure

20 |

21 | We do not sell, trade, or rent your personal information to third 22 | parties for marketing purposes. However, we may share your information 23 | with trusted third-party service providers who assist us in operating 24 | our website, conducting business, or servicing you. 25 |

26 |
27 | 28 |
29 |

Data Security

30 |

31 | We implement industry-standard security measures to protect your 32 | personal information from unauthorized access, disclosure, alteration, 33 | or destruction. However, please note that no method of transmission 34 | over the internet or electronic storage is 100% secure. 35 |

36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /components/MenuSheet.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Sheet, 3 | SheetClose, 4 | SheetContent, 5 | SheetHeader, 6 | SheetTitle, 7 | SheetTrigger, 8 | } from "@/components/ui/sheet"; 9 | import { MenuIcon } from "lucide-react"; 10 | import Link from "next/link"; 11 | import SearchInput from "./SearchInput"; 12 | import { Button } from "./ui/button"; 13 | import { CategoriesQueryResult } from "@/sanity/types"; 14 | 15 | export default async function MenuSheet({ 16 | categories, 17 | }: { 18 | categories: CategoriesQueryResult; 19 | }) { 20 | return ( 21 | 22 | 23 | 27 | 28 | 29 | 30 | Menu 31 | 32 |
33 | 34 | 35 | 38 | 39 | 40 | {categories.map((category, index: number) => ( 41 | 42 | 43 | 46 | 47 | 48 | ))} 49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/cart/AddToCartButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useToast } from "@/components/ui/use-toast"; 4 | import { Actions, State, useCartStore } from "@/store/useCartStore"; 5 | import { PlusIcon } from "lucide-react"; 6 | import Link from "next/link"; 7 | import { useRouter } from "next/navigation"; 8 | import { Button } from "../ui/button"; 9 | import { ToastAction } from "../ui/toast"; 10 | import { ProductBySlugQueryResult } from "@/sanity/types"; 11 | 12 | export default function AddToCartButton({ 13 | product, 14 | }: { 15 | product: ProductBySlugQueryResult; 16 | }) { 17 | const { toast } = useToast(); 18 | const { addToCart, cart }: Actions & State = useCartStore(); 19 | const router = useRouter(); 20 | 21 | const productInCart = cart.find( 22 | (item) => item.slug!.current === product?.slug!.current, 23 | ); 24 | 25 | const handleAddToCart = () => { 26 | addToCart(product); 27 | toast({ 28 | title: "Item added to cart!", 29 | action: ( 30 | 31 | View Cart 32 | 33 | ), 34 | className: "border border-green-600 text-pretty", 35 | }); 36 | }; 37 | 38 | const handleBuyNow = () => { 39 | if (!productInCart) { 40 | addToCart(product); 41 | } 42 | router.push("/cart"); 43 | }; 44 | 45 | return ( 46 | <> 47 | 55 | 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oxabags 2 | 3 | oxabags is an e-commerce website showcasing our collection of bags. We are manufacturers, wholesalers, and exporters of jute bags and cotton bags, specializing in corporate events and exhibitions. 4 | 5 | ![image](https://github.com/user-attachments/assets/1cca385a-d409-4a6a-82ce-f408b5114299) 6 | 7 | ## Features 8 | 9 | - Browse a wide selection of bags 10 | - Shopping cart functionality 11 | - Secure checkout process with form validation 12 | - Statically generated product pages 13 | - Responsive design for mobile and desktop 14 | - Type-safe development with TypeScript 15 | 16 | ## Tech Stack 17 | 18 | - **Framework:** Next.js (App Router) 19 | - **Language:** TypeScript 20 | - **UI Components:** Shadcn/UI 21 | - **CMS:** Sanity.io 22 | - **Hosting:** Cloudflare Pages 23 | - **State Management:** Zustand 24 | - **Payment:** Razorpay Invoices 25 | 26 | ## Screenshots 27 | 28 |
29 | Click to view screenshots 30 | 31 | ![image](https://github.com/user-attachments/assets/1cca385a-d409-4a6a-82ce-f408b5114299) 32 | 33 | ![image](https://github.com/user-attachments/assets/bd62f80c-8002-4556-9261-2005575a5d05) 34 | 35 | ![Screenshot 2024-04-28 213034](https://github.com/AmanVarshney01/oxabags/assets/45312299/ea85b9c5-4f12-4a92-a126-22c8f0c3da45) 36 | 37 | ![Screenshot 2024-04-28 213054](https://github.com/AmanVarshney01/oxabags/assets/45312299/6289e16f-9baf-469f-be17-1f8ab176709e) 38 | 39 | ![Screenshot 2024-04-28 213112](https://github.com/AmanVarshney01/oxabags/assets/45312299/0b054ae0-a170-4bb2-bd29-d493027c8f54) 40 | 41 |
42 | 43 | ## Development 44 | 45 | To set up the project locally: 46 | 47 | 1. Clone the repository 48 | 2. Install dependencies: `npm install` 49 | 3. Set up environment variables for sanity and razorpay (see `.env.example`) 50 | 4. Run the development server: `npm run dev` 51 | 52 | ## Lighthouse Score 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import FloatingBar from "@/components/FloatingBar"; 2 | import Footer from "@/components/Footer"; 3 | import Header from "@/components/Header"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | import { cn } from "@/lib/utils"; 6 | import type { Metadata, Viewport } from "next"; 7 | import { Inter } from "next/font/google"; 8 | import "./globals.css"; 9 | import { 10 | SITE_CATEGORY, 11 | SITE_DESCRIPTION, 12 | SITE_IMAGE, 13 | SITE_KEYWORDS, 14 | SITE_NAME, 15 | SITE_URL, 16 | } from "@/lib/constants"; 17 | 18 | const fontSans = Inter({ subsets: ["latin"], variable: "--font-sans" }); 19 | 20 | export const viewport: Viewport = { 21 | themeColor: [ 22 | { media: "(prefers-color-scheme: light)", color: "white" }, 23 | { media: "(prefers-color-scheme: dark)", color: "black" }, 24 | ], 25 | }; 26 | 27 | export const metadata: Metadata = { 28 | metadataBase: new URL(SITE_URL), 29 | title: { 30 | template: `%s | ${SITE_NAME}`, 31 | default: SITE_NAME, 32 | }, 33 | description: SITE_DESCRIPTION, 34 | keywords: SITE_KEYWORDS, 35 | alternates: { 36 | canonical: "/", 37 | }, 38 | category: SITE_CATEGORY, 39 | openGraph: { 40 | title: SITE_NAME, 41 | description: SITE_DESCRIPTION, 42 | url: SITE_URL, 43 | type: "website", 44 | siteName: SITE_NAME, 45 | images: [ 46 | { 47 | url: SITE_IMAGE, 48 | }, 49 | ], 50 | }, 51 | }; 52 | 53 | export default function RootLayout({ 54 | children, 55 | }: { 56 | children: React.ReactNode; 57 | }) { 58 | return ( 59 | 60 | 66 |
67 |
68 | {children} 69 |
70 |
71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/category/CategoryCardSection.tsx: -------------------------------------------------------------------------------- 1 | import CategoryCard from "@/components/category/CategoryCard"; 2 | import CanvasBag from "@/public/categoryImages/canvas bags.webp"; 3 | import CottonBag from "@/public/categoryImages/cotton bag.webp"; 4 | import DenimBag from "@/public/categoryImages/denim bags.webp"; 5 | import FileFolder from "@/public/categoryImages/file folder.webp"; 6 | import JuteBag from "@/public/categoryImages/JUTE BAGS.webp"; 7 | import Pouch from "@/public/categoryImages/pouch.webp"; 8 | 9 | export default function CategoryCardSection() { 10 | return ( 11 |
12 |
13 | 19 | 25 | 31 | 32 | 38 | 39 | 45 | 46 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormMessage, 10 | } from "@/components/ui/form"; 11 | import { Input } from "@/components/ui/input"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import { SearchIcon } from "lucide-react"; 14 | import { useRouter } from "next/navigation"; 15 | import { useForm } from "react-hook-form"; 16 | import * as z from "zod"; 17 | 18 | const formSchema = z.object({ 19 | searchTerm: z.string().min(1), 20 | }); 21 | 22 | export default function SearchInput() { 23 | const form = useForm>({ 24 | resolver: zodResolver(formSchema), 25 | }); 26 | 27 | const router = useRouter(); 28 | 29 | async function onSubmit(values: z.infer) { 30 | router.push(`/search?q=${values.searchTerm}`); 31 | form.reset(); 32 | } 33 | return ( 34 |
35 | 36 | ( 40 | 41 | 42 |
43 | 44 | 58 |
59 |
60 |
61 | )} 62 | /> 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/Billboard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Carousel, 5 | CarouselContent, 6 | CarouselItem, 7 | } from "@/components/ui/carousel"; 8 | import Autoplay from "embla-carousel-autoplay"; 9 | import Image from "next/image"; 10 | import BuyInBulk from "../public/banner/buyinbulk.webp"; 11 | import Customize from "../public/banner/customize.webp"; 12 | import EcoFriendly from "../public/banner/Eco-Friendly.webp"; 13 | import { AspectRatio } from "./ui/aspect-ratio"; 14 | 15 | export default function Billboard() { 16 | return ( 17 |
18 | 31 | 32 | 33 | 34 | 42 | 43 | 44 | 45 | 46 | Customize your with your own design and preference. 54 | 55 | 56 | 57 | 58 | Customize your with your own design and preference. 66 | 67 | 68 | 69 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /app/category/[categorySlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductCard from "@/components/product/ProductCard"; 2 | import { client } from "@/sanity/lib/client"; 3 | import { categoriesQuery, productsByCategoryQuery } from "@/sanity/lib/queries"; 4 | import { 5 | CategoriesQueryResult, 6 | ProductsByCategoryQueryResult, 7 | } from "@/sanity/types"; 8 | import { Metadata } from "next"; 9 | 10 | export const dynamicParams = false; 11 | 12 | type Props = { 13 | params: Promise<{ categorySlug: string }>; 14 | }; 15 | 16 | export async function generateStaticParams() { 17 | const categorySlugs = 18 | await client.fetch(categoriesQuery); 19 | return categorySlugs.map((categorySlug) => { 20 | return { 21 | categorySlug: categorySlug.slug?.current, 22 | }; 23 | }); 24 | } 25 | 26 | export async function generateMetadata(props: Props): Promise { 27 | const params = await props.params; 28 | const title = params.categorySlug; 29 | 30 | return { 31 | title: title 32 | .split("-") 33 | .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) 34 | .join(" "), 35 | alternates: { 36 | canonical: `/category/${params.categorySlug}`, 37 | }, 38 | }; 39 | } 40 | 41 | export default async function CategoryPage(props: Props) { 42 | const params = await props.params; 43 | const products = await client.fetch( 44 | productsByCategoryQuery, 45 | { 46 | categorySlug: params.categorySlug, 47 | }, 48 | ); 49 | 50 | return ( 51 |
52 | {products && products.length > 0 ? ( 53 |
54 |

55 | {products[0]?.category?.name} 56 |

57 |
58 | {products.map((product, index: number) => ( 59 | 60 | ))} 61 |
62 |
63 | ) : ( 64 |

Sorry No Products found

65 | )} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/FloatingBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | OWNER_PHONE_1, 4 | SITE_DESCRIPTION, 5 | SITE_NAME, 6 | SITE_URL, 7 | } from "@/lib/constants"; 8 | import Whatsapp from "@/public/whatsapp_logo.svg"; 9 | import { PhoneCallIcon, Share } from "lucide-react"; 10 | import Image from "next/image"; 11 | import Link from "next/link"; 12 | import { usePathname } from "next/navigation"; 13 | 14 | export default function FloatingBar() { 15 | const pathname = usePathname(); 16 | const handleShare = async () => { 17 | if (navigator.share) { 18 | navigator.share({ 19 | title: SITE_NAME, 20 | text: SITE_DESCRIPTION, 21 | url: `${SITE_URL}${pathname}`, 22 | }); 23 | } else { 24 | navigator.clipboard.writeText(`${SITE_URL}${pathname}`); 25 | alert("Link copied to clipboard"); 26 | } 27 | }; 28 | 29 | return ( 30 |
31 |
32 | 36 | 37 | Call Us 38 | 39 | 43 | whatsapp logo 50 | Whatsapp 51 | 52 | 59 |
60 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { invoiceSchema } from "@/lib/types"; 4 | 5 | const razorpayID = process.env.RAZORPAY_ID; 6 | const razorpayKey = process.env.RAZORPAY_KEY; 7 | 8 | if (!razorpayID || !razorpayKey) { 9 | throw new Error("Razorpay credentials are not properly configured"); 10 | } 11 | 12 | export const sendInvoiceAction = async (input: unknown) => { 13 | try { 14 | const parsedInput = invoiceSchema.parse(input); 15 | 16 | const lineItems = parsedInput.products.map((product) => ({ 17 | name: product.name, 18 | amount: product.price * 100, 19 | currency: "INR", 20 | quantity: product.quantity, 21 | })); 22 | 23 | const invoice = { 24 | type: "invoice", 25 | customer: { 26 | name: parsedInput.name, 27 | email: parsedInput.email, 28 | contact: parsedInput.phoneNumber, 29 | shipping_address: { 30 | line1: parsedInput.addressLine, 31 | city: parsedInput.city, 32 | state: parsedInput.state, 33 | country: "India", 34 | zipcode: parsedInput.zipcode, 35 | }, 36 | }, 37 | line_items: lineItems, 38 | }; 39 | 40 | const response = await fetch("https://api.razorpay.com/v1/invoices", { 41 | method: "POST", 42 | headers: { 43 | "Content-Type": "application/json", 44 | Authorization: `Basic ${Buffer.from(`${razorpayID}:${razorpayKey}`).toString("base64")}`, 45 | }, 46 | body: JSON.stringify(invoice), 47 | }); 48 | 49 | if (!response.ok) { 50 | const errorData = await response.json(); 51 | throw new Error( 52 | errorData.error.description || "Failed to create invoice", 53 | ); 54 | } 55 | 56 | const data = await response.json(); 57 | 58 | if (data.status === "issued") { 59 | return { 60 | data: { 61 | name: data.customer_details.name, 62 | email: data.customer_details.email, 63 | phoneNumber: data.customer_details.contact, 64 | short_url: data.short_url, 65 | }, 66 | }; 67 | } else { 68 | throw new Error("Invoice was not issued successfully"); 69 | } 70 | } catch (error) { 71 | console.error("Error in sendInvoiceAction:", error); 72 | return { 73 | error: 74 | error instanceof Error ? error.message : "An unexpected error occurred", 75 | }; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | NavigationMenu, 4 | NavigationMenuContent, 5 | NavigationMenuItem, 6 | NavigationMenuLink, 7 | NavigationMenuList, 8 | NavigationMenuTrigger, 9 | navigationMenuTriggerStyle, 10 | } from "@/components/ui/navigation-menu"; 11 | import { CategoriesQueryResult } from "@/sanity/types"; 12 | import Link from "next/link"; 13 | 14 | export default function Navbar({ 15 | categories, 16 | }: { 17 | categories: CategoriesQueryResult; 18 | }) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | Home 26 | 27 | 28 | 29 | 30 | Products 31 | 32 |
    33 | {categories.map((category, index: number) => ( 34 |
  • 35 | 40 | 43 | {category.name} 44 | 45 | 46 |
  • 47 | ))} 48 |
49 |
50 |
51 | 52 | 53 | 54 | About Us 55 | 56 | 57 | 58 | 59 | 60 | 61 | Contact Us 62 | 63 | 64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /components/product/ProductDetailCard.tsx: -------------------------------------------------------------------------------- 1 | import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; 2 | import AddToCartButton from "../cart/AddToCartButton"; 3 | import ProductCarousel from "./ProductCarousel"; 4 | import { ProductBySlugQueryResult } from "@/sanity/types"; 5 | 6 | export default function ProductDetailCard({ 7 | product, 8 | }: { 9 | product: ProductBySlugQueryResult; 10 | }) { 11 | return ( 12 |
13 | 14 |
15 |

{product?.name}

16 |

17 | Price: ₹ {product?.price} 18 | (including GST) 19 |

20 | 21 | 22 | 23 | Product Code: 24 | {product?.id} 25 | 26 | 27 | Size: 28 | {product?.size} 29 | 30 | 31 | Fabric: 32 | {product?.fabric} 33 | 34 | 35 | Color: 36 | {product?.color} 37 | 38 | 39 | Weight: 40 | {product?.weight} 41 | 42 | 43 | Features: 44 | {product?.features} 45 | 46 | {product?.description == null ? null : ( 47 | 48 | Description: 49 | {product?.description} 50 | 51 | )} 52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /components/cart/CartSheet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Sheet, 4 | SheetContent, 5 | SheetDescription, 6 | SheetFooter, 7 | SheetHeader, 8 | SheetTitle, 9 | SheetTrigger, 10 | } from "@/components/ui/sheet"; 11 | import { useCartStore } from "@/store/useCartStore"; 12 | import { ShoppingCartIcon } from "lucide-react"; 13 | import { useRouter } from "next/navigation"; 14 | import { useState } from "react"; 15 | import { useShallow } from "zustand/shallow"; 16 | import { Button } from "../ui/button"; 17 | import CartProductCard from "./CartProductCard"; 18 | 19 | export default function CartSheet() { 20 | const [cart, removeFromCart, addToCart, deleteFromCart] = useCartStore( 21 | useShallow((state) => [ 22 | state.cart, 23 | state.removeFromCart, 24 | state.addToCart, 25 | state.deleteFromCart, 26 | ]), 27 | ); 28 | const [open, setOpen] = useState(false); 29 | const router = useRouter(); 30 | 31 | return ( 32 | 33 | 34 | 41 | 42 | 43 | 44 | Cart 45 | 46 | 47 | {cart.length > 0 ? "" : "Empty"} 48 | 49 |
50 | {cart.map((product, index: number) => ( 51 | 58 | ))} 59 |
60 | 61 | 72 | 73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oxabags", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "typegen": "sanity schema extract --path=sanity/extract.json && sanity typegen generate", 7 | "predev": "npm run typegen", 8 | "dev": "next dev --turbopack", 9 | "prebuild": "npm run typegen", 10 | "build": "next build", 11 | "postbuild": "next-sitemap", 12 | "start": "next start", 13 | "lint": "next lint", 14 | "pages:build": "pnpm dlx @cloudflare/next-on-pages", 15 | "preview": "pnpm run pages:build && wrangler pages dev", 16 | "deploy": "pnpm run pages:build && wrangler pages deploy" 17 | }, 18 | "dependencies": { 19 | "@hookform/resolvers": "^4.1.3", 20 | "@radix-ui/react-aspect-ratio": "^1.1.2", 21 | "@radix-ui/react-dialog": "^1.1.6", 22 | "@radix-ui/react-label": "^2.1.2", 23 | "@radix-ui/react-navigation-menu": "^1.2.5", 24 | "@radix-ui/react-slot": "^1.1.2", 25 | "@radix-ui/react-toast": "^1.2.6", 26 | "@sanity/client": "^6.28.3", 27 | "@sanity/image-url": "^1.1.0", 28 | "@sanity/vision": "^3.81.0", 29 | "class-variance-authority": "^0.7.1", 30 | "clsx": "^2.1.1", 31 | "embla-carousel-autoplay": "8.5.2", 32 | "embla-carousel-react": "8.5.2", 33 | "lucide-react": "^0.484.0", 34 | "next": "15.2.4", 35 | "next-sanity": "^9.9.6", 36 | "next-sitemap": "^4.2.3", 37 | "react": "19.0.0", 38 | "react-dom": "19.0.0", 39 | "react-hook-form": "^7.54.2", 40 | "sanity": "^3.81.0", 41 | "sharp": "^0.33.5", 42 | "styled-components": "^6.1.16", 43 | "tailwind-merge": "^3.0.2", 44 | "tailwindcss-animate": "^1.0.7", 45 | "zod": "^3.24.2", 46 | "zustand": "^5.0.3" 47 | }, 48 | "devDependencies": { 49 | "@cloudflare/next-on-pages": "^1.13.10", 50 | "@tailwindcss/postcss": "^4.0.17", 51 | "@types/node": "^22.13.13", 52 | "@types/react": "19.0.12", 53 | "@types/react-dom": "19.0.4", 54 | "babel-plugin-react-compiler": "19.0.0-beta-aeaed83-20250323", 55 | "eslint": "^9.23.0", 56 | "eslint-config-next": "15.2.4", 57 | "eslint-plugin-next-on-pages": "^1.13.10", 58 | "postcss": "^8.5.3", 59 | "prettier": "^3.5.3", 60 | "prettier-plugin-tailwindcss": "^0.6.11", 61 | "tailwindcss": "^4.0.17", 62 | "typescript": "^5.8.2", 63 | "wrangler": "^4.5.0" 64 | }, 65 | "pnpm": { 66 | "overrides": { 67 | "@types/react": "19.0.2", 68 | "@types/react-dom": "19.0.2" 69 | }, 70 | "onlyBuiltDependencies": [ 71 | "esbuild", 72 | "scrollmirror", 73 | "workerd", 74 | "yarn" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | export const metadata: Metadata = { 4 | title: "About Us", 5 | }; 6 | 7 | export default function About() { 8 | return ( 9 |
10 |

About Us

11 |
    12 |
  • 13 | Established in 2003, Aman Enterprises has been a cornerstone in the 14 | manufacturing and distribution of premium quality bags and pouches. 15 |
  • 16 |
  • 17 | Nestled within the industrious landscape of Uttar Pradesh State 18 | Industrial Development Corporation, Ghaziabad, Uttar Pradesh, our 19 | journey began with a vision to blend tradition with modernity, 20 | offering products that resonate with our customers' diverse 21 | needs. 22 |
  • 23 |
  • 24 | At Aman Enterprises, we take pride in our extensive range of 25 | offerings, including cotton bags, canvas, jute, denim pouches, and 26 | more. Our commitment to quality craftsmanship and attention to detail 27 | sets us apart in the industry. 28 |
  • 29 |
  • 30 | Each product is meticulously crafted to meet the highest standards of 31 | durability, functionality, and style. 32 |
  • 33 |
  • 34 | With years of experience and expertise, we have cultivated strong 35 | relationships with our customers, both individuals and organizations 36 | alike. 37 |
  • 38 |
  • 39 | We understand the unique requirements of our clients, which is why we 40 | offer the flexibility of bulk orders for organizations seeking 41 | high-quality bags and pouches for their various needs. 42 |
  • 43 |
  • 44 | Our dedication to customer satisfaction drives us to continuously 45 | innovate and improve our products and services. 46 |
  • 47 |
  • 48 | Whether you're an individual looking for eco-friendly bags or an 49 | organization in need of bulk purchases, Aman Enterprises is your 50 | reliable partner every step of the way. 51 |
  • 52 |
  • 53 | As a proud member of the community, we are committed to sustainability 54 | and ethical business practices. We strive to minimize our 55 | environmental footprint and contribute positively to society through 56 | our operations. 57 |
  • 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /app/terms-of-service/page.tsx: -------------------------------------------------------------------------------- 1 | export default function TermsOfService() { 2 | return ( 3 |
4 |

Terms Of Service

5 |
6 |

1. Acceptance of Terms

7 |

8 | By accessing and using oxabags.com, you agree to comply with and be 9 | bound by these Terms of Service. 10 |

11 |
12 | 13 |
14 |

2. Use of the Service

15 |

16 | Our website and its content are intended solely for personal and 17 | non-commercial use. You may not use our website for any illegal or 18 | unauthorized purpose. 19 |

20 |
21 | 22 |
23 |

3. Intellectual Property Rights

24 |

25 | All content, trademarks, logos, and intellectual property displayed on 26 | oxabags.com are the property of Aman Enterprise. You may not use, 27 | reproduce, or distribute any content from our website without prior 28 | written permission. 29 |

30 |
31 | 32 |
33 |

4. Payment Processing

34 |

35 | We utilize Razorpay, a third-party payment gateway, for processing 36 | payments on our website. By making a purchase through oxabags.com, you 37 | agree to abide by Razorpay's terms and conditions. 38 |

39 |

40 | Aman Enterprise shall not be liable for any issues, disputes, or 41 | discrepancies arising from payment processing through Razorpay. Please 42 | refer to Razorpay's policies and procedures for more information. 43 |

44 |
45 | 46 |
47 |

5. Limitation of Liability

48 |

49 | We strive to provide accurate and up-to-date information on our 50 | website. However, we do not guarantee the accuracy, completeness, or 51 | reliability of any content. 52 |

53 |

54 | Aman Enterprise shall not be liable for any direct, indirect, 55 | incidental, special, or consequential damages arising out of or in any 56 | way connected with the use of our website or services. 57 |

58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /sanity/schemaTypes/index.ts: -------------------------------------------------------------------------------- 1 | import { type SchemaTypeDefinition } from "sanity"; 2 | 3 | export const schema: { types: SchemaTypeDefinition[] } = { 4 | types: [ 5 | { 6 | name: "product", 7 | title: "Products", 8 | type: "document", 9 | fields: [ 10 | { 11 | name: "id", 12 | title: "ID", 13 | type: "number", 14 | validation: (Rule) => 15 | Rule.required().integer().positive().greaterThan(100), 16 | }, 17 | { 18 | name: "name", 19 | title: "Name", 20 | type: "string", 21 | }, 22 | { 23 | name: "images", 24 | type: "array", 25 | title: "Images", 26 | of: [{ type: "image" }], 27 | }, 28 | { 29 | name: "color", 30 | title: "Color", 31 | type: "string", 32 | }, 33 | { 34 | name: "size", 35 | title: "Size", 36 | type: "string", 37 | }, 38 | { 39 | name: "weight", 40 | title: "Weight", 41 | type: "string", 42 | }, 43 | { 44 | name: "fabric", 45 | title: "Fabric", 46 | type: "string", 47 | }, 48 | { 49 | name: "price", 50 | title: "Price", 51 | type: "number", 52 | }, 53 | { 54 | name: "features", 55 | title: "Features", 56 | type: "string", 57 | }, 58 | { 59 | name: "description", 60 | title: "Description", 61 | type: "text", 62 | }, 63 | { 64 | name: "slug", 65 | title: "Slug", 66 | type: "slug", 67 | options: { 68 | source: "name", 69 | }, 70 | }, 71 | { 72 | name: "showOnHomePage", 73 | title: "Show on Home Page", 74 | type: "boolean", 75 | }, 76 | { 77 | name: "category", 78 | title: "Product Category", 79 | type: "reference", 80 | to: [ 81 | { 82 | type: "category", 83 | }, 84 | ], 85 | }, 86 | ], 87 | }, 88 | { 89 | name: "category", 90 | type: "document", 91 | title: "Categories", 92 | fields: [ 93 | { 94 | name: "name", 95 | title: "Name of Category", 96 | type: "string", 97 | }, 98 | { 99 | name: "slug", 100 | title: "Slug", 101 | type: "slug", 102 | options: { 103 | source: "name", 104 | }, 105 | }, 106 | ], 107 | }, 108 | ], 109 | }; 110 | -------------------------------------------------------------------------------- /components/cart/CartProductCard.tsx: -------------------------------------------------------------------------------- 1 | import { urlForImage } from "@/sanity/lib/image"; 2 | import { DeleteIcon, MinusIcon, PlusIcon } from "lucide-react"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { Button } from "../ui/button"; 6 | import { Card, CardContent } from "../ui/card"; 7 | import { CartProduct } from "@/store/useCartStore"; 8 | 9 | export default function CartProductCard({ 10 | product, 11 | addToCart, 12 | removeFromCart, 13 | deleteFromCart, 14 | }: { 15 | addToCart: (Item: CartProduct) => void; 16 | removeFromCart: (Item: CartProduct) => void; 17 | deleteFromCart: (Item: CartProduct) => void; 18 | } & { product: CartProduct }) { 19 | return ( 20 | 21 | 22 | 23 | {product.name!} 31 | 32 |
33 |
34 |
35 | {product.name} 36 |
37 |
38 | ₹{product.price} 39 |
40 |
41 |
42 |
43 | 51 | 52 | {product.quantity} 53 | 54 | 62 |
63 | 71 |
72 |
73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /app/product/[productSlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductDetailCard from "@/components/product/ProductDetailCard"; 2 | import ProductsMarqueeWrapper from "@/components/product/ProductsMarqueeWrapper"; 3 | import { SITE_NAME, SITE_URL } from "@/lib/constants"; 4 | import { client } from "@/sanity/lib/client"; 5 | import { urlForImage } from "@/sanity/lib/image"; 6 | import { 7 | featuredProductsQuery, 8 | productBySlugQuery, 9 | productsSlugQuery, 10 | } from "@/sanity/lib/queries"; 11 | import { 12 | FeaturedProductsQueryResult, 13 | ProductBySlugQueryResult, 14 | ProductsSlugQueryResult, 15 | } from "@/sanity/types"; 16 | import type { Metadata, ResolvingMetadata } from "next"; 17 | 18 | export const dynamicParams = false; 19 | 20 | type Props = { 21 | params: Promise<{ productSlug: string }>; 22 | }; 23 | 24 | export async function generateStaticParams() { 25 | const productSlugs = 26 | await client.fetch(productsSlugQuery); 27 | return productSlugs.map((productSlug) => { 28 | return { 29 | productSlug: productSlug.slug?.current, 30 | }; 31 | }); 32 | } 33 | 34 | export async function generateMetadata( 35 | props: Props, 36 | parent: ResolvingMetadata, 37 | ): Promise { 38 | const params = await props.params; 39 | const product = await client.fetch( 40 | productBySlugQuery, 41 | { 42 | slug: params.productSlug, 43 | }, 44 | ); 45 | 46 | return { 47 | title: product?.name, 48 | alternates: { 49 | canonical: `/product/${params.productSlug}`, 50 | }, 51 | description: product?.features, 52 | openGraph: { 53 | title: product?.name!, 54 | description: product?.description!, 55 | url: `${SITE_URL}product/${params.productSlug}`, 56 | images: [ 57 | { 58 | url: urlForImage(product?.images?.[0]!).url(), 59 | width: 1200, 60 | height: 630, 61 | alt: product?.name!, 62 | }, 63 | ], 64 | }, 65 | }; 66 | } 67 | 68 | const ProductPage = async (props: Props) => { 69 | const params = await props.params; 70 | const product = await client.fetch( 71 | productBySlugQuery, 72 | { 73 | slug: params.productSlug, 74 | }, 75 | ); 76 | 77 | const favProducts = await client.fetch( 78 | featuredProductsQuery, 79 | ); 80 | 81 | const jsonLd = { 82 | "@context": SITE_URL, 83 | "@type": "Product", 84 | name: product?.name, 85 | description: product?.features, 86 | brand: { 87 | "@type": "Brand", 88 | name: SITE_NAME, 89 | }, 90 | }; 91 | 92 | return ( 93 |
94 | 95 | 96 |