From 323e9d6a9173e8bf5746eeebe4a5488876d21e7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:00:58 +0000 Subject: [PATCH 1/3] Initial plan From 8a10900de3fca05acab385b2a718e0c9a1894d8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:23:15 +0000 Subject: [PATCH 2/3] feat: add Theme Marketplace and Theme Editor as multi-zone apps - Prisma: Add MarketplaceTheme, ThemeInstallation, ThemeRating models + ThemeCategory/ThemeStatus enums - types.ts: Add ThemeCategory, MarketplaceThemeMeta types; extend ThemeTemplateId with 8 new themes - theme-templates.ts: 8 new categorised themes (TechNova, FashionForward, CafeBistro, GlowUp, HomeVibes, SportZone, JewelBox, Bookshelf) - API: /api/marketplace/themes (GET list), /[id] (GET detail), /[id]/install (POST), /[id]/rate (POST) - apps/theme-marketplace/: Full Next.js zone (basePath /themes) with gallery, filters, detail page - apps/theme-editor/: Full Next.js zone (basePath /themes/editor) with live preview, sidebar (colors/typography/layout/CSS) - next.config.ts: Multi-zone rewrites for /themes/* and /themes/editor/* - Appearance page: Add Theme Marketplace & Theme Editor buttons - theme-marketplace-panel.tsx: Add 8 new themes to helper functions + Full Marketplace link Co-authored-by: rezwana-karim <126201034+rezwana-karim@users.noreply.github.com> --- .env.example | 11 + apps/theme-editor/next.config.ts | 31 + apps/theme-editor/package.json | 23 + apps/theme-editor/src/app/globals.css | 1 + apps/theme-editor/src/app/layout.tsx | 20 + apps/theme-editor/src/app/page.tsx | 98 +++ .../src/components/theme-editor-client.tsx | 591 ++++++++++++++++++ apps/theme-editor/src/lib/api.ts | 87 +++ apps/theme-editor/src/lib/types.ts | 70 +++ apps/theme-editor/tsconfig.json | 22 + apps/theme-marketplace/next.config.ts | 30 + apps/theme-marketplace/package.json | 23 + apps/theme-marketplace/src/app/[id]/page.tsx | 230 +++++++ apps/theme-marketplace/src/app/globals.css | 1 + apps/theme-marketplace/src/app/layout.tsx | 71 +++ apps/theme-marketplace/src/app/page.tsx | 103 +++ .../src/components/marketplace-filters.tsx | 117 ++++ .../src/components/theme-card.tsx | 181 ++++++ .../src/components/theme-detail-client.tsx | 102 +++ .../src/components/theme-gallery.tsx | 162 +++++ apps/theme-marketplace/src/lib/api.ts | 41 ++ apps/theme-marketplace/src/lib/types.ts | 88 +++ apps/theme-marketplace/tsconfig.json | 22 + next.config.ts | 44 ++ prisma/schema.prisma | 108 ++++ .../marketplace/themes/[id]/install/route.ts | 127 ++++ .../api/marketplace/themes/[id]/rate/route.ts | 80 +++ src/app/api/marketplace/themes/[id]/route.ts | 90 +++ src/app/api/marketplace/themes/route.ts | 191 ++++++ .../stores/[storeId]/appearance/page.tsx | 16 +- .../editor/theme-marketplace-panel.tsx | 37 +- src/lib/storefront/theme-templates.ts | 379 ++++++++++- src/lib/storefront/types.ts | 61 +- tsconfig.json | 1 + vercel.json | 1 + 35 files changed, 3222 insertions(+), 38 deletions(-) create mode 100644 apps/theme-editor/next.config.ts create mode 100644 apps/theme-editor/package.json create mode 100644 apps/theme-editor/src/app/globals.css create mode 100644 apps/theme-editor/src/app/layout.tsx create mode 100644 apps/theme-editor/src/app/page.tsx create mode 100644 apps/theme-editor/src/components/theme-editor-client.tsx create mode 100644 apps/theme-editor/src/lib/api.ts create mode 100644 apps/theme-editor/src/lib/types.ts create mode 100644 apps/theme-editor/tsconfig.json create mode 100644 apps/theme-marketplace/next.config.ts create mode 100644 apps/theme-marketplace/package.json create mode 100644 apps/theme-marketplace/src/app/[id]/page.tsx create mode 100644 apps/theme-marketplace/src/app/globals.css create mode 100644 apps/theme-marketplace/src/app/layout.tsx create mode 100644 apps/theme-marketplace/src/app/page.tsx create mode 100644 apps/theme-marketplace/src/components/marketplace-filters.tsx create mode 100644 apps/theme-marketplace/src/components/theme-card.tsx create mode 100644 apps/theme-marketplace/src/components/theme-detail-client.tsx create mode 100644 apps/theme-marketplace/src/components/theme-gallery.tsx create mode 100644 apps/theme-marketplace/src/lib/api.ts create mode 100644 apps/theme-marketplace/src/lib/types.ts create mode 100644 apps/theme-marketplace/tsconfig.json create mode 100644 src/app/api/marketplace/themes/[id]/install/route.ts create mode 100644 src/app/api/marketplace/themes/[id]/rate/route.ts create mode 100644 src/app/api/marketplace/themes/[id]/route.ts create mode 100644 src/app/api/marketplace/themes/route.ts diff --git a/.env.example b/.env.example index e8fcdd7c..d4b7dcb3 100644 --- a/.env.example +++ b/.env.example @@ -58,3 +58,14 @@ FACEBOOK_WEBHOOK_VERIFY_TOKEN="" FACEBOOK_ACCESS_LEVEL="STANDARD" FACEBOOK_CONVERSIONS_ACCESS_TOKEN="" FACEBOOK_TEST_EVENT_CODE="TEST89865" + +# ───────────────────────────────────────────── +# Multi-Zone: Theme Marketplace & Editor +# ───────────────────────────────────────────── +# URLs of the separately deployed zone apps. +# In development: run apps/theme-marketplace on port 3001 +# and apps/theme-editor on port 3002. +THEME_MARKETPLACE_URL="http://localhost:3001" +THEME_EDITOR_URL="http://localhost:3002" +# The main StormCom app URL (used by zone apps to call the main API) +NEXT_PUBLIC_MAIN_APP_URL="http://localhost:3000" diff --git a/apps/theme-editor/next.config.ts b/apps/theme-editor/next.config.ts new file mode 100644 index 00000000..3707c00c --- /dev/null +++ b/apps/theme-editor/next.config.ts @@ -0,0 +1,31 @@ +import type { NextConfig } from 'next'; + +/** + * Theme Editor Zone Configuration + * + * basePath: /themes/editor — this zone handles all /themes/editor/* routes. + * In production the main StormCom app rewrites /themes/editor/* to this app. + * Run locally on port 3002 (`npm run dev` in apps/theme-editor). + */ +const nextConfig: NextConfig = { + basePath: '/themes/editor', + reactCompiler: true, + reactStrictMode: true, + typescript: { ignoreBuildErrors: false }, + poweredByHeader: false, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { key: 'Access-Control-Allow-Origin', value: process.env.MAIN_APP_URL ?? '*' }, + { key: 'Access-Control-Allow-Credentials', value: 'true' }, + // Allow the editor itself to embed the store preview iframe + { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/apps/theme-editor/package.json b/apps/theme-editor/package.json new file mode 100644 index 00000000..d31377ec --- /dev/null +++ b/apps/theme-editor/package.json @@ -0,0 +1,23 @@ +{ + "name": "@stormcom/theme-editor", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 3002", + "build": "next build", + "start": "next start --port 3002", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "next": "^16.1.6", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "5.9.3" + } +} diff --git a/apps/theme-editor/src/app/globals.css b/apps/theme-editor/src/app/globals.css new file mode 100644 index 00000000..f1d8c73c --- /dev/null +++ b/apps/theme-editor/src/app/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/apps/theme-editor/src/app/layout.tsx b/apps/theme-editor/src/app/layout.tsx new file mode 100644 index 00000000..da5cf479 --- /dev/null +++ b/apps/theme-editor/src/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: { + default: 'Theme Editor — StormCom', + template: '%s | Theme Editor', + }, + description: 'Customize your storefront theme with live preview.', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/theme-editor/src/app/page.tsx b/apps/theme-editor/src/app/page.tsx new file mode 100644 index 00000000..a9588f1c --- /dev/null +++ b/apps/theme-editor/src/app/page.tsx @@ -0,0 +1,98 @@ +import type { Metadata } from 'next'; +import { ThemeEditorClient } from '@/components/theme-editor-client'; +import { fetchStorefrontDraft, fetchStorefrontConfig, fetchStoreInfo, fetchThemeConfig } from '@/lib/api'; +import type { ThemeSettings } from '@/lib/types'; + +export const metadata: Metadata = { title: 'Theme Editor' }; + +const MAIN_APP_URL = process.env.NEXT_PUBLIC_MAIN_APP_URL ?? 'http://localhost:3000'; + +// Default theme if nothing can be loaded +const DEFAULT_THEME: ThemeSettings = { + templateId: 'modern', + colors: { + primary: '#4F46E5', + secondary: '#10B981', + accent: '#F59E0B', + background: '#FFFFFF', + foreground: '#111827', + muted: '#F3F4F6', + }, + typography: { + fontFamily: 'geist', + baseFontSize: 16, + headingScale: 1.25, + }, + layout: 'full-width', + borderRadius: 'lg', +}; + +interface SearchParams { + storeId?: string; + themeId?: string; +} + +export default async function ThemeEditorPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const { storeId, themeId } = await searchParams; + + if (!storeId) { + return ( +
+
+

No Store Selected

+

+ Please open the Theme Editor from your dashboard or the Theme Marketplace. +

+
+ + Go to Dashboard + + + Browse Themes + +
+
+
+ ); + } + + // Load store info, draft, and marketplace theme in parallel + const [storeInfo, draft, marketplaceThemeConfig] = await Promise.all([ + fetchStoreInfo(storeId), + fetchStorefrontDraft(storeId).catch(() => null), + themeId ? fetchThemeConfig(themeId).catch(() => null) : Promise.resolve(null), + ]); + + // Fall back to published config if no draft + const publishedConfig = !draft ? await fetchStorefrontConfig(storeId).catch(() => null) : null; + const config = draft ?? publishedConfig; + + // Theme priority: marketplace theme URL param > draft/published > default + const initialTheme: ThemeSettings = + (marketplaceThemeConfig as ThemeSettings | null) ?? + (config?.theme as ThemeSettings | undefined) ?? + DEFAULT_THEME; + + const storeName = storeInfo?.name ?? 'My Store'; + const storeSlug = storeInfo?.slug ?? storeId; + + return ( + + ); +} diff --git a/apps/theme-editor/src/components/theme-editor-client.tsx b/apps/theme-editor/src/components/theme-editor-client.tsx new file mode 100644 index 00000000..0751fc20 --- /dev/null +++ b/apps/theme-editor/src/components/theme-editor-client.tsx @@ -0,0 +1,591 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef } from 'react'; +import type { ThemeSettings, FontFamily } from '@/lib/types'; +import { saveDraft, publishDraft } from '@/lib/api'; + +const FONT_OPTIONS: FontFamily[] = ['geist', 'inter', 'roboto', 'poppins', 'montserrat', 'playfair']; +const BORDER_RADIUS_OPTIONS = ['none', 'sm', 'md', 'lg', 'xl'] as const; +const LAYOUT_OPTIONS = ['full-width', 'boxed', 'centered'] as const; + +interface Props { + storeId: string; + storeName: string; + storeSlug: string; + previewBaseUrl: string; + initialTheme: ThemeSettings; +} + +type ColorKey = keyof ThemeSettings['colors']; + +const COLOR_LABELS: Record = { + primary: 'Primary', + secondary: 'Secondary', + accent: 'Accent', + background: 'Background', + foreground: 'Text', + muted: 'Muted', +}; + +export function ThemeEditorClient({ storeId, storeName, storeSlug, previewBaseUrl, initialTheme }: Props) { + const [theme, setTheme] = useState(initialTheme); + const [isDirty, setIsDirty] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isPublishing, setIsPublishing] = useState(false); + const [saveError, setSaveError] = useState(''); + const [publishSuccess, setPublishSuccess] = useState(false); + const [activeSection, setActiveSection] = useState<'colors' | 'typography' | 'layout' | 'css'>('colors'); + const [previewDevice, setPreviewDevice] = useState<'desktop' | 'tablet' | 'mobile'>('desktop'); + + const iframeRef = useRef(null); + const autosaveTimer = useRef | null>(null); + + const previewUrl = `${previewBaseUrl}/store/${storeSlug}?preview=1&t=${Date.now()}`; + + // Send live theme updates to the preview iframe + const sendThemeToPreview = useCallback((t: ThemeSettings) => { + iframeRef.current?.contentWindow?.postMessage( + { type: 'STORMCOM_THEME_UPDATE', theme: t }, + '*' + ); + }, []); + + // Auto-save draft after 2 seconds of inactivity + useEffect(() => { + if (!isDirty) return; + if (autosaveTimer.current) clearTimeout(autosaveTimer.current); + autosaveTimer.current = setTimeout(async () => { + setIsSaving(true); + setSaveError(''); + try { + await saveDraft(storeId, theme); + setIsDirty(false); + } catch (e) { + setSaveError(e instanceof Error ? e.message : 'Auto-save failed'); + } finally { + setIsSaving(false); + } + }, 2000); + return () => { if (autosaveTimer.current) clearTimeout(autosaveTimer.current); }; + }, [theme, isDirty, storeId]); + + const updateColor = useCallback((key: ColorKey, value: string) => { + setTheme((prev) => { + const next = { ...prev, colors: { ...prev.colors, [key]: value } }; + sendThemeToPreview(next); + return next; + }); + setIsDirty(true); + setPublishSuccess(false); + }, [sendThemeToPreview]); + + const updateTypography = useCallback(( + key: K, + value: ThemeSettings['typography'][K] + ) => { + setTheme((prev) => { + const next = { ...prev, typography: { ...prev.typography, [key]: value } }; + sendThemeToPreview(next); + return next; + }); + setIsDirty(true); + setPublishSuccess(false); + }, [sendThemeToPreview]); + + const updateLayout = useCallback((layout: ThemeSettings['layout']) => { + setTheme((prev) => { + const next = { ...prev, layout }; + sendThemeToPreview(next); + return next; + }); + setIsDirty(true); + setPublishSuccess(false); + }, [sendThemeToPreview]); + + const updateBorderRadius = useCallback((borderRadius: ThemeSettings['borderRadius']) => { + setTheme((prev) => { + const next = { ...prev, borderRadius }; + sendThemeToPreview(next); + return next; + }); + setIsDirty(true); + setPublishSuccess(false); + }, [sendThemeToPreview]); + + const updateCustomCSS = useCallback((customCSS: string) => { + setTheme((prev) => { + const next = { ...prev, customCSS }; + sendThemeToPreview(next); + return next; + }); + setIsDirty(true); + setPublishSuccess(false); + }, [sendThemeToPreview]); + + const handleSave = async () => { + setIsSaving(true); + setSaveError(''); + try { + await saveDraft(storeId, theme); + setIsDirty(false); + } catch (e) { + setSaveError(e instanceof Error ? e.message : 'Save failed'); + } finally { + setIsSaving(false); + } + }; + + const handlePublish = async () => { + setIsPublishing(true); + setSaveError(''); + setPublishSuccess(false); + try { + // Save first to ensure latest changes are in draft + await saveDraft(storeId, theme); + await publishDraft(storeId); + setIsDirty(false); + setPublishSuccess(true); + } catch (e) { + setSaveError(e instanceof Error ? e.message : 'Publish failed'); + } finally { + setIsPublishing(false); + } + }; + + const handleReset = () => { + setTheme(initialTheme); + sendThemeToPreview(initialTheme); + setIsDirty(false); + setPublishSuccess(false); + setSaveError(''); + }; + + const previewWidth = + previewDevice === 'mobile' ? 375 : + previewDevice === 'tablet' ? 768 : + '100%'; + + return ( +
+ {/* ── Top toolbar ──────────────────────────────────────── */} +
+
+ + + + + Marketplace + + | + {storeName} + {isDirty && ( + + Unsaved + + )} + {isSaving && ( + Saving… + )} + {publishSuccess && ( + + + + + Published! + + )} +
+ + {/* Device switcher */} +
+ {(['desktop', 'tablet', 'mobile'] as const).map((d) => ( + + ))} +
+ + {/* Actions */} +
+ {saveError && ( + {saveError} + )} + + + +
+
+ + {/* ── Main area: sidebar + preview ──────────────────────── */} +
+ {/* Sidebar */} + + + {/* Preview pane */} +
+
+