@olonjs/cli 3.0.127 → 3.0.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/src_tenant_alpha.sh +645 -92
- package/assets/templates/alpha/src_tenant.sh +645 -92
- package/package.json +1 -1
|
@@ -1597,6 +1597,37 @@ If user says "use dark theme" or "change my theme":
|
|
|
1597
1597
|
4. Write the updated JSON back to the file
|
|
1598
1598
|
5. Inform the user the setting has been changed and whether a reload is needed
|
|
1599
1599
|
|
|
1600
|
+
END_OF_FILE_CONTENT
|
|
1601
|
+
echo "Creating components.json..."
|
|
1602
|
+
cat << 'END_OF_FILE_CONTENT' > "components.json"
|
|
1603
|
+
{
|
|
1604
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
1605
|
+
"style": "radix-nova",
|
|
1606
|
+
"rsc": false,
|
|
1607
|
+
"tsx": true,
|
|
1608
|
+
"tailwind": {
|
|
1609
|
+
"config": "",
|
|
1610
|
+
"css": "src/index.css",
|
|
1611
|
+
"baseColor": "zinc",
|
|
1612
|
+
"cssVariables": true,
|
|
1613
|
+
"prefix": ""
|
|
1614
|
+
},
|
|
1615
|
+
"iconLibrary": "lucide",
|
|
1616
|
+
"rtl": false,
|
|
1617
|
+
"aliases": {
|
|
1618
|
+
"components": "@/components",
|
|
1619
|
+
"utils": "@/lib/utils",
|
|
1620
|
+
"ui": "@/components/ui",
|
|
1621
|
+
"lib": "@/lib",
|
|
1622
|
+
"hooks": "@/hooks"
|
|
1623
|
+
},
|
|
1624
|
+
"menuColor": "default",
|
|
1625
|
+
"menuAccent": "bold",
|
|
1626
|
+
"registries": {}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
|
|
1600
1631
|
END_OF_FILE_CONTENT
|
|
1601
1632
|
echo "Creating index.html..."
|
|
1602
1633
|
cat << 'END_OF_FILE_CONTENT' > "index.html"
|
|
@@ -1632,7 +1663,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1632
1663
|
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
1633
1664
|
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs",
|
|
1634
1665
|
"build": "tsc && vite build",
|
|
1635
|
-
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts
|
|
1666
|
+
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts ",
|
|
1636
1667
|
"preview": "vite preview",
|
|
1637
1668
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
1638
1669
|
"bakemail": "npm run bake:email --",
|
|
@@ -1643,7 +1674,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1643
1674
|
"@tiptap/extension-link": "^2.11.5",
|
|
1644
1675
|
"@tiptap/react": "^2.11.5",
|
|
1645
1676
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1646
|
-
"@olonjs/core": "^1.0.
|
|
1677
|
+
"@olonjs/core": "^1.0.120",
|
|
1647
1678
|
"class-variance-authority": "^0.7.1",
|
|
1648
1679
|
"clsx": "^2.1.1",
|
|
1649
1680
|
"lucide-react": "^0.474.0",
|
|
@@ -1672,7 +1703,6 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1672
1703
|
}
|
|
1673
1704
|
|
|
1674
1705
|
END_OF_FILE_CONTENT
|
|
1675
|
-
# SKIP: public/assets/images/plug-graded-square.jpg is binary and cannot be embedded as text.
|
|
1676
1706
|
mkdir -p "scripts"
|
|
1677
1707
|
echo "Creating scripts/bake-email.tsx..."
|
|
1678
1708
|
cat << 'END_OF_FILE_CONTENT' > "scripts/bake-email.tsx"
|
|
@@ -2064,7 +2094,7 @@ if (targets.length === 0) {
|
|
|
2064
2094
|
console.log(`[bake] Targets: ${targets.map((t) => t.slug).join(', ')}`);
|
|
2065
2095
|
|
|
2066
2096
|
const ssrEntryUrl = pathToFileURL(path.resolve(root, 'dist-ssr/entry-ssg.js')).href;
|
|
2067
|
-
const { render, getCss, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
|
|
2097
|
+
const { render, getCss, getRemoteStylesheets, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
|
|
2068
2098
|
|
|
2069
2099
|
const template = await fs.readFile(path.resolve(root, 'dist/index.html'), 'utf-8');
|
|
2070
2100
|
const hasCommentMarker = template.includes('<!--app-html-->');
|
|
@@ -2075,6 +2105,9 @@ if (!hasCommentMarker && !hasRootDivMarker) {
|
|
|
2075
2105
|
|
|
2076
2106
|
const inlinedCss = getCss();
|
|
2077
2107
|
const styleTag = `<style data-bake="inline">${inlinedCss}</style>`;
|
|
2108
|
+
const remoteStylesheetTags = getRemoteStylesheets()
|
|
2109
|
+
.map((href) => `<link rel="stylesheet" href="${escapeHtmlAttribute(href)}">`)
|
|
2110
|
+
.join('\n ');
|
|
2078
2111
|
const webMcpBuildState = getWebMcpBuildState();
|
|
2079
2112
|
|
|
2080
2113
|
for (const { slug } of targets) {
|
|
@@ -2148,7 +2181,7 @@ for (const { slug, out, depth } of targets) {
|
|
|
2148
2181
|
].join('\n ');
|
|
2149
2182
|
|
|
2150
2183
|
let bakedHtml = fixedTemplate
|
|
2151
|
-
.replace('</head>', ` ${styleTag}\n ${contractLinks}\n</head>`)
|
|
2184
|
+
.replace('</head>', ` ${remoteStylesheetTags ? `${remoteStylesheetTags}\n ` : ''}${styleTag}\n ${contractLinks}\n</head>`)
|
|
2152
2185
|
.replace(/<title>.*?<\/title>/, `<title>${safeTitle}</title>\n ${metaTags}`);
|
|
2153
2186
|
|
|
2154
2187
|
if (hasCommentMarker) {
|
|
@@ -2569,6 +2602,9 @@ import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
|
2569
2602
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
2570
2603
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
2571
2604
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
2605
|
+
import { useOlonForms } from '@/lib/useOlonForms';
|
|
2606
|
+
import { OlonFormsContext } from '@/lib/OlonFormsContext';
|
|
2607
|
+
import { iconMap } from '@/lib/IconResolver';
|
|
2572
2608
|
|
|
2573
2609
|
import tenantCss from './index.css?inline';
|
|
2574
2610
|
|
|
@@ -2917,6 +2953,42 @@ function buildThemeFontVarsCss(input: unknown): string {
|
|
|
2917
2953
|
return `:root{--theme-font-primary:${primary};--theme-font-serif:${serif};--theme-font-mono:${mono};}`;
|
|
2918
2954
|
}
|
|
2919
2955
|
|
|
2956
|
+
const REMOTE_CSS_LINK_ATTR = 'data-jp-tenant-remote-css';
|
|
2957
|
+
|
|
2958
|
+
function isRemoteStylesheetHref(value: string): boolean {
|
|
2959
|
+
return /^https?:\/\//i.test(value.trim());
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function extractLeadingRemoteCssImports(cssText: string): { hrefs: string[]; rest: string } {
|
|
2963
|
+
const hrefs = new Set<string>();
|
|
2964
|
+
const leadingTriviaPattern = /^(?:\s+|\/\*[\s\S]*?\*\/)*/;
|
|
2965
|
+
const importPattern =
|
|
2966
|
+
/^@import\s+url\(\s*(?:'([^']+)'|"([^"]+)"|([^'")\s][^)]*))\s*\)\s*([^;]*);/i;
|
|
2967
|
+
let rest = cssText;
|
|
2968
|
+
|
|
2969
|
+
for (;;) {
|
|
2970
|
+
const trivia = rest.match(leadingTriviaPattern);
|
|
2971
|
+
if (trivia && trivia[0]) {
|
|
2972
|
+
rest = rest.slice(trivia[0].length);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
const match = rest.match(importPattern);
|
|
2976
|
+
if (!match) break;
|
|
2977
|
+
|
|
2978
|
+
const href = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
|
2979
|
+
const trailingDirectives = (match[4] ?? '').trim();
|
|
2980
|
+
|
|
2981
|
+
if (!isRemoteStylesheetHref(href) || trailingDirectives.length > 0) {
|
|
2982
|
+
break;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
hrefs.add(href);
|
|
2986
|
+
rest = rest.slice(match[0].length);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
return { hrefs: Array.from(hrefs), rest };
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2920
2992
|
function setTenantPreviewReady(ready: boolean): void {
|
|
2921
2993
|
if (typeof window !== 'undefined') {
|
|
2922
2994
|
(window as Window & { __TENANT_PREVIEW_READY__?: boolean }).__TENANT_PREVIEW_READY__ = ready;
|
|
@@ -2927,6 +2999,7 @@ function setTenantPreviewReady(ready: boolean): void {
|
|
|
2927
2999
|
}
|
|
2928
3000
|
|
|
2929
3001
|
function App() {
|
|
3002
|
+
const { states: formStates } = useOlonForms();
|
|
2930
3003
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
2931
3004
|
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
2932
3005
|
const isHotSaveMode = isCloudMode && !isSave2RepoMode;
|
|
@@ -3331,6 +3404,39 @@ function App() {
|
|
|
3331
3404
|
void runCloudSave(pendingCloudSave.current, false);
|
|
3332
3405
|
}, [runCloudSave]);
|
|
3333
3406
|
|
|
3407
|
+
const tenantCssParts = useMemo(() => extractLeadingRemoteCssImports(tenantCss), [tenantCss]);
|
|
3408
|
+
const resolvedTenantCss = useMemo(
|
|
3409
|
+
() => [buildThemeFontVarsCss(themeConfig), tenantCssParts.rest].filter(Boolean).join('\n'),
|
|
3410
|
+
[tenantCssParts],
|
|
3411
|
+
);
|
|
3412
|
+
|
|
3413
|
+
useEffect(() => {
|
|
3414
|
+
if (typeof document === 'undefined') return undefined;
|
|
3415
|
+
|
|
3416
|
+
const createdLinks: HTMLLinkElement[] = [];
|
|
3417
|
+
|
|
3418
|
+
tenantCssParts.hrefs.forEach((href) => {
|
|
3419
|
+
const existing = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
|
|
3420
|
+
(link) => (link as HTMLLinkElement).href === href,
|
|
3421
|
+
) as HTMLLinkElement | undefined;
|
|
3422
|
+
if (existing) return;
|
|
3423
|
+
|
|
3424
|
+
const link = document.createElement('link');
|
|
3425
|
+
link.rel = 'stylesheet';
|
|
3426
|
+
link.href = href;
|
|
3427
|
+
link.setAttribute(REMOTE_CSS_LINK_ATTR, href);
|
|
3428
|
+
document.head.appendChild(link);
|
|
3429
|
+
createdLinks.push(link);
|
|
3430
|
+
});
|
|
3431
|
+
|
|
3432
|
+
return () => {
|
|
3433
|
+
createdLinks.forEach((link) => {
|
|
3434
|
+
if (link.getAttribute(REMOTE_CSS_LINK_ATTR) !== link.href) return;
|
|
3435
|
+
if (link.parentNode) link.parentNode.removeChild(link);
|
|
3436
|
+
});
|
|
3437
|
+
};
|
|
3438
|
+
}, [tenantCssParts]);
|
|
3439
|
+
|
|
3334
3440
|
const config: JsonPagesConfig = {
|
|
3335
3441
|
tenantId: TENANT_ID,
|
|
3336
3442
|
basePath: APP_BASE_PATH,
|
|
@@ -3341,7 +3447,8 @@ function App() {
|
|
|
3341
3447
|
themeConfig,
|
|
3342
3448
|
menuConfig,
|
|
3343
3449
|
refDocuments,
|
|
3344
|
-
|
|
3450
|
+
iconRegistry: iconMap,
|
|
3451
|
+
themeCss: { tenant: resolvedTenantCss },
|
|
3345
3452
|
addSection: addSectionConfig,
|
|
3346
3453
|
webmcp: {
|
|
3347
3454
|
enabled: true,
|
|
@@ -3503,6 +3610,7 @@ function App() {
|
|
|
3503
3610
|
|
|
3504
3611
|
return (
|
|
3505
3612
|
<ThemeProvider>
|
|
3613
|
+
<OlonFormsContext.Provider value={formStates}>
|
|
3506
3614
|
<>
|
|
3507
3615
|
{isCloudMode && showTopProgress ? (
|
|
3508
3616
|
<>
|
|
@@ -3624,6 +3732,7 @@ function App() {
|
|
|
3624
3732
|
onRetry={retryCloudSave}
|
|
3625
3733
|
/>
|
|
3626
3734
|
</>
|
|
3735
|
+
</OlonFormsContext.Provider>
|
|
3627
3736
|
</ThemeProvider>
|
|
3628
3737
|
);
|
|
3629
3738
|
}
|
|
@@ -3634,91 +3743,90 @@ END_OF_FILE_CONTENT
|
|
|
3634
3743
|
mkdir -p "src/components"
|
|
3635
3744
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
3636
3745
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
|
|
3637
|
-
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3638
|
-
|
|
3639
|
-
type Theme = 'dark' | 'light'
|
|
3640
|
-
|
|
3641
|
-
interface ThemeContextValue {
|
|
3642
|
-
theme: Theme
|
|
3643
|
-
toggleTheme: () => void
|
|
3644
|
-
setTheme: (t: Theme) => void
|
|
3645
|
-
}
|
|
3646
|
-
|
|
3647
|
-
const ThemeContext = createContext<ThemeContextValue>({
|
|
3648
|
-
theme: 'dark',
|
|
3649
|
-
toggleTheme: () => {},
|
|
3650
|
-
setTheme: () => {},
|
|
3651
|
-
})
|
|
3652
|
-
|
|
3653
|
-
const STORAGE_KEY = 'olon:theme'
|
|
3654
|
-
|
|
3655
|
-
function isTheme(value: unknown): value is Theme {
|
|
3656
|
-
return value === 'dark' || value === 'light'
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
function resolveInitialTheme(): Theme {
|
|
3660
|
-
if (typeof window === 'undefined') return 'dark'
|
|
3661
|
-
|
|
3662
|
-
const fromDom = document.documentElement.getAttribute('data-theme')
|
|
3663
|
-
if (isTheme(fromDom)) return fromDom
|
|
3664
|
-
|
|
3665
|
-
const fromStorage = window.localStorage.getItem(STORAGE_KEY)
|
|
3666
|
-
if (isTheme(fromStorage)) return fromStorage
|
|
3667
|
-
|
|
3668
|
-
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
|
|
3669
|
-
return prefersLight ? 'light' : 'dark'
|
|
3670
|
-
}
|
|
3671
|
-
|
|
3672
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
3673
|
-
const [theme, setThemeState] = useState<Theme>(resolveInitialTheme)
|
|
3674
|
-
|
|
3675
|
-
useEffect(() => {
|
|
3676
|
-
document.documentElement.setAttribute('data-theme', theme)
|
|
3677
|
-
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
3678
|
-
}, [theme])
|
|
3679
|
-
|
|
3680
|
-
function setTheme(t: Theme) {
|
|
3681
|
-
setThemeState(t)
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
function toggleTheme() {
|
|
3685
|
-
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme])
|
|
3689
|
-
|
|
3690
|
-
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
export function useTheme() {
|
|
3694
|
-
return useContext(ThemeContext)
|
|
3695
|
-
}
|
|
3696
|
-
|
|
3697
|
-
END_OF_FILE_CONTENT
|
|
3698
|
-
mkdir -p "src/components/empty-tenant"
|
|
3699
|
-
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
3700
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/View.tsx"
|
|
3701
|
-
import type { EmptyTenantData } from './types';
|
|
3746
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3702
3747
|
|
|
3703
|
-
type
|
|
3704
|
-
data?: EmptyTenantData;
|
|
3705
|
-
};
|
|
3748
|
+
type Theme = 'dark' | 'light'
|
|
3706
3749
|
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3750
|
+
interface ThemeContextValue {
|
|
3751
|
+
theme: Theme
|
|
3752
|
+
toggleTheme: () => void
|
|
3753
|
+
setTheme: (t: Theme) => void
|
|
3754
|
+
}
|
|
3710
3755
|
|
|
3756
|
+
const ThemeContext = createContext<ThemeContextValue>({
|
|
3757
|
+
theme: 'dark',
|
|
3758
|
+
toggleTheme: () => {},
|
|
3759
|
+
setTheme: () => {},
|
|
3760
|
+
})
|
|
3711
3761
|
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
)
|
|
3762
|
+
const STORAGE_KEY = 'olon:theme'
|
|
3763
|
+
|
|
3764
|
+
function isTheme(value: unknown): value is Theme {
|
|
3765
|
+
return value === 'dark' || value === 'light'
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
function resolveInitialTheme(): Theme {
|
|
3769
|
+
if (typeof window === 'undefined') return 'dark'
|
|
3770
|
+
|
|
3771
|
+
const fromDom = document.documentElement.getAttribute('data-theme')
|
|
3772
|
+
if (isTheme(fromDom)) return fromDom
|
|
3773
|
+
|
|
3774
|
+
const fromStorage = window.localStorage.getItem(STORAGE_KEY)
|
|
3775
|
+
if (isTheme(fromStorage)) return fromStorage
|
|
3776
|
+
|
|
3777
|
+
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
|
|
3778
|
+
return prefersLight ? 'light' : 'dark'
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
3782
|
+
const [theme, setThemeState] = useState<Theme>(resolveInitialTheme)
|
|
3783
|
+
|
|
3784
|
+
useEffect(() => {
|
|
3785
|
+
document.documentElement.setAttribute('data-theme', theme)
|
|
3786
|
+
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
3787
|
+
}, [theme])
|
|
3788
|
+
|
|
3789
|
+
function setTheme(t: Theme) {
|
|
3790
|
+
setThemeState(t)
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function toggleTheme() {
|
|
3794
|
+
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme])
|
|
3798
|
+
|
|
3799
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
export function useTheme() {
|
|
3803
|
+
return useContext(ThemeContext)
|
|
3720
3804
|
}
|
|
3721
3805
|
|
|
3806
|
+
END_OF_FILE_CONTENT
|
|
3807
|
+
mkdir -p "src/components/empty-tenant"
|
|
3808
|
+
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
3809
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/View.tsx"
|
|
3810
|
+
import type { EmptyTenantData } from './types';
|
|
3811
|
+
|
|
3812
|
+
type EmptyTenantViewProps = {
|
|
3813
|
+
data?: EmptyTenantData;
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
export function EmptyTenantView({ data }: EmptyTenantViewProps) {
|
|
3817
|
+
const title = data?.title?.trim() || 'Your tenant is empty.';
|
|
3818
|
+
const description = data?.description?.trim() || 'Create your first page to start building your site.';
|
|
3819
|
+
|
|
3820
|
+
return (
|
|
3821
|
+
<main className="min-h-screen flex items-center justify-center bg-background text-foreground px-6">
|
|
3822
|
+
<section className="w-full max-w-xl rounded-xl border border-border bg-card p-8 shadow-sm">
|
|
3823
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
3824
|
+
<p className="mt-3 text-sm text-muted-foreground">{description}</p>
|
|
3825
|
+
</section>
|
|
3826
|
+
</main>
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3722
3830
|
END_OF_FILE_CONTENT
|
|
3723
3831
|
echo "Creating src/components/empty-tenant/index.ts..."
|
|
3724
3832
|
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/index.ts"
|
|
@@ -3735,7 +3843,6 @@ import { BaseSectionData } from '@/lib/base-schemas';
|
|
|
3735
3843
|
export const EmptyTenantSchema = BaseSectionData.extend({
|
|
3736
3844
|
title: z.string().optional().describe('ui:text'),
|
|
3737
3845
|
description: z.string().optional().describe('ui:textarea'),
|
|
3738
|
-
|
|
3739
3846
|
});
|
|
3740
3847
|
|
|
3741
3848
|
export const EmptyTenantSettingsSchema = z.object({});
|
|
@@ -3749,6 +3856,196 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
3749
3856
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
3750
3857
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
3751
3858
|
|
|
3859
|
+
END_OF_FILE_CONTENT
|
|
3860
|
+
mkdir -p "src/components/form-demo"
|
|
3861
|
+
echo "Creating src/components/form-demo/View.tsx..."
|
|
3862
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
3863
|
+
import { Icon } from '@/lib/IconResolver';
|
|
3864
|
+
import { useFormState } from '@/lib/OlonFormsContext';
|
|
3865
|
+
import type { FormDemoData } from './types';
|
|
3866
|
+
|
|
3867
|
+
type FormDemoViewProps = {
|
|
3868
|
+
data: FormDemoData;
|
|
3869
|
+
};
|
|
3870
|
+
|
|
3871
|
+
const missingEnv =
|
|
3872
|
+
!import.meta.env.VITE_JSONPAGES_CLOUD_URL &&
|
|
3873
|
+
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
3874
|
+
|
|
3875
|
+
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
3876
|
+
const steps = [
|
|
3877
|
+
{
|
|
3878
|
+
done: !!recipientEmail,
|
|
3879
|
+
label: 'recipientEmail nel JSON della sezione',
|
|
3880
|
+
code: '"recipientEmail": "tu@esempio.it"',
|
|
3881
|
+
},
|
|
3882
|
+
{
|
|
3883
|
+
done: !missingEnv,
|
|
3884
|
+
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
3885
|
+
code: 'VITE_JSONPAGES_CLOUD_URL=https://cloud.olonjs.io',
|
|
3886
|
+
},
|
|
3887
|
+
{
|
|
3888
|
+
done: !!import.meta.env.VITE_JSONPAGES_API_KEY || !!import.meta.env.VITE_OLONJS_API_KEY,
|
|
3889
|
+
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
3890
|
+
code: 'VITE_JSONPAGES_API_KEY=sk-...',
|
|
3891
|
+
},
|
|
3892
|
+
];
|
|
3893
|
+
|
|
3894
|
+
const allDone = steps.every((s) => s.done);
|
|
3895
|
+
if (allDone) return null;
|
|
3896
|
+
|
|
3897
|
+
return (
|
|
3898
|
+
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-3 text-sm">
|
|
3899
|
+
<p className="font-medium text-foreground">Quasi pronto — completa questi passaggi</p>
|
|
3900
|
+
<ol className="space-y-2">
|
|
3901
|
+
{steps.map((step, i) => (
|
|
3902
|
+
<li key={i} className="flex items-start gap-2">
|
|
3903
|
+
<span className={step.done ? 'text-green-500' : 'text-muted-foreground'}>
|
|
3904
|
+
{step.done ? '✓' : `${i + 1}.`}
|
|
3905
|
+
</span>
|
|
3906
|
+
<span className={step.done ? 'text-muted-foreground line-through' : 'text-foreground'}>
|
|
3907
|
+
{step.label}
|
|
3908
|
+
{!step.done && (
|
|
3909
|
+
<code className="block mt-0.5 text-xs bg-background rounded px-1.5 py-0.5 font-mono text-muted-foreground border border-border">
|
|
3910
|
+
{step.code}
|
|
3911
|
+
</code>
|
|
3912
|
+
)}
|
|
3913
|
+
</span>
|
|
3914
|
+
</li>
|
|
3915
|
+
))}
|
|
3916
|
+
</ol>
|
|
3917
|
+
</div>
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
export function FormDemoView({ data }: FormDemoViewProps) {
|
|
3922
|
+
const formId = data.anchorId?.trim() || 'form-demo';
|
|
3923
|
+
const { status, message } = useFormState(formId);
|
|
3924
|
+
|
|
3925
|
+
return (
|
|
3926
|
+
<main className="min-h-screen flex items-center justify-center bg-background text-foreground px-6">
|
|
3927
|
+
<section className="w-full max-w-xl rounded-xl border border-border bg-card p-8 shadow-sm space-y-6">
|
|
3928
|
+
{data.icon && (
|
|
3929
|
+
<div data-jp-field="icon" className="mb-2">
|
|
3930
|
+
<Icon name={data.icon} size={24} />
|
|
3931
|
+
</div>
|
|
3932
|
+
)}
|
|
3933
|
+
{data.title && (
|
|
3934
|
+
<div>
|
|
3935
|
+
<h1
|
|
3936
|
+
data-jp-field="title"
|
|
3937
|
+
className="text-2xl font-semibold tracking-tight"
|
|
3938
|
+
>
|
|
3939
|
+
{data.title}
|
|
3940
|
+
</h1>
|
|
3941
|
+
{data.description && (
|
|
3942
|
+
<p
|
|
3943
|
+
data-jp-field="description"
|
|
3944
|
+
className="mt-3 text-sm text-muted-foreground"
|
|
3945
|
+
>
|
|
3946
|
+
{data.description}
|
|
3947
|
+
</p>
|
|
3948
|
+
)}
|
|
3949
|
+
</div>
|
|
3950
|
+
)}
|
|
3951
|
+
|
|
3952
|
+
<SetupGuide recipientEmail={data.recipientEmail} />
|
|
3953
|
+
|
|
3954
|
+
<form
|
|
3955
|
+
id={formId}
|
|
3956
|
+
data-olon-recipient={data.recipientEmail ?? ''}
|
|
3957
|
+
className="space-y-4"
|
|
3958
|
+
>
|
|
3959
|
+
<div>
|
|
3960
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3961
|
+
Nome
|
|
3962
|
+
</label>
|
|
3963
|
+
<input
|
|
3964
|
+
name="name"
|
|
3965
|
+
type="text"
|
|
3966
|
+
required
|
|
3967
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3968
|
+
/>
|
|
3969
|
+
</div>
|
|
3970
|
+
|
|
3971
|
+
<div>
|
|
3972
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3973
|
+
Email
|
|
3974
|
+
</label>
|
|
3975
|
+
<input
|
|
3976
|
+
name="email"
|
|
3977
|
+
type="email"
|
|
3978
|
+
required
|
|
3979
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3980
|
+
/>
|
|
3981
|
+
</div>
|
|
3982
|
+
|
|
3983
|
+
<div>
|
|
3984
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3985
|
+
Messaggio
|
|
3986
|
+
</label>
|
|
3987
|
+
<textarea
|
|
3988
|
+
name="message"
|
|
3989
|
+
required
|
|
3990
|
+
rows={4}
|
|
3991
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3992
|
+
/>
|
|
3993
|
+
</div>
|
|
3994
|
+
|
|
3995
|
+
{status === 'error' && (
|
|
3996
|
+
<p className="text-xs text-destructive">{message}</p>
|
|
3997
|
+
)}
|
|
3998
|
+
{status === 'success' && (
|
|
3999
|
+
<p className="text-xs text-green-600">
|
|
4000
|
+
{data.successMessage || message}
|
|
4001
|
+
</p>
|
|
4002
|
+
)}
|
|
4003
|
+
|
|
4004
|
+
<button
|
|
4005
|
+
type="submit"
|
|
4006
|
+
disabled={status === 'submitting'}
|
|
4007
|
+
className="w-full rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed transition-opacity"
|
|
4008
|
+
>
|
|
4009
|
+
{status === 'submitting' ? 'Invio...' : (data.submitLabel || 'Invia')}
|
|
4010
|
+
</button>
|
|
4011
|
+
</form>
|
|
4012
|
+
</section>
|
|
4013
|
+
</main>
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
END_OF_FILE_CONTENT
|
|
4018
|
+
echo "Creating src/components/form-demo/index.ts..."
|
|
4019
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/index.ts"
|
|
4020
|
+
export * from './View';
|
|
4021
|
+
export * from './schema';
|
|
4022
|
+
export * from './types';
|
|
4023
|
+
|
|
4024
|
+
END_OF_FILE_CONTENT
|
|
4025
|
+
echo "Creating src/components/form-demo/schema.ts..."
|
|
4026
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/schema.ts"
|
|
4027
|
+
import { z } from 'zod';
|
|
4028
|
+
import { BaseSectionData, WithFormRecipient } from '@/lib/base-schemas';
|
|
4029
|
+
|
|
4030
|
+
export const FormDemoSchema = BaseSectionData.merge(WithFormRecipient).extend({
|
|
4031
|
+
icon: z.string().optional().describe('ui:icon-picker'),
|
|
4032
|
+
title: z.string().optional().describe('ui:text'),
|
|
4033
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4034
|
+
submitLabel: z.string().default('Invia').describe('ui:text'),
|
|
4035
|
+
successMessage: z.string().default('Richiesta inviata con successo.').describe('ui:text'),
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
export const FormDemoSettingsSchema = z.object({});
|
|
4039
|
+
|
|
4040
|
+
END_OF_FILE_CONTENT
|
|
4041
|
+
echo "Creating src/components/form-demo/types.ts..."
|
|
4042
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/types.ts"
|
|
4043
|
+
import { z } from 'zod';
|
|
4044
|
+
import { FormDemoSchema, FormDemoSettingsSchema } from './schema';
|
|
4045
|
+
|
|
4046
|
+
export type FormDemoData = z.infer<typeof FormDemoSchema>;
|
|
4047
|
+
export type FormDemoSettings = z.infer<typeof FormDemoSettingsSchema>;
|
|
4048
|
+
|
|
3752
4049
|
END_OF_FILE_CONTENT
|
|
3753
4050
|
mkdir -p "src/components/save-drawer"
|
|
3754
4051
|
echo "Creating src/components/save-drawer/DeployConnector.tsx..."
|
|
@@ -7662,6 +7959,33 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
|
7662
7959
|
}
|
|
7663
7960
|
END_OF_FILE_CONTENT
|
|
7664
7961
|
mkdir -p "src/data/pages"
|
|
7962
|
+
echo "Creating src/data/pages/home.json..."
|
|
7963
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
7964
|
+
{
|
|
7965
|
+
"id": "home-page",
|
|
7966
|
+
"slug": "home",
|
|
7967
|
+
"meta": {
|
|
7968
|
+
"title": "Home",
|
|
7969
|
+
"description": "OlonJS tenant alpha — form smoke test"
|
|
7970
|
+
},
|
|
7971
|
+
"sections": [
|
|
7972
|
+
{
|
|
7973
|
+
"id": "form-demo-1",
|
|
7974
|
+
"type": "form-demo",
|
|
7975
|
+
"data": {
|
|
7976
|
+
"anchorId": "form-demo",
|
|
7977
|
+
"title": "Contattaci",
|
|
7978
|
+
"description": "Compila il modulo e ti risponderemo al più presto.",
|
|
7979
|
+
"recipientEmail": "test@olonjs.io",
|
|
7980
|
+
"submitLabel": "Invia",
|
|
7981
|
+
"successMessage": "Richiesta inviata con successo.",
|
|
7982
|
+
"icon": "mail"
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
],
|
|
7986
|
+
"global-header": false
|
|
7987
|
+
}
|
|
7988
|
+
END_OF_FILE_CONTENT
|
|
7665
7989
|
mkdir -p "src/emails"
|
|
7666
7990
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
7667
7991
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -8044,6 +8368,39 @@ function buildThemeCssFromSot(theme: ThemeConfig): string {
|
|
|
8044
8368
|
return `:root{${serialized}}`;
|
|
8045
8369
|
}
|
|
8046
8370
|
|
|
8371
|
+
function isRemoteStylesheetHref(value: string): boolean {
|
|
8372
|
+
return /^https?:\/\//i.test(value.trim());
|
|
8373
|
+
}
|
|
8374
|
+
|
|
8375
|
+
function extractLeadingRemoteCssImports(cssText: string): { hrefs: string[]; rest: string } {
|
|
8376
|
+
const hrefs = new Set<string>();
|
|
8377
|
+
const leadingTriviaPattern = /^(?:\s+|\/\*[\s\S]*?\*\/)*/;
|
|
8378
|
+
const importPattern =
|
|
8379
|
+
/^@import(?:\s+url\(\s*(?:'([^']+)'|"([^"]+)"|([^'")\s][^)]*))\s*\)|\s*(['"])([^'"]+)\4)\s*([^;]*);/i;
|
|
8380
|
+
let rest = cssText;
|
|
8381
|
+
|
|
8382
|
+
for (;;) {
|
|
8383
|
+
const trivia = rest.match(leadingTriviaPattern);
|
|
8384
|
+
if (trivia && trivia[0]) {
|
|
8385
|
+
rest = rest.slice(trivia[0].length);
|
|
8386
|
+
}
|
|
8387
|
+
|
|
8388
|
+
const match = rest.match(importPattern);
|
|
8389
|
+
if (!match) break;
|
|
8390
|
+
|
|
8391
|
+
const href = (match[1] ?? match[2] ?? match[3] ?? match[5] ?? '').trim();
|
|
8392
|
+
const trailingDirectives = (match[6] ?? '').trim();
|
|
8393
|
+
if (!isRemoteStylesheetHref(href) || trailingDirectives.length > 0) {
|
|
8394
|
+
break;
|
|
8395
|
+
}
|
|
8396
|
+
|
|
8397
|
+
hrefs.add(href);
|
|
8398
|
+
rest = rest.slice(match[0].length);
|
|
8399
|
+
}
|
|
8400
|
+
|
|
8401
|
+
return { hrefs: Array.from(hrefs), rest };
|
|
8402
|
+
}
|
|
8403
|
+
|
|
8047
8404
|
function resolveTenantId(): string {
|
|
8048
8405
|
const site: Record<string, unknown> = isRecord(siteConfig) ? siteConfig : {};
|
|
8049
8406
|
const identityRaw = site['identity'];
|
|
@@ -8099,8 +8456,13 @@ export function render(slug: string): string {
|
|
|
8099
8456
|
|
|
8100
8457
|
export function getCss(): string {
|
|
8101
8458
|
const themeCss = buildThemeCssFromSot(themeConfig);
|
|
8102
|
-
|
|
8103
|
-
return
|
|
8459
|
+
const { rest } = extractLeadingRemoteCssImports(tenantCss);
|
|
8460
|
+
if (!themeCss) return rest;
|
|
8461
|
+
return `${themeCss}\n${rest}`;
|
|
8462
|
+
}
|
|
8463
|
+
|
|
8464
|
+
export function getRemoteStylesheets(): string[] {
|
|
8465
|
+
return extractLeadingRemoteCssImports(tenantCss).hrefs;
|
|
8104
8466
|
}
|
|
8105
8467
|
|
|
8106
8468
|
export function getPageMeta(slug: string): { title: string; description: string } {
|
|
@@ -8127,6 +8489,7 @@ export function getWebMcpBuildState(): {
|
|
|
8127
8489
|
}
|
|
8128
8490
|
|
|
8129
8491
|
END_OF_FILE_CONTENT
|
|
8492
|
+
# SKIP: src/entry-ssg.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
8130
8493
|
echo "Creating src/fonts.css..."
|
|
8131
8494
|
cat << 'END_OF_FILE_CONTENT' > "src/fonts.css"
|
|
8132
8495
|
@import url('https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
@@ -9074,11 +9437,13 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
|
9074
9437
|
import type { SectionType } from '@/types';
|
|
9075
9438
|
import type { SectionComponentPropsMap } from '@/types';
|
|
9076
9439
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
9440
|
+
import { FormDemoView } from '@/components/form-demo';
|
|
9077
9441
|
|
|
9078
9442
|
export const ComponentRegistry: {
|
|
9079
9443
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
9080
9444
|
} = {
|
|
9081
9445
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
9446
|
+
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
9082
9447
|
};
|
|
9083
9448
|
|
|
9084
9449
|
END_OF_FILE_CONTENT
|
|
@@ -9096,10 +9461,11 @@ import {
|
|
|
9096
9461
|
X,
|
|
9097
9462
|
Sparkles,
|
|
9098
9463
|
Zap,
|
|
9464
|
+
Mail,
|
|
9099
9465
|
type LucideIcon
|
|
9100
9466
|
} from 'lucide-react';
|
|
9101
9467
|
|
|
9102
|
-
const iconMap = {
|
|
9468
|
+
export const iconMap = {
|
|
9103
9469
|
'layers': Layers,
|
|
9104
9470
|
'github': Github,
|
|
9105
9471
|
'arrow-right': ArrowRight,
|
|
@@ -9110,6 +9476,7 @@ const iconMap = {
|
|
|
9110
9476
|
'x': X,
|
|
9111
9477
|
'sparkles': Sparkles,
|
|
9112
9478
|
'zap': Zap,
|
|
9479
|
+
'mail': Mail,
|
|
9113
9480
|
} as const satisfies Record<string, LucideIcon>;
|
|
9114
9481
|
|
|
9115
9482
|
export type IconName = keyof typeof iconMap;
|
|
@@ -9139,15 +9506,46 @@ export const Icon: React.FC<IconProps> = ({ name, size = 20, className }) => {
|
|
|
9139
9506
|
|
|
9140
9507
|
|
|
9141
9508
|
|
|
9509
|
+
END_OF_FILE_CONTENT
|
|
9510
|
+
echo "Creating src/lib/OlonFormsContext.ts..."
|
|
9511
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/OlonFormsContext.ts"
|
|
9512
|
+
import { createContext, useContext } from 'react';
|
|
9513
|
+
|
|
9514
|
+
export type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
|
|
9515
|
+
|
|
9516
|
+
export interface FormState {
|
|
9517
|
+
status: FormStatus;
|
|
9518
|
+
message: string;
|
|
9519
|
+
}
|
|
9520
|
+
|
|
9521
|
+
const DEFAULT_FORM_STATE: FormState = { status: 'idle', message: '' };
|
|
9522
|
+
|
|
9523
|
+
/**
|
|
9524
|
+
* Context holding the submission state of every olon-managed form,
|
|
9525
|
+
* keyed by the form's id attribute (or anchorId).
|
|
9526
|
+
* Provided by App.tsx via useOlonForms().
|
|
9527
|
+
*/
|
|
9528
|
+
export const OlonFormsContext = createContext<Record<string, FormState>>({});
|
|
9529
|
+
|
|
9530
|
+
/**
|
|
9531
|
+
* Read the submission state for a specific form.
|
|
9532
|
+
* @param formId - must match the id attribute on the <form> element.
|
|
9533
|
+
*/
|
|
9534
|
+
export function useFormState(formId: string): FormState {
|
|
9535
|
+
const states = useContext(OlonFormsContext);
|
|
9536
|
+
return states[formId] ?? DEFAULT_FORM_STATE;
|
|
9537
|
+
}
|
|
9538
|
+
|
|
9142
9539
|
END_OF_FILE_CONTENT
|
|
9143
9540
|
echo "Creating src/lib/addSectionConfig.ts..."
|
|
9144
9541
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
9145
9542
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
9146
9543
|
|
|
9147
|
-
const addableSectionTypes = ['empty-tenant'] as const;
|
|
9544
|
+
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
9148
9545
|
|
|
9149
9546
|
const sectionTypeLabels: Record<string, string> = {
|
|
9150
9547
|
'empty-tenant': 'Empty Tenant',
|
|
9548
|
+
'form-demo': 'Form Demo',
|
|
9151
9549
|
};
|
|
9152
9550
|
|
|
9153
9551
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
@@ -9156,9 +9554,17 @@ function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
|
9156
9554
|
return {
|
|
9157
9555
|
title: 'Your tenant is empty.',
|
|
9158
9556
|
description: 'Create your first page to start building your site.',
|
|
9159
|
-
ctaLabel: 'Open Studio',
|
|
9160
9557
|
};
|
|
9161
|
-
|
|
9558
|
+
case 'form-demo':
|
|
9559
|
+
return {
|
|
9560
|
+
title: 'Contattaci',
|
|
9561
|
+
description: 'Compila il modulo e ti risponderemo al più presto.',
|
|
9562
|
+
recipientEmail: '',
|
|
9563
|
+
submitLabel: 'Invia',
|
|
9564
|
+
successMessage: 'Richiesta inviata con successo.',
|
|
9565
|
+
};
|
|
9566
|
+
default:
|
|
9567
|
+
return {};
|
|
9162
9568
|
}
|
|
9163
9569
|
}
|
|
9164
9570
|
|
|
@@ -9211,6 +9617,16 @@ export const CtaSchema = z.object({
|
|
|
9211
9617
|
variant: z.enum(['primary', 'secondary', 'accent']).default('primary').describe('ui:select'),
|
|
9212
9618
|
});
|
|
9213
9619
|
|
|
9620
|
+
/**
|
|
9621
|
+
* Mixin for any section capsule that includes a contact form.
|
|
9622
|
+
* Merge into the section data schema to expose recipientEmail
|
|
9623
|
+
* as an editable field in the Studio inspector.
|
|
9624
|
+
* The View must set data-olon-recipient={data.recipientEmail} on the <form>.
|
|
9625
|
+
*/
|
|
9626
|
+
export const WithFormRecipient = z.object({
|
|
9627
|
+
recipientEmail: z.string().optional().describe('ui:text'),
|
|
9628
|
+
});
|
|
9629
|
+
|
|
9214
9630
|
END_OF_FILE_CONTENT
|
|
9215
9631
|
echo "Creating src/lib/cloudSaveStream.ts..."
|
|
9216
9632
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
|
|
@@ -9461,9 +9877,11 @@ END_OF_FILE_CONTENT
|
|
|
9461
9877
|
echo "Creating src/lib/schemas.ts..."
|
|
9462
9878
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
9463
9879
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
9880
|
+
import { FormDemoSchema } from '@/components/form-demo';
|
|
9464
9881
|
|
|
9465
9882
|
export const SECTION_SCHEMAS = {
|
|
9466
9883
|
'empty-tenant': EmptyTenantSchema,
|
|
9884
|
+
'form-demo': FormDemoSchema,
|
|
9467
9885
|
} as const;
|
|
9468
9886
|
|
|
9469
9887
|
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
@@ -9565,6 +9983,137 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
9565
9983
|
|
|
9566
9984
|
return { submit, status, message, reset };
|
|
9567
9985
|
}
|
|
9986
|
+
END_OF_FILE_CONTENT
|
|
9987
|
+
echo "Creating src/lib/useOlonForms.ts..."
|
|
9988
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
9989
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
9990
|
+
import type { FormState } from './OlonFormsContext';
|
|
9991
|
+
|
|
9992
|
+
const API_BASE =
|
|
9993
|
+
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
9994
|
+
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
9995
|
+
|
|
9996
|
+
const API_KEY =
|
|
9997
|
+
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
9998
|
+
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
9999
|
+
|
|
10000
|
+
interface UseOlonFormsOptions {
|
|
10001
|
+
/** Override the submit endpoint. Defaults to VITE_OLONJS_CLOUD_URL/forms/submit */
|
|
10002
|
+
endpoint?: string;
|
|
10003
|
+
}
|
|
10004
|
+
|
|
10005
|
+
/**
|
|
10006
|
+
* Mount once in App.tsx. Scans the DOM for all <form data-olon-recipient="...">
|
|
10007
|
+
* elements and attaches submit handlers. Returns per-form states to be provided
|
|
10008
|
+
* via OlonFormsContext.Provider.
|
|
10009
|
+
*
|
|
10010
|
+
* Views consume state via useFormState(formId) — no direct coupling to this hook.
|
|
10011
|
+
*/
|
|
10012
|
+
export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<string, FormState> } {
|
|
10013
|
+
const [states, setStates] = useState<Record<string, FormState>>({});
|
|
10014
|
+
|
|
10015
|
+
const setFormState = useCallback((formId: string, state: FormState) => {
|
|
10016
|
+
setStates((prev) => ({ ...prev, [formId]: state }));
|
|
10017
|
+
}, []);
|
|
10018
|
+
|
|
10019
|
+
useEffect(() => {
|
|
10020
|
+
const resolvedBase = options?.endpoint
|
|
10021
|
+
? options.endpoint.replace(/\/$/, '')
|
|
10022
|
+
: API_BASE
|
|
10023
|
+
? API_BASE.replace(/\/$/, '')
|
|
10024
|
+
: null;
|
|
10025
|
+
|
|
10026
|
+
if (!resolvedBase || !API_KEY) {
|
|
10027
|
+
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
10028
|
+
return;
|
|
10029
|
+
}
|
|
10030
|
+
|
|
10031
|
+
const endpoint = resolvedBase.endsWith('/forms/submit')
|
|
10032
|
+
? resolvedBase
|
|
10033
|
+
: `${resolvedBase}/forms/submit`;
|
|
10034
|
+
|
|
10035
|
+
const forms = Array.from(
|
|
10036
|
+
document.querySelectorAll<HTMLFormElement>('form[data-olon-recipient]')
|
|
10037
|
+
);
|
|
10038
|
+
|
|
10039
|
+
const controllers: AbortController[] = [];
|
|
10040
|
+
|
|
10041
|
+
async function handleSubmit(form: HTMLFormElement, event: SubmitEvent) {
|
|
10042
|
+
event.preventDefault();
|
|
10043
|
+
|
|
10044
|
+
const formId = form.id || form.dataset.olonRecipient || 'olon-form';
|
|
10045
|
+
const recipientEmail = form.dataset.olonRecipient ?? '';
|
|
10046
|
+
|
|
10047
|
+
setFormState(formId, { status: 'submitting', message: 'Invio in corso...' });
|
|
10048
|
+
|
|
10049
|
+
const raw: Record<string, string> = {};
|
|
10050
|
+
new FormData(form).forEach((value, key) => {
|
|
10051
|
+
raw[key] = String(value).trim();
|
|
10052
|
+
});
|
|
10053
|
+
|
|
10054
|
+
const payload = {
|
|
10055
|
+
...raw,
|
|
10056
|
+
recipientEmail,
|
|
10057
|
+
page: window.location.pathname,
|
|
10058
|
+
source: 'olon-form',
|
|
10059
|
+
submittedAt: new Date().toISOString(),
|
|
10060
|
+
};
|
|
10061
|
+
|
|
10062
|
+
const controller = new AbortController();
|
|
10063
|
+
controllers.push(controller);
|
|
10064
|
+
|
|
10065
|
+
try {
|
|
10066
|
+
const response = await fetch(endpoint, {
|
|
10067
|
+
method: 'POST',
|
|
10068
|
+
headers: {
|
|
10069
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
10070
|
+
'Content-Type': 'application/json',
|
|
10071
|
+
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
10072
|
+
},
|
|
10073
|
+
body: JSON.stringify(payload),
|
|
10074
|
+
signal: controller.signal,
|
|
10075
|
+
});
|
|
10076
|
+
|
|
10077
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
10078
|
+
error?: string;
|
|
10079
|
+
code?: string;
|
|
10080
|
+
};
|
|
10081
|
+
|
|
10082
|
+
if (!response.ok) {
|
|
10083
|
+
throw new Error(body.error ?? body.code ?? `Submit failed (${response.status})`);
|
|
10084
|
+
}
|
|
10085
|
+
|
|
10086
|
+
setFormState(formId, {
|
|
10087
|
+
status: 'success',
|
|
10088
|
+
message: 'Richiesta inviata con successo.',
|
|
10089
|
+
});
|
|
10090
|
+
form.reset();
|
|
10091
|
+
} catch (error: unknown) {
|
|
10092
|
+
if (error instanceof Error && error.name === 'AbortError') return;
|
|
10093
|
+
const message =
|
|
10094
|
+
error instanceof Error ? error.message : 'Invio non riuscito. Riprova.';
|
|
10095
|
+
setFormState(formId, { status: 'error', message });
|
|
10096
|
+
}
|
|
10097
|
+
}
|
|
10098
|
+
|
|
10099
|
+
type Listener = { form: HTMLFormElement; handler: (e: Event) => void };
|
|
10100
|
+
const listeners: Listener[] = [];
|
|
10101
|
+
|
|
10102
|
+
forms.forEach((form) => {
|
|
10103
|
+
const handler = (e: Event) => void handleSubmit(form, e as SubmitEvent);
|
|
10104
|
+
form.addEventListener('submit', handler);
|
|
10105
|
+
listeners.push({ form, handler });
|
|
10106
|
+
});
|
|
10107
|
+
|
|
10108
|
+
return () => {
|
|
10109
|
+
controllers.forEach((c) => c.abort());
|
|
10110
|
+
listeners.forEach(({ form, handler }) => form.removeEventListener('submit', handler));
|
|
10111
|
+
};
|
|
10112
|
+
}, [options?.endpoint, setFormState]);
|
|
10113
|
+
|
|
10114
|
+
return { states };
|
|
10115
|
+
}
|
|
10116
|
+
|
|
9568
10117
|
END_OF_FILE_CONTENT
|
|
9569
10118
|
echo "Creating src/lib/utils.ts..."
|
|
9570
10119
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
|
|
@@ -9630,17 +10179,21 @@ mkdir -p "src/types"
|
|
|
9630
10179
|
echo "Creating src/types.ts..."
|
|
9631
10180
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
9632
10181
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
10182
|
+
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
9633
10183
|
|
|
9634
10184
|
export type SectionComponentPropsMap = {
|
|
9635
10185
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
10186
|
+
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
9636
10187
|
};
|
|
9637
10188
|
|
|
9638
10189
|
declare module '@olonjs/core' {
|
|
9639
10190
|
export interface SectionDataRegistry {
|
|
9640
10191
|
'empty-tenant': EmptyTenantData;
|
|
10192
|
+
'form-demo': FormDemoData;
|
|
9641
10193
|
}
|
|
9642
10194
|
export interface SectionSettingsRegistry {
|
|
9643
10195
|
'empty-tenant': EmptyTenantSettings;
|
|
10196
|
+
'form-demo': FormDemoSettings;
|
|
9644
10197
|
}
|
|
9645
10198
|
}
|
|
9646
10199
|
|