├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prettier.config.js ├── public └── placeholder.png ├── src ├── app │ ├── Footer.tsx │ ├── MainNavigation.tsx │ ├── MobileMenu.tsx │ ├── Navbar.tsx │ ├── ReactQueryProvider.tsx │ ├── ShoppingCartButton.tsx │ ├── api │ │ ├── auth │ │ │ └── callback │ │ │ │ └── wix │ │ │ │ └── route.ts │ │ └── review-media-upload-url │ │ │ └── route.ts │ ├── checkout-success │ │ ├── ClearCart.tsx │ │ └── page.tsx │ ├── collections │ │ └── [slug] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── loading.tsx │ ├── not-found.tsx │ ├── opengraph-image.png │ ├── page.tsx │ ├── products │ │ ├── [slug] │ │ │ ├── ProductDetails.tsx │ │ │ ├── ProductMedia.tsx │ │ │ ├── ProductOptions.tsx │ │ │ ├── ProductPrice.tsx │ │ │ ├── ProductReviews.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── id │ │ │ └── [id] │ │ │ └── page.tsx │ ├── profile │ │ ├── MemberInfoForm.tsx │ │ ├── Orders.tsx │ │ └── page.tsx │ ├── shop │ │ ├── SearchFilterLayout.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ └── terms │ │ └── page.tsx ├── assets │ ├── banner.jpg │ └── logo.png ├── components │ ├── AddToCartButton.tsx │ ├── BackInStockNotificationButton.tsx │ ├── BuyNowButton.tsx │ ├── CheckoutButton.tsx │ ├── DiscountBadge.tsx │ ├── LoadingButton.tsx │ ├── Order.tsx │ ├── PaginationBar.tsx │ ├── Product.tsx │ ├── SearchField.tsx │ ├── UserButton.tsx │ ├── WixImage.tsx │ ├── reviews │ │ ├── CreateProductReviewButton.tsx │ │ ├── CreateProductReviewDialog.tsx │ │ ├── StarRatingInput.tsx │ │ └── useMediaUpload.ts │ └── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx ├── env.ts ├── hooks │ ├── auth.ts │ ├── back-in-stock.ts │ ├── cart.ts │ ├── checkout.ts │ ├── members.ts │ ├── reviews.ts │ └── use-toast.ts ├── lib │ ├── constants.ts │ ├── utils.ts │ ├── validation.ts │ ├── wix-client.base.ts │ ├── wix-client.browser.ts │ └── wix-client.server.ts ├── middleware.ts └── wix-api │ ├── auth.ts │ ├── backInStockNotifications.ts │ ├── cart.ts │ ├── checkout.ts │ ├── collections.ts │ ├── members.ts │ ├── orders.ts │ ├── products.ts │ └── reviews.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "prettier", 5 | "plugin:@tanstack/eslint-plugin-query/recommended" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.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 | # env files (can opt-in for commiting if needed) 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js 15 Wix Headless E-Commerce Store 2 | 3 | A beautiful and fast e-commerce website built with **Next.js 15** and **Wix Studio Headless APIs**. 4 | 5 | Includes everything an online store needs, like cart management, checkout, user accounts, email automations, and payment provider integration. 6 | 7 | Uses Next.js best practices like server-side rendering, URL state, optimistic updates, and more. 8 | 9 | Watch the tutorial for free: https://www.youtube.com/watch?v=gr--RC_naa0 10 | 11 | ![thumbnail](https://github.com/user-attachments/assets/61631ee7-832a-4d2e-82de-396627bb024f) 12 | -------------------------------------------------------------------------------- /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": "src/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 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | staleTimes: { 5 | dynamic: 30, 6 | }, 7 | }, 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-15-wix-store", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.9.0", 13 | "@radix-ui/react-accordion": "^1.2.0", 14 | "@radix-ui/react-checkbox": "^1.1.1", 15 | "@radix-ui/react-dialog": "^1.1.1", 16 | "@radix-ui/react-dropdown-menu": "^2.1.1", 17 | "@radix-ui/react-label": "^2.1.0", 18 | "@radix-ui/react-navigation-menu": "^1.2.0", 19 | "@radix-ui/react-select": "^2.1.1", 20 | "@radix-ui/react-slot": "^1.1.0", 21 | "@radix-ui/react-toast": "^1.2.1", 22 | "@radix-ui/react-tooltip": "^1.1.2", 23 | "@t3-oss/env-nextjs": "^0.11.1", 24 | "@tanstack/react-query": "^5.55.4", 25 | "@wix/ecom": "^1.0.727", 26 | "@wix/media": "^1.0.116", 27 | "@wix/members": "^1.0.100", 28 | "@wix/redirects": "^1.0.55", 29 | "@wix/reviews": "^1.0.36", 30 | "@wix/sdk": "^1.12.12", 31 | "@wix/stores": "^1.0.214", 32 | "class-variance-authority": "^0.7.0", 33 | "clsx": "^2.1.1", 34 | "date-fns": "^3.6.0", 35 | "js-cookie": "^3.0.5", 36 | "ky": "^1.7.2", 37 | "lucide-react": "^0.440.0", 38 | "next": "15.0.0-rc.0", 39 | "next-themes": "^0.3.0", 40 | "react": "19.0.0-rc-f994737d14-20240522", 41 | "react-dom": "19.0.0-rc-f994737d14-20240522", 42 | "react-hook-form": "^7.53.0", 43 | "react-medium-image-zoom": "^5.2.9", 44 | "tailwind-merge": "^2.5.2", 45 | "tailwindcss-animate": "^1.0.7", 46 | "zod": "^3.23.8" 47 | }, 48 | "devDependencies": { 49 | "@tailwindcss/typography": "^0.5.15", 50 | "@tanstack/eslint-plugin-query": "^5.53.0", 51 | "@tanstack/react-query-devtools": "^5.55.4", 52 | "@types/js-cookie": "^3.0.6", 53 | "@types/node": "^20", 54 | "@types/react": "^18", 55 | "@types/react-dom": "^18", 56 | "eslint": "^8", 57 | "eslint-config-next": "15.0.0-rc.0", 58 | "eslint-config-prettier": "^9.1.0", 59 | "postcss": "^8", 60 | "prettier": "^3.3.3", 61 | "prettier-plugin-tailwindcss": "^0.6.6", 62 | "tailwindcss": "^3.4.1", 63 | "typescript": "^5" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier-plugin-tailwindcss"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codinginflow/nextjs-15-wix-store/11f628c1b80eaf71ad010d2d42167e9bb6da10bf/public/placeholder.png -------------------------------------------------------------------------------- /src/app/MainNavigation.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | NavigationMenu, 5 | NavigationMenuContent, 6 | NavigationMenuItem, 7 | NavigationMenuLink, 8 | NavigationMenuList, 9 | NavigationMenuTrigger, 10 | navigationMenuTriggerStyle, 11 | } from "@/components/ui/navigation-menu"; 12 | import { cn } from "@/lib/utils"; 13 | import { collections } from "@wix/stores"; 14 | import Link from "next/link"; 15 | 16 | interface MainNavigationProps { 17 | collections: collections.Collection[]; 18 | className?: string; 19 | } 20 | 21 | export default function MainNavigation({ 22 | collections, 23 | className, 24 | }: MainNavigationProps) { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | Shop 32 | 33 | 34 | 35 | 36 | Collections 37 | 38 |
    39 | {collections.map((collection) => ( 40 |
  • 41 | 46 | 52 | {collection.name} 53 | 54 | 55 |
  • 56 | ))} 57 |
58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/app/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import SearchField from "@/components/SearchField"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Sheet, 7 | SheetContent, 8 | SheetHeader, 9 | SheetTitle, 10 | } from "@/components/ui/sheet"; 11 | import UserButton from "@/components/UserButton"; 12 | import { twConfig } from "@/lib/utils"; 13 | import { members } from "@wix/members"; 14 | import { collections } from "@wix/stores"; 15 | import { MenuIcon } from "lucide-react"; 16 | import Link from "next/link"; 17 | import { usePathname, useSearchParams } from "next/navigation"; 18 | import { useEffect, useState } from "react"; 19 | 20 | interface MobileMenuProps { 21 | collections: collections.Collection[]; 22 | loggedInMember: members.Member | null; 23 | } 24 | 25 | export default function MobileMenu({ 26 | collections, 27 | loggedInMember, 28 | }: MobileMenuProps) { 29 | const pathname = usePathname(); 30 | const searchParams = useSearchParams(); 31 | 32 | const [isOpen, setIsOpen] = useState(false); 33 | 34 | useEffect(() => { 35 | const handleResize = () => { 36 | if (window.innerWidth > parseInt(twConfig.theme.screens.lg)) { 37 | setIsOpen(false); 38 | } 39 | }; 40 | window.addEventListener("resize", handleResize); 41 | return () => window.removeEventListener("resize", handleResize); 42 | }, []); 43 | 44 | useEffect(() => { 45 | setIsOpen(false); 46 | }, [pathname, searchParams]); 47 | 48 | return ( 49 | <> 50 | 58 | 59 | 60 | 61 | Navigation 62 | 63 |
64 | 65 |
    66 |
  • 67 | 68 | Shop 69 | 70 |
  • 71 | {collections.map((collection) => ( 72 |
  • 73 | 77 | {collection.name} 78 | 79 |
  • 80 | ))} 81 |
82 | 83 |
84 |
85 |
86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import logo from "@/assets/logo.png"; 2 | import SearchField from "@/components/SearchField"; 3 | import UserButton from "@/components/UserButton"; 4 | import { getWixServerClient } from "@/lib/wix-client.server"; 5 | import { getCart } from "@/wix-api/cart"; 6 | import { getCollections } from "@/wix-api/collections"; 7 | import { getLoggedInMember } from "@/wix-api/members"; 8 | import Image from "next/image"; 9 | import Link from "next/link"; 10 | import { Suspense } from "react"; 11 | import MainNavigation from "./MainNavigation"; 12 | import MobileMenu from "./MobileMenu"; 13 | import ShoppingCartButton from "./ShoppingCartButton"; 14 | 15 | export default async function Navbar() { 16 | const wixClient = getWixServerClient(); 17 | 18 | const [cart, loggedInMember, collections] = await Promise.all([ 19 | getCart(wixClient), 20 | getLoggedInMember(wixClient), 21 | getCollections(wixClient), 22 | ]); 23 | 24 | return ( 25 |
26 |
27 | 28 | 32 | 33 |
34 | 35 | Flow Shop logo 36 | Flow Shop 37 | 38 | 42 |
43 | 44 |
45 | 49 | 50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/ReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { useState } from "react"; 6 | 7 | export default function ReactQueryProvider({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const [client] = useState(new QueryClient()); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/ShoppingCartButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import CheckoutButton from "@/components/CheckoutButton"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | Sheet, 7 | SheetContent, 8 | SheetHeader, 9 | SheetTitle, 10 | } from "@/components/ui/sheet"; 11 | import WixImage from "@/components/WixImage"; 12 | import { 13 | useCart, 14 | useRemoveCartItem, 15 | useUpdateCartItemQuantity, 16 | } from "@/hooks/cart"; 17 | import { currentCart } from "@wix/ecom"; 18 | import { Loader2, ShoppingCartIcon, X } from "lucide-react"; 19 | import Link from "next/link"; 20 | import { useState } from "react"; 21 | 22 | interface ShoppingCartButtonProps { 23 | initialData: currentCart.Cart | null; 24 | } 25 | 26 | export default function ShoppingCartButton({ 27 | initialData, 28 | }: ShoppingCartButtonProps) { 29 | const [sheetOpen, setSheetOpen] = useState(false); 30 | 31 | const cartQuery = useCart(initialData); 32 | 33 | const totalQuantity = 34 | cartQuery.data?.lineItems?.reduce( 35 | (acc, item) => acc + (item.quantity || 0), 36 | 0, 37 | ) || 0; 38 | 39 | return ( 40 | <> 41 |
42 | 48 |
49 | 50 | 51 | 52 | 53 | Your cart{" "} 54 | 55 | ({totalQuantity} {totalQuantity === 1 ? "item" : "items"}) 56 | 57 | 58 | 59 |
60 |
    61 | {cartQuery.data?.lineItems?.map((item) => ( 62 | setSheetOpen(false)} 66 | /> 67 | ))} 68 |
69 | {cartQuery.isPending && ( 70 | 71 | )} 72 | {cartQuery.error && ( 73 |

{cartQuery.error.message}

74 | )} 75 | {!cartQuery.isPending && !cartQuery.data?.lineItems?.length && ( 76 |
77 |
78 |

Your cart is empty

79 | setSheetOpen(false)} 83 | > 84 | Start shopping now 85 | 86 |
87 |
88 | )} 89 |
90 |
91 |
92 |
93 |

Subtotal amount:

94 |

95 | {/* @ts-expect-error */} 96 | {cartQuery.data?.subtotal?.formattedConvertedAmount} 97 |

98 |

99 | Shipping and taxes calculated at checkout 100 |

101 |
102 | 106 |
107 |
108 |
109 | 110 | ); 111 | } 112 | 113 | interface ShoppingCartItemProps { 114 | item: currentCart.LineItem; 115 | onProductLinkClicked: () => void; 116 | } 117 | 118 | function ShoppingCartItem({ 119 | item, 120 | onProductLinkClicked, 121 | }: ShoppingCartItemProps) { 122 | const updateQuantityMutation = useUpdateCartItemQuantity(); 123 | 124 | const removeItemMutation = useRemoveCartItem(); 125 | 126 | const productId = item._id; 127 | 128 | if (!productId) return null; 129 | 130 | const slug = item.url?.split("/").pop(); 131 | 132 | const quantityLimitReached = 133 | !!item.quantity && 134 | !!item.availability?.quantityAvailable && 135 | item.quantity >= item.availability.quantityAvailable; 136 | 137 | return ( 138 |
  • 139 |
    140 | 141 | 148 | 149 | 155 |
    156 |
    157 | 158 |

    {item.productName?.translated || "Item"}

    159 | 160 | {!!item.descriptionLines?.length && ( 161 |

    162 | {item.descriptionLines 163 | .map( 164 | (line) => 165 | line.colorInfo?.translated || line.plainText?.translated, 166 | ) 167 | .join(", ")} 168 |

    169 | )} 170 |
    171 | {item.quantity} x {item.price?.formattedConvertedAmount} 172 | {item.fullPrice && item.fullPrice.amount !== item.price?.amount && ( 173 | 174 | {item.fullPrice.formattedConvertedAmount} 175 | 176 | )} 177 |
    178 |
    179 | 192 | {item.quantity} 193 | 206 | {quantityLimitReached && Quantity limit reached} 207 |
    208 |
    209 |
  • 210 | ); 211 | } 212 | -------------------------------------------------------------------------------- /src/app/api/auth/callback/wix/route.ts: -------------------------------------------------------------------------------- 1 | import { WIX_OAUTH_DATA_COOKIE, WIX_SESSION_COOKIE } from "@/lib/constants"; 2 | import { getWixServerClient } from "@/lib/wix-client.server"; 3 | import { OauthData } from "@wix/sdk"; 4 | import { cookies } from "next/headers"; 5 | import { NextRequest } from "next/server"; 6 | 7 | export async function GET(req: NextRequest) { 8 | const code = req.nextUrl.searchParams.get("code"); 9 | const state = req.nextUrl.searchParams.get("state"); 10 | const error = req.nextUrl.searchParams.get("error"); 11 | const error_description = req.nextUrl.searchParams.get("error_description"); 12 | 13 | if (error) { 14 | return new Response(error_description, { status: 400 }); 15 | } 16 | 17 | const oAuthData: OauthData = JSON.parse( 18 | cookies().get(WIX_OAUTH_DATA_COOKIE)?.value || "{}", 19 | ); 20 | 21 | if (!code || !state || !oAuthData) { 22 | return new Response("Invalid request", { status: 400 }); 23 | } 24 | 25 | const wixClient = getWixServerClient(); 26 | 27 | const memberTokens = await wixClient.auth.getMemberTokens( 28 | code, 29 | state, 30 | oAuthData, 31 | ); 32 | 33 | cookies().delete(WIX_OAUTH_DATA_COOKIE); 34 | cookies().set(WIX_SESSION_COOKIE, JSON.stringify(memberTokens), { 35 | maxAge: 60 * 60 * 24 * 14, 36 | secure: process.env.NODE_ENV === "production", 37 | }); 38 | 39 | return new Response(null, { 40 | status: 302, 41 | headers: { 42 | Location: oAuthData.originalUri || "/", 43 | }, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/app/api/review-media-upload-url/route.ts: -------------------------------------------------------------------------------- 1 | import { getWixAdminClient } from "@/lib/wix-client.server"; 2 | import { NextRequest } from "next/server"; 3 | 4 | export async function GET(req: NextRequest) { 5 | const fileName = req.nextUrl.searchParams.get("fileName"); 6 | const mimeType = req.nextUrl.searchParams.get("mimeType"); 7 | 8 | if (!fileName || !mimeType) { 9 | return new Response("Missing required query parameters", { 10 | status: 400, 11 | }); 12 | } 13 | 14 | const { uploadUrl } = await getWixAdminClient().files.generateFileUploadUrl( 15 | mimeType, 16 | { 17 | fileName, 18 | filePath: "product-reviews-media", 19 | private: false, 20 | }, 21 | ); 22 | 23 | return Response.json({ uploadUrl }); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/checkout-success/ClearCart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useClearCart } from "@/hooks/cart"; 4 | import { useEffect } from "react"; 5 | 6 | export default function ClearCart() { 7 | const { mutate } = useClearCart(); 8 | 9 | useEffect(mutate, [mutate]); 10 | 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/checkout-success/page.tsx: -------------------------------------------------------------------------------- 1 | import Order from "@/components/Order"; 2 | import { getWixServerClient } from "@/lib/wix-client.server"; 3 | import { getLoggedInMember } from "@/wix-api/members"; 4 | import { getOrder } from "@/wix-api/orders"; 5 | import { Metadata } from "next"; 6 | import Link from "next/link"; 7 | import { notFound } from "next/navigation"; 8 | import ClearCart from "./ClearCart"; 9 | 10 | interface PageProps { 11 | searchParams: { orderId: string }; 12 | } 13 | 14 | export const metadata: Metadata = { 15 | title: "Checkout success", 16 | }; 17 | 18 | export default async function Page({ searchParams: { orderId } }: PageProps) { 19 | const wixClient = getWixServerClient(); 20 | 21 | const [order, loggedInMember] = await Promise.all([ 22 | getOrder(wixClient, orderId), 23 | getLoggedInMember(wixClient), 24 | ]); 25 | 26 | if (!order) { 27 | notFound(); 28 | } 29 | 30 | const orderCreatedDate = order._createdDate 31 | ? new Date(order._createdDate) 32 | : null; 33 | 34 | return ( 35 |
    36 |

    We received your order!

    37 |

    A summary of your order was sent to your email address.

    38 |

    Order details

    39 | 40 | {loggedInMember && ( 41 | 42 | View all your orders 43 | 44 | )} 45 | {orderCreatedDate && 46 | orderCreatedDate.getTime() > Date.now() - 60_000 * 5 && } 47 |
    48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/app/collections/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import WixImage from "@/components/WixImage"; 3 | import { cn } from "@/lib/utils"; 4 | import { getWixServerClient } from "@/lib/wix-client.server"; 5 | import { getCollectionBySlug } from "@/wix-api/collections"; 6 | import { notFound } from "next/navigation"; 7 | import { Suspense } from "react"; 8 | 9 | interface LayoutProps { 10 | children: React.ReactNode; 11 | params: { slug: string }; 12 | } 13 | 14 | export default function Layout({ children, params }: LayoutProps) { 15 | return ( 16 | }> 17 | {children} 18 | 19 | ); 20 | } 21 | 22 | async function CollectionsLayout({ children, params: { slug } }: LayoutProps) { 23 | const collection = await getCollectionBySlug(getWixServerClient(), slug); 24 | 25 | if (!collection) notFound(); 26 | 27 | const banner = collection.media?.mainMedia?.image; 28 | 29 | return ( 30 |
    31 |
    32 | {banner && ( 33 |
    34 | 40 |
    41 |

    42 | {collection.name} 43 |

    44 |
    45 | )} 46 |

    52 | {collection.name} 53 |

    54 |
    55 | {children} 56 |
    57 | ); 58 | } 59 | 60 | function LoadingSkeleton() { 61 | return ( 62 |
    63 | 64 |
    65 |

    Products

    66 |
    67 | {Array.from({ length: 8 }).map((_, i) => ( 68 | 69 | ))} 70 |
    71 |
    72 |
    73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/app/collections/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import PaginationBar from "@/components/PaginationBar"; 2 | import Product from "@/components/Product"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import { getWixServerClient } from "@/lib/wix-client.server"; 5 | import { getCollectionBySlug } from "@/wix-api/collections"; 6 | import { queryProducts } from "@/wix-api/products"; 7 | import { Metadata } from "next"; 8 | import { notFound } from "next/navigation"; 9 | import { Suspense } from "react"; 10 | 11 | interface PageProps { 12 | params: { slug: string }; 13 | searchParams: { page?: string }; 14 | } 15 | 16 | export async function generateMetadata({ 17 | params: { slug }, 18 | }: PageProps): Promise { 19 | const collection = await getCollectionBySlug(getWixServerClient(), slug); 20 | 21 | if (!collection) notFound(); 22 | 23 | const banner = collection.media?.mainMedia?.image; 24 | 25 | return { 26 | title: collection.name, 27 | description: collection.description, 28 | openGraph: { 29 | images: banner ? [{ url: banner.url }] : [], 30 | }, 31 | }; 32 | } 33 | 34 | export default async function Page({ 35 | params: { slug }, 36 | searchParams: { page = "1" }, 37 | }: PageProps) { 38 | const collection = await getCollectionBySlug(getWixServerClient(), slug); 39 | 40 | if (!collection?._id) notFound(); 41 | 42 | return ( 43 |
    44 |

    Products

    45 | } key={page}> 46 | 47 | 48 |
    49 | ); 50 | } 51 | 52 | interface ProductsProps { 53 | collectionId: string; 54 | page: number; 55 | } 56 | 57 | async function Products({ collectionId, page }: ProductsProps) { 58 | const pageSize = 8; 59 | 60 | const collectionProducts = await queryProducts(getWixServerClient(), { 61 | collectionIds: collectionId, 62 | limit: pageSize, 63 | skip: (page - 1) * pageSize, 64 | }); 65 | 66 | if (!collectionProducts.length) notFound(); 67 | 68 | if (page > (collectionProducts.totalPages || 1)) notFound(); 69 | 70 | return ( 71 |
    72 |
    73 | {collectionProducts.items.map((product) => ( 74 | 75 | ))} 76 |
    77 | 81 |
    82 | ); 83 | } 84 | 85 | function LoadingSkeleton() { 86 | return ( 87 |
    88 | {Array.from({ length: 8 }).map((_, i) => ( 89 | 90 | ))} 91 |
    92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codinginflow/nextjs-15-wix-store/11f628c1b80eaf71ad010d2d42167e9bb6da10bf/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import "~react-medium-image-zoom/dist/styles.css"; 6 | 7 | @layer base { 8 | :root { 9 | --background: 0 0% 100%; 10 | --foreground: 20 14.3% 4.1%; 11 | --card: 0 0% 100%; 12 | --card-foreground: 20 14.3% 4.1%; 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | --primary: 24.6 95% 53.1%; 16 | --primary-foreground: 60 9.1% 97.8%; 17 | --secondary: 60 4.8% 95.9%; 18 | --secondary-foreground: 24 9.8% 10%; 19 | --muted: 60 4.8% 95.9%; 20 | --muted-foreground: 25 5.3% 44.7%; 21 | --accent: 60 4.8% 95.9%; 22 | --accent-foreground: 24 9.8% 10%; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 60 9.1% 97.8%; 25 | --border: 20 5.9% 90%; 26 | --input: 20 5.9% 90%; 27 | --ring: 24.6 95% 53.1%; 28 | --radius: 0rem; 29 | --chart-1: 12 76% 61%; 30 | --chart-2: 173 58% 39%; 31 | --chart-3: 197 37% 24%; 32 | --chart-4: 43 74% 66%; 33 | --chart-5: 27 87% 67%; 34 | } 35 | 36 | .dark { 37 | --background: 20 14.3% 4.1%; 38 | --foreground: 60 9.1% 97.8%; 39 | --card: 20 14.3% 4.1%; 40 | --card-foreground: 60 9.1% 97.8%; 41 | --popover: 20 14.3% 4.1%; 42 | --popover-foreground: 60 9.1% 97.8%; 43 | --primary: 20.5 90.2% 48.2%; 44 | --primary-foreground: 60 9.1% 97.8%; 45 | --secondary: 12 6.5% 15.1%; 46 | --secondary-foreground: 60 9.1% 97.8%; 47 | --muted: 12 6.5% 15.1%; 48 | --muted-foreground: 24 5.4% 63.9%; 49 | --accent: 12 6.5% 15.1%; 50 | --accent-foreground: 60 9.1% 97.8%; 51 | --destructive: 0 72.2% 50.6%; 52 | --destructive-foreground: 60 9.1% 97.8%; 53 | --border: 12 6.5% 15.1%; 54 | --input: 12 6.5% 15.1%; 55 | --ring: 20.5 90.2% 48.2%; 56 | --chart-1: 220 70% 50%; 57 | --chart-2: 160 60% 45%; 58 | --chart-3: 30 80% 55%; 59 | --chart-4: 280 65% 60%; 60 | --chart-5: 340 75% 55%; 61 | 62 | [data-rmiz-modal-overlay="visible"] { 63 | background-color: black; 64 | } 65 | } 66 | } 67 | 68 | @layer base { 69 | * { 70 | @apply border-border; 71 | } 72 | body { 73 | @apply bg-background text-foreground; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/toaster"; 2 | import type { Metadata } from "next"; 3 | import { ThemeProvider } from "next-themes"; 4 | import { Lora } from "next/font/google"; 5 | import Footer from "./Footer"; 6 | import "./globals.css"; 7 | import Navbar from "./Navbar"; 8 | import ReactQueryProvider from "./ReactQueryProvider"; 9 | 10 | const lora = Lora({ subsets: ["latin"] }); 11 | 12 | export const metadata: Metadata = { 13 | title: { 14 | template: "%s | Flow Shop", 15 | absolute: "Flow Shop", 16 | }, 17 | description: "A full-stack e-commerce application built with Next.js 15", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 28 | 34 | 35 | 36 |
    {children}
    37 |