From f1ad9477a34c89095a39ae2df47afe93fae9ca69 Mon Sep 17 00:00:00 2001 From: Rezwana Karim <126201034+rezwana-karim@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:59:59 +0600 Subject: [PATCH 1/6] Upgrade ESLint to v10, adjust config & add reports Upgrade ESLint to v10 and adapt the repo to the new flat config: remove incompatible eslint-config-next imports and replace with a minimal flat configuration. Add developer artifacts and reports (.copilot-tracking analysis and DEV_SERVER_VERIFICATION_REPORT.md), update .gitignore to ignore *.py, and refresh lint/type error outputs. Also include miscellaneous updates across package.json, lockfile, and several src files (services, pages, and integration handlers) to align with the linting/config changes and dependency updates. --- .../architecture-and-optimization-analysis.md | 655 ++ .gitignore | 1 + DEV_SERVER_VERIFICATION_REPORT.md | 350 + eslint.config.mjs | 22 +- lint-errors.json | 53 +- package-lock.json | 8431 +++++------------ package.json | 4 +- src/app/(auth)/signup/page.tsx | 3 + src/app/api/webhook/payment/route.ts | 13 +- .../dashboard/integrations/facebook/page.tsx | 32 +- src/app/dashboard/products/[id]/page.tsx | 16 +- src/app/dashboard/products/new/page.tsx | 16 +- .../settings/integrations/facebook/page.tsx | 26 +- src/app/store/[slug]/checkout/page.tsx | 16 + src/components/orders-page-client.tsx | 16 +- src/lib/services/inventory.service.ts | 120 +- src/lib/services/order-processing.service.ts | 2 +- src/lib/services/product.service.ts | 17 +- typescript-errors.json | 53 +- 19 files changed, 3583 insertions(+), 6263 deletions(-) create mode 100644 .copilot-tracking/explore/architecture-and-optimization-analysis.md create mode 100644 DEV_SERVER_VERIFICATION_REPORT.md diff --git a/.copilot-tracking/explore/architecture-and-optimization-analysis.md b/.copilot-tracking/explore/architecture-and-optimization-analysis.md new file mode 100644 index 00000000..5c5ab767 --- /dev/null +++ b/.copilot-tracking/explore/architecture-and-optimization-analysis.md @@ -0,0 +1,655 @@ + + +I'll conduct a comprehensive architectural analysis of the StormCom project. Let me start by enabling structured thinking and then systematically explore the codebase. + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/package.json) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/next.config.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/tsconfig.json) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/prisma/schema.prisma) + +Searched for regex `prisma\.(.*?)\.findMany|prisma\.(.*?)\.findUnique|prisma\.(.*?)\.count`, 50 results + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/middleware.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/auth.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/app/api) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib) + +Searched for files matching `**/*.service.ts`, 18 matches + +Searched for regex `export.*async.*function.*\{` (`**/src/lib/services/*.ts`), 2 results + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cached-queries.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cache-utils.ts) + +Compacted conversation + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cached-queries.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cache-utils.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cached-queries.ts) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/src/lib/cache-utils.ts) + +Searched for files matching `**/*PERFORMANCE*.md`, 3 matches + +Searched for files matching `**/*ARCHITECTURE*.md`, no matches + +Searched for files matching `**/*AUDIT*.md`, 7 matches + +Searched for regex `export\s+const\s+revalidate|export\s+const\s+dynamic|generateStaticParams` (`**/src/app/**/*.tsx`), no results + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/COMPREHENSIVE_PRODUCTION_AUDIT_SUMMARY.md) + +Read [](file:///f%3A/codestorm/codestorm/stormcom-ui/stormcom/PERFORMANCE_ANALYSIS_SUMMARY.md) + +Based on my comprehensive exploration of the **StormCom codebase**, here's the detailed architecture analysis: + +--- + +# πŸ—οΈ **StormCom - Comprehensive Architecture Analysis** + +## **Executive Summary** + +StormCom is a **modern multi-tenant SaaS e-commerce platform** built with cutting-edge technologies. The architecture demonstrates **strong engineering fundamentals** with excellent multi-tenant isolation, comprehensive caching strategies, and performance optimizations, but requires **critical security fixes** and **UI/UX accessibility improvements**. + +**Overall Grade: B+ (Good with Critical Fixes Required)** + +--- + +## **πŸ“Š Technology Stack Assessment** + +### **Frontend** ⭐⭐⭐⭐⭐ (Excellent) +- **Next.js 16.1.6** - Latest with App Router, React Compiler, Turbopack +- **React 19.2.3** - Latest with Server Components, Actions +- **TypeScript 5.9.3** - Strict mode enabled +- **Radix UI** - 20+ accessible component packages +- **TailwindCSS 4.0** - Latest with PostCSS integration +- **Zustand 5.0.9** - State management with Zundo (undo/redo) +- **Monaco Editor** - For advanced code editing + +### **Backend/API** ⭐⭐⭐⭐⭐ (Excellent) +- **Prisma 6.19.0** - PostgreSQL ORM with adapter +- **NextAuth 4.24.13** - Authentication with JWT optimization +- **Node.js 22.x** - Latest LTS on Vercel + +### **Integrations** ⭐⭐⭐⭐ (Very Good) +- **Stripe 20.0.0** - Payment processing +- **Resend** - Email delivery +- **Facebook/Meta APIs** - Commerce, Messenger, Conversions API +- **Pathao** - Courier/shipping integration +- **SSLCommerz** - Payment gateway + +### **Testing** ⚠️ (Needs Improvement) +- **Vitest 4.0.16** - Unit testing configured +- **Playwright 1.57.0** - E2E testing available +- ❌ **Zero integration tests** for multi-tenancy +- ❌ **No visual regression testing** + +--- + +## **🎯 Architecture Overview** + +### **Multi-Tenant Model** ⭐⭐⭐⭐⭐ + +``` +Organization (1) ━━━ Store (1) ━━┳━━ Products (Many) + β”‚ ┃ + β”œβ”€ Membership β”„β”„β”„ User ┃ + β”‚ ┣━━ Orders (Many) + └─ Subscription ┃ + ┣━━ Customers (Many) + ┃ + ┗━━ Staff (Many) +``` + +**Key Patterns:** +- βœ… **Perfect tenant isolation** - Zero data leakage across 171 API routes +- βœ… **Subdomain routing** - `vendor.codestormhub.live` β†’ store +- βœ… **Custom domain support** - Full DNS integration +- βœ… **Path-based access** - Fallback `/store/[slug]` routes + +**Middleware Architecture:** +```typescript +middleware.ts (Edge Runtime) +β”œβ”€ Subdomain detection (extractSubdomain) +β”œβ”€ Custom domain lookup (isCustomDomain) +β”œβ”€ Store cache (EdgeCache, 10-min TTL) +β”œβ”€ API lookup (/api/stores/lookup) +β”œβ”€ Rewrite to /store/[slug] +└─ Security headers injection +``` + +--- + +## **🚨 Critical Issues (Requires Immediate Action)** + +### **1. Security Critical** πŸ”΄ + +#### **SEC-001: Plaintext API Credentials in Database** +- **Risk**: CRITICAL - Data breach = full third-party API access +- **Impact**: GDPR violations, regulatory fines, liability +- **Affected**: + - `FacebookIntegration`: accessToken, refreshToken, appSecret, webhookSecret + - `PathaoIntegration`: clientSecret, password, accessToken +- **Fix Required**: + ```typescript + // Implement Prisma middleware for field-level encryption + prisma.$use(async (params, next) => { + if (params.model === 'FacebookIntegration') { + if (params.action === 'create' || params.action === 'update') { + params.args.data.accessToken = encrypt(params.args.data.accessToken); + } + if (params.action === 'findUnique' || params.action === 'findMany') { + const result = await next(params); + if (result) { + result.accessToken = decrypt(result.accessToken); + } + return result; + } + } + return next(params); + }); + ``` +- **Effort**: 16-24 hours +- **Priority**: **P0 - Block production until fixed** + +#### **SEC-002: Webhook Replay Attack Vulnerability** +- **Risk**: HIGH - Duplicate orders, financial loss +- **Fix**: Implement nonce + timestamp validation +- **Effort**: 4-6 hours + +#### **SEC-003: Missing Admin Audit Logging & Rate Limiting** +- **Risk**: MEDIUM - Super admin abuse without trail +- **Fix**: Add rate limiting (100 req/hr) + enhanced logging +- **Effort**: 6-8 hours + +### **2. UI/UX Critical** πŸ”΄ + +#### **UX-001: Missing Autocomplete Attributes** (WCAG 1.3.5 violation) +- **Impact**: Poor conversion rates, failed accessibility audits +- **Affected**: Checkout forms, auth forms +- **Fix**: + ```tsx + + ``` +- **Effort**: 2-4 hours + +#### **UX-002: Missing inputMode="tel" on Phone Inputs** +- **Impact**: Poor mobile checkout (60%+ of traffic) +- **Fix**: `` +- **Effort**: 1 hour + +**Total Critical Accessibility Issues: 8** +**Current WCAG Score: 62%** β†’ **Target: 95%** + +--- + +## **⚑ Performance Architecture** + +### **Caching Strategy** ⭐⭐⭐⭐⭐ (Excellent) + +```typescript +// Three-tier caching system +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ 1. React cache() - Request deduplication +β”‚ - Zero duplicate queries per request +β”‚ - Automatic within component tree +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 2. unstable_cache() - Persistent cross-request +β”‚ - Dashboard stats: 5 min (dynamic) +β”‚ - Product lists: 5 min (dynamic) +β”‚ - Categories: 1 hour (semi-static) +β”‚ - Store settings: 1 hour (semi-static) +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 3. EdgeCache (Middleware) - In-memory +β”‚ - Store lookups: 10 min TTL +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Cache Profiles:** +```typescript +export const SIMPLE_CACHE_DURATIONS = { + realTime: 30, // Order stats, inventory + dynamic: 300, // Dashboard, products + semiStatic: 3600, // Categories, settings + static: 86400, // Analytics, reports + longTerm: 604800, // Historical data +}; +``` + +**Invalidation System:** +```typescript +// Automatic tag-based invalidation +await invalidateCache('product.update', storeId, productId); +// Invalidates: +// - `product:${storeId}:${productId}` +// - `productList:${storeId}` +// - `store:${storeId}` +``` + +### **Pagination Strategy** ⭐⭐⭐⭐⭐ + +**Cursor-based pagination** (17x faster for deep pages): +```typescript +// Instead of offset-based: +// SELECT * FROM products SKIP 10000 TAKE 20 ❌ (slow) + +// Use cursor-based: +await prisma.product.findMany({ + cursor: { id: lastProductId }, + skip: 1, + take: 21, // +1 for hasMore detection +}); +// βœ… 17x faster at page 500+ +``` + +### **Database Optimization** ⭐⭐⭐⭐⭐ + +**Index Strategy:** +- βœ… **100+ composite indexes** for multi-tenant queries +- βœ… All foreign keys indexed +- βœ… Compound indexes: `[storeId, status, createdAt]` +- βœ… Text search indexes: `[storeId, sku]`, `[storeId, email]` + +**Query Batching:** +```typescript +// Use $transaction for atomic batch queries +const [totalOrders, totalRevenue, customers] = await prisma.$transaction([ + prisma.order.count({ where: { storeId } }), + prisma.order.aggregate({ _sum: { totalAmount: true } }), + prisma.customer.count({ where: { storeId } }), +]); +``` + +--- + +## **πŸ” Identified Performance Bottlenecks** + +### **Critical** 🚨 + +#### **PERF-001: Bundle Size Crisis** +- **Problem**: 5-8MB uncompressed, ZERO dynamic imports +- **Impact**: 5-7s initial load on mobile +- **Affected**: + - `facebook/dashboard.tsx` (1,106 lines) + - `orders-table.tsx` (981 lines) + - `product-edit-form.tsx` (886 lines) +- **Fix**: Lazy loading with `React.lazy()` + Suspense + ```typescript + const FacebookDashboard = lazy(() => import('./facebook/dashboard')); + + }> + + + ``` +- **Expected Gain**: **30-40% bundle reduction**, 1-2s faster TTI +- **Effort**: 2-3 hours + +#### **PERF-002: Database Over-Fetching** +- **Problem**: 40-60% excess data in list views +- **Fix**: Separate selects for list vs detail views + ```typescript + // List view (before) + select: { /* ALL FIELDS */ } // ❌ 60% waste + + // List view (after) + select: { id, name, price, thumbnail } // βœ… Only needed + ``` +- **Expected Gain**: **40-60% smaller payloads**, 20-30% faster queries +- **Effort**: 4-6 hours + +#### **PERF-003: Missing API Caching** +- **Problem**: Only 12/35 routes cached +- **Many using**: `export const dynamic = 'force-dynamic'` +- **Fix**: Add `unstable_cache` to hot routes +- **Expected Gain**: **80-90% cache hit rate**, sub-100ms responses +- **Effort**: 2-3 hours per route + +#### **PERF-004: Unnecessary Re-renders** +- **Problem**: 200+ inline functions, limited memoization +- **Fix**: Add `useMemo`, `useCallback`, `React.memo` + ```typescript + // Before + + )} + - {/* Top Discount Banners */} - - - {/* Dynamic Hero Section */} - - - {/* Trust Badges */} - - - {/* Categories Section - Enhanced */} - {config.categories.enabled && categories.length > 0 && ( -
- {/* Section Header */} -
-
-

- {config.categories.title} -

- {config.categories.subtitle && ( -

- {config.categories.subtitle} -

- )} -
- {config.categories.showViewAll && ( - - )} -
- - {/* Categories Grid */} -
- {categories.map((category) => ( - - {/* Image Container */} -
- {category.image ? ( - {category.name} - ) : ( -
- πŸ“¦ -
- )} - - {/* Gradient Overlay */} -
- - {/* Hover Shine Effect */} -
-
- - {/* Category Info */} -
-

- {category.name} -

-

- {category._count.products} {category._count.products === 1 ? 'Item' : 'Items'} -

+
+ {categories.map((category) => ( + +
+ {category.image ? ( + {category.name} + ) : ( +
+ πŸ“¦
- - {/* Product Count Badge */} - {category._count.products > 0 && ( -
- {category._count.products} -
- )} - - ))} -
-
- )} - - {/* Featured Products Section */} - {config.featuredProducts.enabled && ( -
-
-
-

{config.featuredProducts.title}

- {config.featuredProducts.subtitle && ( -

- {config.featuredProducts.subtitle} -

)} +
+
- {config.featuredProducts.showViewAll && ( - - )} -
- {featuredProducts.length === 0 ? ( -
-
πŸ›οΈ
-

No Featured Products Yet

-

- Check back soon for amazing deals! +

+

+ {category.name} +

+

+ {category._count.products}{" "} + {category._count.products === 1 ? "Item" : "Items"}

-
- ) : ( - + + {category._count.products > 0 && ( +
+ {category._count.products} +
+ )} + + ))} +
+
+ ); +} + +function FeaturedProductsSection({ + config, + products, + storeSlug, + prefix, +}: { + config: StorefrontConfig["featuredProducts"]; + products: HomeData["featuredProducts"]; + storeSlug: string; + prefix: string; +}) { + if (!config.enabled) return null; + + return ( +
+
+
+

{config.title}

+ {config.subtitle && ( +

{config.subtitle}

)} -
+ + {config.showViewAll && ( + + )} + + + {products.length === 0 ? ( +
+
πŸ›οΈ
+

No Featured Products Yet

+

+ Check back soon for amazing deals! +

+ +
+ ) : ( + )} + + ); +} + +// ── Section order renderer ───────────────────────────────────────────── + +function renderSection( + sectionId: SectionId, + config: StorefrontConfig, + data: HomeData, + store: { name: string; slug: string }, + prefix: string, +): React.ReactNode { + switch (sectionId) { + case "hero": + return ( + + ); + + case "trustBadges": + return ; - {/* New Arrivals Section */} - {newArrivals.length > 0 && ( -
+ case "categories": + return ( + + ); + + case "featuredProducts": + return ( + + ); + + case "newArrivals": + if (!config.newArrivals?.enabled || data.newArrivals.length === 0) return null; + return ( +
- +
-

- Fresh additions to our collection -

+ {config.newArrivals.subtitle && ( +

{config.newArrivals.subtitle}

+ )}
- + {config.newArrivals.showViewAll && ( + + )}
- )} + ); - {/* Best Sellers Section */} - {bestSellers.length > 0 && ( -
+ case "bestSellers": + if (!config.bestSellers?.enabled || data.bestSellers.length === 0) return null; + return ( +
- +
-

- Our most popular products -

+ {config.bestSellers.subtitle && ( +

{config.bestSellers.subtitle}

+ )}
- + {config.bestSellers.showViewAll && ( + + )}
- )} + ); - {/* Testimonials Section */} - {config.testimonials && ( - - )} + case "testimonials": + return config.testimonials ? ( + + ) : null; - {/* Brands/Partners Section */} - {config.brands && ( - - )} + case "brands": + return config.brands ? ( + + ) : null; - {/* Newsletter Section */} - {config.newsletter && ( - - )} + case "newsletter": + return config.newsletter ? ( + + ) : null; + + case "discountBanners": + return ( + + ); + + case "content": + return config.content ? ( + + ) : null; + + default: + return null; + } +} + +// ── Page component ───────────────────────────────────────────────────── + +interface StoreHomePageProps { + params: Promise<{ slug: string }>; +} - {/* Bottom Discount Banners */} - +/** + * Storefront homepage β€” renders sections in the order + * defined by config.sectionOrder for true WYSIWYG parity + * with the visual editor. + */ +export default async function StoreHomePage({ params }: StoreHomePageProps) { + const { slug } = await params; + + const store = await resolveStorefrontStore(slug, { + id: true, + name: true, + slug: true, + description: true, + storefrontConfig: true, + }); + + const config = parseStorefrontConfig(store.storefrontConfig, store.name); + const prefix = await getStoreUrlPrefix(store.slug); + const data = await fetchHomeData(store.id, config); + + return ( +
+ {/* Top discount banner β€” always rendered first, outside sectionOrder */} + + + {/* Ordered sections from config.sectionOrder */} + {config.sectionOrder.map((sectionId) => + renderSection( + sectionId, + config, + data, + { name: store.name, slug: store.slug }, + prefix, + ), + )}
); } diff --git a/src/app/store/[slug]/products/[productSlug]/page.tsx b/src/app/store/[slug]/products/[productSlug]/page.tsx index 19498fb3..adbb53ad 100644 --- a/src/app/store/[slug]/products/[productSlug]/page.tsx +++ b/src/app/store/[slug]/products/[productSlug]/page.tsx @@ -1,4 +1,3 @@ -import { headers } from "next/headers"; import { prisma } from "@/lib/prisma"; import { notFound } from "next/navigation"; import type { Metadata } from "next"; @@ -9,6 +8,7 @@ import { ProductGrid } from "../../components/product-grid"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Package, TruckIcon, RefreshCw, ShieldCheck } from "lucide-react"; +import { resolveStorefrontStore } from "@/lib/storefront/server/resolve-storefront-store"; import { getStoreUrlPrefix, buildStoreUrl } from "@/lib/store-url"; interface StoreProductPageProps { @@ -20,15 +20,11 @@ export async function generateMetadata({ }: StoreProductPageProps): Promise { const { slug, productSlug } = await params; - const store = await prisma.store.findFirst({ - where: { slug, deletedAt: null }, - select: { id: true, name: true }, + const store = await resolveStorefrontStore(slug, { + id: true, + name: true, }); - if (!store) { - return { title: "Product Not Found" }; - } - const product = await prisma.product.findFirst({ where: { storeId: store.id, @@ -62,22 +58,12 @@ export async function generateMetadata({ export default async function StoreProductPage({ params }: StoreProductPageProps) { const { slug, productSlug } = await params; - const headersList = await headers(); - const storeId = headersList.get("x-store-id"); - - const store = await prisma.store.findFirst({ - where: storeId ? { id: storeId, deletedAt: null } : { slug, deletedAt: null }, - select: { - id: true, - name: true, - slug: true, - }, + const store = await resolveStorefrontStore(slug, { + id: true, + name: true, + slug: true, }); - if (!store) { - notFound(); - } - const product = await prisma.product.findFirst({ where: { storeId: store.id, diff --git a/src/app/store/[slug]/products/page.tsx b/src/app/store/[slug]/products/page.tsx index 83dbf698..dfbb6b48 100644 --- a/src/app/store/[slug]/products/page.tsx +++ b/src/app/store/[slug]/products/page.tsx @@ -1,6 +1,4 @@ -import { headers } from "next/headers"; import { prisma } from "@/lib/prisma"; -import { notFound } from "next/navigation"; import type { Metadata } from "next"; import type { Prisma } from "@prisma/client"; import { ProductFilters } from "../components/product-filters"; @@ -9,6 +7,7 @@ import { EmptyState } from "../components/empty-state"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import Link from "next/link"; +import { resolveStorefrontStore } from "@/lib/storefront/server/resolve-storefront-store"; import { getStoreUrlPrefix, buildStoreUrl } from "@/lib/store-url"; import { ChevronLeft, ChevronRight } from "lucide-react"; @@ -80,26 +79,12 @@ export default async function StoreProductsPage({ const { slug } = await params; const search = await searchParams; - // Get store ID from headers (set by middleware) or lookup by slug - const headersList = await headers(); - const storeId = headersList.get("x-store-id"); - - // Get store - const store = await prisma.store.findFirst({ - where: storeId - ? { id: storeId, deletedAt: null } - : { slug, deletedAt: null }, - select: { - id: true, - name: true, - slug: true, - }, + const store = await resolveStorefrontStore(slug, { + id: true, + name: true, + slug: true, }); - if (!store) { - notFound(); - } - // Parse pagination and filters from search params with validation const pageNum = typeof search.page === "string" ? parseInt(search.page) : 1; const page = !isNaN(pageNum) && pageNum > 0 ? pageNum : 1; diff --git a/src/app/track/page.tsx b/src/app/track/page.tsx index 555eeac4..e04b1c16 100644 --- a/src/app/track/page.tsx +++ b/src/app/track/page.tsx @@ -33,6 +33,7 @@ export default function TrackOrderPage() { const response = await fetch('/api/orders/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ query: input }), }); diff --git a/src/components/admin/activity-filters.tsx b/src/components/admin/activity-filters.tsx index 85ed2438..c23d888b 100644 --- a/src/components/admin/activity-filters.tsx +++ b/src/components/admin/activity-filters.tsx @@ -90,7 +90,9 @@ export function ActivityFilters({ actorOptions = [], storeOptions = [] }: Activi if (dateTo) params.set("to", dateTo.toISOString()); params.set("format", "csv"); - const response = await fetch(`/api/admin/activity/export?${params.toString()}`); + const response = await fetch(`/api/admin/activity/export?${params.toString()}`, { + credentials: 'include', + }); if (!response.ok) { throw new Error("Failed to export activity"); diff --git a/src/components/admin/admin-dashboard.tsx b/src/components/admin/admin-dashboard.tsx index 5d37552b..6a750683 100644 --- a/src/components/admin/admin-dashboard.tsx +++ b/src/components/admin/admin-dashboard.tsx @@ -33,7 +33,9 @@ export function AdminDashboard() { const fetchStats = async () => { setLoading(true); try { - const response = await fetch('/api/admin/stats?period=30d'); + const response = await fetch('/api/admin/stats?period=30d', { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to fetch stats'); const data = await response.json(); diff --git a/src/components/admin/create-store-form.tsx b/src/components/admin/create-store-form.tsx index fd3ce51c..757527f7 100644 --- a/src/components/admin/create-store-form.tsx +++ b/src/components/admin/create-store-form.tsx @@ -120,6 +120,7 @@ export function CreateStoreForm({ approvedUsers, selectedUser }: CreateStoreForm const response = await fetch('/api/admin/stores', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(values), }); diff --git a/src/components/admin/notification-bell.tsx b/src/components/admin/notification-bell.tsx index c3b6b44e..e1ef1ac7 100644 --- a/src/components/admin/notification-bell.tsx +++ b/src/components/admin/notification-bell.tsx @@ -62,7 +62,9 @@ export function NotificationBell() { const fetchNotifications = async () => { try { - const response = await fetch("/api/notifications?limit=5"); + const response = await fetch("/api/notifications?limit=5", { + credentials: 'include', + }); if (response.ok) { const data = await response.json(); setNotifications(data.data || []); @@ -87,6 +89,7 @@ export function NotificationBell() { await fetch(`/api/notifications/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, + credentials: 'include', body: JSON.stringify({ read: true }), }); setNotifications((prev) => @@ -112,6 +115,7 @@ export function NotificationBell() { try { await fetch("/api/notifications/mark-all-read", { method: "POST", + credentials: 'include', }); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); setUnreadCount(0); diff --git a/src/components/admin/notifications-list.tsx b/src/components/admin/notifications-list.tsx index 93f8c32b..9dbf977c 100644 --- a/src/components/admin/notifications-list.tsx +++ b/src/components/admin/notifications-list.tsx @@ -64,6 +64,7 @@ export function NotificationsList({ notifications: initialNotifications }: Notif const response = await fetch(`/api/notifications/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ read: true }), }); @@ -84,6 +85,7 @@ export function NotificationsList({ notifications: initialNotifications }: Notif try { const response = await fetch('/api/notifications/mark-all-read', { method: 'POST', + credentials: 'include', }); if (!response.ok) throw new Error('Failed to mark all as read'); diff --git a/src/components/admin/pending-users-list.tsx b/src/components/admin/pending-users-list.tsx index ed6da178..d283adf7 100644 --- a/src/components/admin/pending-users-list.tsx +++ b/src/components/admin/pending-users-list.tsx @@ -77,6 +77,7 @@ export function PendingUsersList({ users: initialUsers }: PendingUsersListProps) try { const response = await fetch(`/api/admin/users/${user.id}/approve`, { method: 'POST', + credentials: 'include', }); if (!response.ok) { @@ -113,6 +114,7 @@ export function PendingUsersList({ users: initialUsers }: PendingUsersListProps) const response = await fetch(`/api/admin/users/${selectedUser.id}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ reason: rejectReason }), }); diff --git a/src/components/admin/store-request-actions.tsx b/src/components/admin/store-request-actions.tsx index c751a554..7bf70e59 100644 --- a/src/components/admin/store-request-actions.tsx +++ b/src/components/admin/store-request-actions.tsx @@ -42,6 +42,7 @@ export function StoreRequestActions({ requestId, storeName }: StoreRequestAction const response = await fetch(`/api/admin/store-requests/${requestId}/approve`, { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: 'include', body: JSON.stringify({ subscriptionPlan }), }); @@ -76,6 +77,7 @@ export function StoreRequestActions({ requestId, storeName }: StoreRequestAction const response = await fetch(`/api/admin/store-requests/${requestId}/reject`, { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: 'include', body: JSON.stringify({ reason: rejectionReason }), }); diff --git a/src/components/admin/users-data-table.tsx b/src/components/admin/users-data-table.tsx index 36475fd6..f80ea17d 100644 --- a/src/components/admin/users-data-table.tsx +++ b/src/components/admin/users-data-table.tsx @@ -255,7 +255,10 @@ export function UsersDataTable({ users: initialUsers }: UsersDataTableProps) { const handleApprove = async (userId: string) => { try { - const response = await fetch(`/api/admin/users/${userId}/approve`, { method: 'POST' }); + const response = await fetch(`/api/admin/users/${userId}/approve`, { + method: 'POST', + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to approve'); toast.success('User approved'); router.refresh(); @@ -266,7 +269,10 @@ export function UsersDataTable({ users: initialUsers }: UsersDataTableProps) { const handleUnsuspend = async (userId: string) => { try { - const response = await fetch(`/api/admin/users/${userId}/suspend`, { method: 'DELETE' }); + const response = await fetch(`/api/admin/users/${userId}/suspend`, { + method: 'DELETE', + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to reactivate'); toast.success('User reactivated'); router.refresh(); diff --git a/src/components/analytics-dashboard.tsx b/src/components/analytics-dashboard.tsx index 0d7f3860..3e472400 100644 --- a/src/components/analytics-dashboard.tsx +++ b/src/components/analytics-dashboard.tsx @@ -71,7 +71,9 @@ export function AnalyticsDashboard({ storeId, dateRange }: AnalyticsDashboardPro if (dateRange?.from) params.set('from', dateRange.from); if (dateRange?.to) params.set('to', dateRange.to); - const response = await fetch(`/api/analytics/dashboard?${params.toString()}`); + const response = await fetch(`/api/analytics/dashboard?${params.toString()}`, { + credentials: 'include', + }); if (!response.ok) { throw new Error('Failed to fetch analytics data'); diff --git a/src/components/analytics/analytics-dashboard.tsx b/src/components/analytics/analytics-dashboard.tsx index 230f6167..0dd5c1dd 100644 --- a/src/components/analytics/analytics-dashboard.tsx +++ b/src/components/analytics/analytics-dashboard.tsx @@ -121,6 +121,7 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP const response = await fetch(`/api/analytics/dashboard?${params}`, { signal: abortController.signal, cache: 'no-store', + credentials: 'include', }); if (!response.ok) throw new Error('Failed to fetch analytics'); @@ -190,14 +191,14 @@ export function AnalyticsDashboard({ storeId: propStoreId }: AnalyticsDashboardP {/* Time Range Selector */} setTimeRange(value as TimeRange)}> - - Last 7 days - Last 30 days - Last 90 days - Last year + + Last 7 days + Last 30 days + Last 90 days + Last year - + {/* Metric Cards - use key to force re-render on time range change */}
diff --git a/src/components/analytics/customer-metrics.tsx b/src/components/analytics/customer-metrics.tsx index 89a2a932..8db751bf 100644 --- a/src/components/analytics/customer-metrics.tsx +++ b/src/components/analytics/customer-metrics.tsx @@ -57,7 +57,9 @@ export function CustomerMetrics({ storeId, timeRange }: CustomerMetricsProps) { setLoading(true); try { const dateRange = getDateRange(timeRange); - const response = await fetch(`/api/analytics/customers?storeId=${storeId}&startDate=${dateRange.startDate}&endDate=${dateRange.endDate}`); + const response = await fetch(`/api/analytics/customers?storeId=${storeId}&startDate=${dateRange.startDate}&endDate=${dateRange.endDate}`, { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to fetch customer metrics'); const result = await response.json(); diff --git a/src/components/analytics/revenue-chart.tsx b/src/components/analytics/revenue-chart.tsx index aa2b7ac8..0a50cede 100644 --- a/src/components/analytics/revenue-chart.tsx +++ b/src/components/analytics/revenue-chart.tsx @@ -63,7 +63,9 @@ export function RevenueChart({ storeId, timeRange }: RevenueChartProps) { setLoading(true); try { const dateRange = getDateRange(timeRange); - const response = await fetch(`/api/analytics/revenue?storeId=${storeId}&from=${dateRange.from}&to=${dateRange.to}`); + const response = await fetch(`/api/analytics/revenue?storeId=${storeId}&from=${dateRange.from}&to=${dateRange.to}`, { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to fetch revenue data'); const result = await response.json(); @@ -100,39 +102,60 @@ export function RevenueChart({ storeId, timeRange }: RevenueChartProps) { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }; + // Get computed colors for SVG rendering + const getPrimaryColor = () => { + if (typeof window === 'undefined') return '#3b82f6'; + // Check if dark mode + const isDark = document.documentElement.classList.contains('dark'); + // Return appropriate color based on theme + return isDark ? '#fbbf24' : '#3b82f6'; // amber in dark, blue in light + }; + + const getChartColor2 = () => { + if (typeof window === 'undefined') return '#06b6d4'; + const isDark = document.documentElement.classList.contains('dark'); + return isDark ? '#34d399' : '#06b6d4'; // emerald in dark, cyan in light + }; + + const primaryColor = getPrimaryColor(); + const chart2Color = getChartColor2(); + return ( - + { - if (name === 'revenue') return [formatCurrency(value), 'Revenue']; + if (name === 'Revenue') return [formatCurrency(value as number), 'Revenue']; return [value, 'Orders']; }} labelFormatter={formatDate} + contentStyle={{ backgroundColor: '#fff', border: '1px solid #e5e7eb', borderRadius: '0.5rem' }} /> diff --git a/src/components/analytics/top-products-table.tsx b/src/components/analytics/top-products-table.tsx index 4ed752e7..1eec23fc 100644 --- a/src/components/analytics/top-products-table.tsx +++ b/src/components/analytics/top-products-table.tsx @@ -40,7 +40,9 @@ export function TopProductsTable({ storeId, timeRange }: TopProductsTableProps) setLoading(true); try { - const response = await fetch(`/api/analytics/products/top?storeId=${storeId}&range=${timeRange}&limit=5`); + const response = await fetch(`/api/analytics/products/top?storeId=${storeId}&range=${timeRange}&limit=5`, { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to fetch top products'); const result = await response.json(); diff --git a/src/components/attribute-edit-client.tsx b/src/components/attribute-edit-client.tsx index c6820bba..78f4a85a 100644 --- a/src/components/attribute-edit-client.tsx +++ b/src/components/attribute-edit-client.tsx @@ -22,7 +22,10 @@ export function AttributeEditClient({ id }: AttributeEditClientProps) { (async () => { try { setLoading(true); - const res = await fetch(`/api/attributes/${id}`, { cache: 'no-store' }); + const res = await fetch(`/api/attributes/${id}`, { + cache: 'no-store', + credentials: 'include', + }); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j?.error || `Failed to load attribute (${res.status})`); diff --git a/src/components/attribute-form.tsx b/src/components/attribute-form.tsx index 982e5f4b..d674ba3e 100644 --- a/src/components/attribute-form.tsx +++ b/src/components/attribute-form.tsx @@ -81,6 +81,7 @@ export function AttributeForm({ attributeId, initialData, storeId }: AttributeFo headers: { 'Content-Type': 'application/json', }, + credentials: 'include', body: JSON.stringify({ name: name.trim(), values: trimmedValues, diff --git a/src/components/attributes-table.tsx b/src/components/attributes-table.tsx index 39b2ef50..0667b790 100644 --- a/src/components/attributes-table.tsx +++ b/src/components/attributes-table.tsx @@ -65,7 +65,9 @@ export function AttributesTable({ storeId }: AttributesTableProps) { perPage: '50', }); - const response = await fetch(`/api/attributes?${params}`); + const response = await fetch(`/api/attributes?${params}`, { + credentials: 'include', + }); if (!response.ok) { throw new Error('Failed to fetch attributes'); } @@ -91,6 +93,7 @@ export function AttributesTable({ storeId }: AttributesTableProps) { setDeleting(true); const response = await fetch(`/api/attributes/${deleteId}`, { method: 'DELETE', + credentials: 'include', }); if (!response.ok) { diff --git a/src/components/brand-form-client.tsx b/src/components/brand-form-client.tsx index 8e1a3876..30ec2ad7 100644 --- a/src/components/brand-form-client.tsx +++ b/src/components/brand-form-client.tsx @@ -66,6 +66,7 @@ export function BrandFormClient({ brand }: BrandFormClientProps) { const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify({ ...formData, logoUrl: formData.logoUrl || null, diff --git a/src/components/brands-page-client.tsx b/src/components/brands-page-client.tsx index 119e7477..4512bd9c 100644 --- a/src/components/brands-page-client.tsx +++ b/src/components/brands-page-client.tsx @@ -40,7 +40,9 @@ export function BrandsPageClient({ storeId }: { storeId: string }) { const fetchBrands = async () => { try { setLoading(true); - const response = await fetch('/api/brands'); + const response = await fetch('/api/brands', { + credentials: 'include', + }); if (response.ok) { const data = await response.json(); setBrands(data.brands || []); @@ -61,6 +63,7 @@ export function BrandsPageClient({ storeId }: { storeId: string }) { try { const response = await fetch(`/api/brands/${slug}`, { method: 'DELETE', + credentials: 'include', }); if (response.ok) { diff --git a/src/components/cart/cart-list.tsx b/src/components/cart/cart-list.tsx index cd66f8cc..91d79e6c 100644 --- a/src/components/cart/cart-list.tsx +++ b/src/components/cart/cart-list.tsx @@ -72,7 +72,9 @@ export function CartList() { const fetchCart = async () => { setLoading(true); try { - const response = await fetch('/api/cart'); + const response = await fetch('/api/cart', { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to fetch cart'); const data = await response.json(); diff --git a/src/components/categories-page-client.tsx b/src/components/categories-page-client.tsx index 555e578d..98f57e65 100644 --- a/src/components/categories-page-client.tsx +++ b/src/components/categories-page-client.tsx @@ -43,7 +43,9 @@ export function CategoriesPageClient() { const fetchCategories = async () => { try { setLoading(true); - const response = await fetch('/api/categories'); + const response = await fetch('/api/categories', { + credentials: 'include', + }); if (response.ok) { const data = await response.json(); setCategories(data.categories || []); diff --git a/src/components/customers/customers-list.tsx b/src/components/customers/customers-list.tsx index ae4de694..647775b0 100644 --- a/src/components/customers/customers-list.tsx +++ b/src/components/customers/customers-list.tsx @@ -131,7 +131,9 @@ export function CustomersList({ storeId }: CustomersListProps) { } await executeExport(async () => { - const response = await fetch(`/api/customers/export?storeId=${storeId}`); + const response = await fetch(`/api/customers/export?storeId=${storeId}`, { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to export customers'); const blob = await response.blob(); diff --git a/src/components/dashboard/storefront/editor/add-section-modal.tsx b/src/components/dashboard/storefront/editor/add-section-modal.tsx index 2f8b3d2a..5194bb0f 100644 --- a/src/components/dashboard/storefront/editor/add-section-modal.tsx +++ b/src/components/dashboard/storefront/editor/add-section-modal.tsx @@ -30,6 +30,12 @@ import { TrendingUp, Image as ImageIcon, Plus, + Clock, + Flame, + MessageSquare, + BarChart3, + Gift, + Truck, } from 'lucide-react'; import type { SectionId } from '@/lib/storefront/types'; @@ -97,6 +103,55 @@ const AVAILABLE_SECTIONS: Record< icon: LayoutGrid, keywords: ['content', 'custom', 'blocks', 'flexible', 'rich', 'builder'], }, + newArrivals: { + label: 'New Arrivals', + description: 'Showcase recently added products with fresh-in badges', + icon: Clock, + keywords: ['new', 'arrivals', 'recent', 'latest', 'fresh'], + }, + bestSellers: { + label: 'Best Sellers', + description: 'Display top-selling products ranked by order volume', + icon: Flame, + keywords: ['best', 'sellers', 'popular', 'trending', 'top'], + }, + // ── New block-based sections ────────────────────────────────────── + features: { + label: 'Features', + description: 'Highlight store or product selling points with icons', + icon: Sparkles, + keywords: ['features', 'benefits', 'selling points', 'icons', 'highlights'], + }, + reviews: { + label: 'Customer Reviews', + description: 'Product or store reviews with star ratings and breakdowns', + icon: MessageSquare, + keywords: ['reviews', 'ratings', 'feedback', 'stars', 'testimonials', 'social proof'], + }, + stats: { + label: 'Stats', + description: 'Showcase key business or product metrics with big numbers', + icon: BarChart3, + keywords: ['stats', 'metrics', 'numbers', 'milestones', 'data'], + }, + ctaSection: { + label: 'Call to Action', + description: 'Compelling CTA banner to drive purchases or sign-ups', + icon: Megaphone, + keywords: ['cta', 'call to action', 'banner', 'conversion', 'button'], + }, + promoSection: { + label: 'Promo Section', + description: 'Promotional full-width section with headline and CTAs', + icon: Gift, + keywords: ['promo', 'promotion', 'discount', 'sale', 'offer', 'deal'], + }, + incentives: { + label: 'Incentives', + description: 'Free shipping, easy returns, and secure payment trust strip', + icon: Truck, + keywords: ['incentives', 'shipping', 'returns', 'secure', 'trust', 'benefits'], + }, }; interface AddSectionModalProps { diff --git a/src/components/dashboard/storefront/editor/editor-sidebar.tsx b/src/components/dashboard/storefront/editor/editor-sidebar.tsx index 6f9be769..9ac65975 100644 --- a/src/components/dashboard/storefront/editor/editor-sidebar.tsx +++ b/src/components/dashboard/storefront/editor/editor-sidebar.tsx @@ -83,7 +83,7 @@ function isSectionEnabled( config: StorefrontConfig, sectionId: SectionId, ): boolean { - const section = config[sectionId]; + const section = (config as unknown as Record)[sectionId]; if (Array.isArray(section)) { return section.length > 0; // discountBanners β€” has items } @@ -165,7 +165,7 @@ export function EditorSidebar() { // ─── Toggle section enable/disable ────────────────────────────────── const toggleSection = (sectionId: SectionId, enabled: boolean) => { - const section = config[sectionId]; + const section = (config as unknown as Record)[sectionId]; if (Array.isArray(section)) { // Can't toggle discountBanners array β€” skip return; @@ -183,7 +183,7 @@ export function EditorSidebar() { reorderSections(newOrder); // Enable the section by default when adding - const section = config[sectionId]; + const section = (config as unknown as Record)[sectionId]; if (section && typeof section === 'object' && 'enabled' in section) { updateConfig({ [sectionId]: { ...section, enabled: true }, diff --git a/src/components/dashboard/storefront/editor/media-picker.tsx b/src/components/dashboard/storefront/editor/media-picker.tsx index 41040a83..59f95b6f 100644 --- a/src/components/dashboard/storefront/editor/media-picker.tsx +++ b/src/components/dashboard/storefront/editor/media-picker.tsx @@ -93,7 +93,9 @@ export function MediaPicker({ setLibraryLoading(true); setLibraryError(''); try { - const response = await fetch(`/api/media/list?storeId=${encodeURIComponent(storeId)}`); + const response = await fetch(`/api/media/list?storeId=${encodeURIComponent(storeId)}`, { + credentials: 'include', + }); if (!response.ok) throw new Error('Failed to load library'); const data = await response.json(); setLibraryImages(data.files || []); diff --git a/src/components/dashboard/storefront/editor/section-settings-panel.tsx b/src/components/dashboard/storefront/editor/section-settings-panel.tsx index ab891736..dc2fedd2 100644 --- a/src/components/dashboard/storefront/editor/section-settings-panel.tsx +++ b/src/components/dashboard/storefront/editor/section-settings-panel.tsx @@ -42,7 +42,7 @@ function getSectionEnabled( config: StorefrontConfig, sectionId: SectionId, ): boolean | undefined { - const section = config[sectionId]; + const section = (config as unknown as Record)[sectionId]; if (Array.isArray(section)) return undefined; // arrays like discountBanners if (section && typeof section === 'object' && 'enabled' in section) { return section.enabled as boolean; @@ -64,7 +64,7 @@ export function SectionSettingsPanel({ sectionId }: SectionSettingsPanelProps) { const enabled = getSectionEnabled(config, sectionId); const handleToggle = (value: boolean) => { - const section = config[sectionId]; + const section = (config as unknown as Record)[sectionId]; if (Array.isArray(section)) return; if (section && typeof section === 'object') { updateConfig({ diff --git a/src/components/dashboard/storefront/editor/theme-marketplace-panel.tsx b/src/components/dashboard/storefront/editor/theme-marketplace-panel.tsx index bd6104d3..11c5d9f2 100644 --- a/src/components/dashboard/storefront/editor/theme-marketplace-panel.tsx +++ b/src/components/dashboard/storefront/editor/theme-marketplace-panel.tsx @@ -2,19 +2,13 @@ /** * Theme Marketplace Panel - * - * Browse, preview, and install theme templates. - * Features: - * - Theme gallery with preview cards - * - Search and filtering - * - One-click installation - * - Theme metadata (author, version, description) + * + * Browse, preview, and install theme templates with industry filtering. */ import { useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; - import { Badge } from '@/components/ui/badge'; import { Card, @@ -34,12 +28,13 @@ import { import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; import { getAllThemeTemplates } from '@/lib/storefront/theme-templates'; -import type { ThemeSettings, ThemeTemplateId } from '@/lib/storefront/types'; -import { - Search, - Download, - Star, - Check, +import { getIndustryLabel, getTemplateStats } from '@/lib/storefront/theme-catalog'; +import type { ThemeSettings, ThemeTemplateId, IndustryCategory } from '@/lib/storefront/types'; +import { + Search, + Download, + Star, + Check, Package, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -50,141 +45,140 @@ interface ThemeMarketplacePanelProps { onApplyTheme: (theme: ThemeSettings) => void; } -type ThemeCategory = 'all' | 'popular' | 'modern' | 'classic' | 'minimal'; - -// Deterministic mock marketplace metadata keyed by template ID -const MOCK_STATS: Record = { - modern: { downloads: 850, rating: 4.8 }, - classic: { downloads: 720, rating: 4.6 }, - bold: { downloads: 540, rating: 4.7 }, - elegant: { downloads: 980, rating: 4.9 }, - minimal: { downloads: 630, rating: 4.5 }, - boutique: { downloads: 410, rating: 4.7 }, -}; +type FilterCategory = 'all' | IndustryCategory; -export function ThemeMarketplacePanel({ currentTheme, onApplyTheme }: ThemeMarketplacePanelProps) { +export function ThemeMarketplacePanel({ + currentTheme, + onApplyTheme, +}: ThemeMarketplacePanelProps) { const [searchQuery, setSearchQuery] = useState(''); - const [selectedCategory, setSelectedCategory] = useState('all'); - const [sortBy, setSortBy] = useState<'name' | 'popular' | 'recent'>('popular'); - - const templates = getAllThemeTemplates(); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [sortBy, setSortBy] = useState<'popular' | 'rating' | 'name'>('popular'); - const enhancedThemes = useMemo(() => { - return templates.map((template) => { - const stats = MOCK_STATS[template.id] ?? { downloads: 100, rating: 4.5 }; - return { - ...template, - author: 'StormCom', - version: '1.0.0', - downloads: stats.downloads, - rating: stats.rating, - category: getCategoryForTheme(template.id), - tags: getTagsForTheme(template.id), - previewImage: `/theme-previews/${template.id}.png`, - }; - }); + const templates = useMemo(() => getAllThemeTemplates(), []); + + const availableCategories = useMemo(() => { + const cats = new Set(templates.map((t) => t.industry)); + return Array.from(cats) as IndustryCategory[]; }, [templates]); - - // Filter themes based on search and category + const filteredThemes = useMemo(() => { - let filtered = enhancedThemes; - - // Search filter - if (searchQuery) { - const query = searchQuery.toLowerCase(); - filtered = filtered.filter((theme) => - theme.name.toLowerCase().includes(query) || - theme.description.toLowerCase().includes(query) || - theme.tags.some((tag) => tag.toLowerCase().includes(query)) - ); - } - - // Category filter + let filtered = templates.map((t) => { + const stats = getTemplateStats(t.id); + return { ...t, downloads: stats.downloads, rating: stats.rating }; + }); + if (selectedCategory !== 'all') { - filtered = filtered.filter((theme) => theme.category === selectedCategory); + filtered = filtered.filter((t) => t.industry === selectedCategory); } - - // Sort - if (sortBy === 'name') { - filtered.sort((a, b) => a.name.localeCompare(b.name)); - } else if (sortBy === 'popular') { - filtered.sort((a, b) => b.downloads - a.downloads); - } else if (sortBy === 'recent') { - // Keep original order for recent + + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + filtered = filtered.filter( + (t) => + t.name.toLowerCase().includes(q) || + t.description.toLowerCase().includes(q) || + t.tags.some((tag) => tag.toLowerCase().includes(q)), + ); } - + + switch (sortBy) { + case 'popular': + filtered.sort((a, b) => b.downloads - a.downloads); + break; + case 'rating': + filtered.sort((a, b) => b.rating - a.rating); + break; + case 'name': + filtered.sort((a, b) => a.name.localeCompare(b.name)); + break; + } + return filtered; - }, [enhancedThemes, searchQuery, selectedCategory, sortBy]); - + }, [templates, selectedCategory, searchQuery, sortBy]); + const handleInstallTheme = (templateId: ThemeTemplateId) => { const template = templates.find((t) => t.id === templateId); if (!template) return; - + onApplyTheme({ ...template.theme, - customCSS: currentTheme.customCSS, // Preserve custom CSS + customCSS: currentTheme.customCSS, }); - - toast.success(`${template.name} theme installed successfully`); - }; - - const isCurrentTheme = (templateId: ThemeTemplateId) => { - return currentTheme.templateId === templateId; + + toast.success(`${template.name} theme installed`); }; - + + const isActive = (templateId: ThemeTemplateId) => + currentTheme.templateId === templateId; + return (
{/* Header */}
-

Theme Marketplace

+

+ Theme Marketplace +

Browse and install professional themes for your store

- - {/* Search and Filters */} +
- +
- - setSortBy(v as typeof sortBy)} + > + Most Popular - Recently Added + Highest Rated Name (A-Z)
- - {/* Category Tabs */} - setSelectedCategory(v as ThemeCategory)}> - - All - Popular - Modern - Classic - Minimal + + {/* Industry Tabs */} + setSelectedCategory(v as FilterCategory)} + > + + + All + + {availableCategories.map((cat) => ( + + {getIndustryLabel(cat)} + + ))}
- + {/* Theme Grid */} -
+
{filteredThemes.length === 0 ? (
- +
) : (
- {filteredThemes.map((theme) => ( - - {/* Preview Image */} -
- {/* Theme color swatches as preview */} -
-
-
-
-
-
-
- - {/* Overlay with quick actions */} -
- -
- - {/* Current theme badge */} - {isCurrentTheme(theme.id) && ( - - - Active - + {filteredThemes.map((theme) => { + const active = isActive(theme.id); + return ( + - - -
-
- {theme.name} - - by {theme.author} β€’ v{theme.version} - + > + {/* Color Swatch Preview */} +
+
+ -
- - {theme.rating.toFixed(1)} + + {/* Hover overlay */} +
+
-
- - - -

- {theme.description} -

- - {/* Tags */} -
- {theme.tags.map((tag) => ( - - {tag} + + {active && ( + + - ))} + )}
-
- - -
- - - {theme.downloads.toLocaleString()} installs - - - -
-
- - ))} + + +
+
+ + {theme.name} + + + by StormCom β€’{' '} + {getIndustryLabel(theme.industry)} + +
+
+
+
+
+ + +

+ {theme.description} +

+
+ {theme.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} +
+
+ + +
+ + + +
+
+ + ); + })}
)}
); -} - -// Helper functions -function getCategoryForTheme(id: ThemeTemplateId): ThemeCategory { - const categories: Record = { - modern: 'modern', - classic: 'classic', - bold: 'popular', - elegant: 'popular', - minimal: 'minimal', - boutique: 'modern', - }; - return categories[id] || 'all'; -} - -function getTagsForTheme(id: ThemeTemplateId): string[] { - const tags: Record = { - modern: ['Clean', 'Minimal', 'Professional'], - classic: ['Traditional', 'Warm', 'Trustworthy'], - bold: ['Vibrant', 'Eye-catching', 'Modern'], - elegant: ['Luxury', 'Sophisticated', 'Premium'], - minimal: ['Ultra-clean', 'Simple', 'Apple-inspired'], - boutique: ['Playful', 'Friendly', 'Small Business'], - }; - return tags[id] || []; -} +} \ No newline at end of file diff --git a/src/components/dashboard/storefront/editor/theme-settings-panel.tsx b/src/components/dashboard/storefront/editor/theme-settings-panel.tsx index 207f0760..8db3417b 100644 --- a/src/components/dashboard/storefront/editor/theme-settings-panel.tsx +++ b/src/components/dashboard/storefront/editor/theme-settings-panel.tsx @@ -99,6 +99,7 @@ const FONT_OPTIONS: { value: FontFamily; label: string }[] = [ { value: 'poppins', label: 'Poppins' }, { value: 'playfair', label: 'Playfair Display' }, { value: 'montserrat', label: 'Montserrat' }, + { value: 'cormorant', label: 'Cormorant Garamond' }, ]; const LAYOUT_OPTIONS: { value: LayoutVariant; label: string }[] = [ diff --git a/src/components/dashboard/storefront/theme-selector.tsx b/src/components/dashboard/storefront/theme-selector.tsx index 9c407a8c..19c9e25d 100644 --- a/src/components/dashboard/storefront/theme-selector.tsx +++ b/src/components/dashboard/storefront/theme-selector.tsx @@ -6,8 +6,9 @@ * Allows selecting theme templates and customizing colors. */ -import { getAllThemeTemplates, getThemeTemplate } from "@/lib/storefront/theme-templates"; -import type { ThemeSettings, ThemeTemplateId } from "@/lib/storefront/types"; +import { getAllThemeTemplates, getThemeTemplate, getV2Templates, getLegacyTemplates } from "@/lib/storefront/theme-templates"; +import { FONT_FAMILIES, LAYOUT_META, RADIUS_OPTIONS } from "@/lib/storefront/theme-catalog"; +import type { ThemeSettings, ThemeTemplateId, FontFamily, LayoutVariant } from "@/lib/storefront/types"; import { isValidHexColor } from "@/lib/storefront/defaults"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -34,7 +35,8 @@ interface ThemeSelectorProps { } export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) { - const templates = getAllThemeTemplates(); + const v2Templates = getV2Templates(); + const legacyTemplates = getLegacyTemplates(); const handleTemplateSelect = (templateId: ThemeTemplateId) => { const template = getThemeTemplate(templateId); @@ -45,7 +47,6 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) { }; const handleColorChange = (key: keyof ThemeSettings["colors"], value: string) => { - // Validate hex color if (!isValidHexColor(value)) return; onThemeChange({ @@ -57,6 +58,65 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) { }); }; + const TemplateCard = ({ template }: { template: ReturnType[number] }) => ( +
+ +