Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
31 changes: 31 additions & 0 deletions apps/theme-editor/next.config.ts
Original file line number Diff line number Diff line change
@@ -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;
23 changes: 23 additions & 0 deletions apps/theme-editor/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions apps/theme-editor/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "tailwindcss";
20 changes: 20 additions & 0 deletions apps/theme-editor/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en" suppressHydrationWarning>
<body className="h-screen overflow-hidden bg-gray-100 text-gray-900 antialiased">
{children}
</body>
</html>
);
}
98 changes: 98 additions & 0 deletions apps/theme-editor/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchParams>;
}) {
const { storeId, themeId } = await searchParams;

if (!storeId) {
return (
<div className="flex h-full flex-col items-center justify-center gap-4 text-center px-4">
<div className="rounded-xl border border-amber-200 bg-amber-50 px-6 py-5 max-w-md">
<h2 className="font-semibold text-amber-900">No Store Selected</h2>
<p className="mt-2 text-sm text-amber-700">
Please open the Theme Editor from your dashboard or the Theme Marketplace.
</p>
<div className="mt-4 flex gap-3 justify-center">
<a
href="/dashboard"
className="rounded-lg bg-amber-600 px-4 py-2 text-sm font-semibold text-white hover:bg-amber-700"
>
Go to Dashboard
</a>
<a
href="/themes"
className="rounded-lg border border-amber-300 px-4 py-2 text-sm font-semibold text-amber-700 hover:bg-amber-100"
>
Browse Themes
</a>
</div>
</div>
</div>
);
}

// 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 ??
config?.theme ??
DEFAULT_THEME;

const storeName = storeInfo?.name ?? 'My Store';
const storeSlug = storeInfo?.slug ?? storeId;

return (
<ThemeEditorClient
storeId={storeId}
storeName={storeName}
storeSlug={storeSlug}
previewBaseUrl={MAIN_APP_URL}
initialTheme={initialTheme}
/>
);
}
Loading
Loading