@olonjs/cli 3.0.128 → 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 +529 -87
- package/assets/templates/alpha/src_tenant.sh +529 -87
- 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"
|
|
@@ -2572,6 +2602,9 @@ import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
|
2572
2602
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
2573
2603
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
2574
2604
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
2605
|
+
import { useOlonForms } from '@/lib/useOlonForms';
|
|
2606
|
+
import { OlonFormsContext } from '@/lib/OlonFormsContext';
|
|
2607
|
+
import { iconMap } from '@/lib/IconResolver';
|
|
2575
2608
|
|
|
2576
2609
|
import tenantCss from './index.css?inline';
|
|
2577
2610
|
|
|
@@ -2966,6 +2999,7 @@ function setTenantPreviewReady(ready: boolean): void {
|
|
|
2966
2999
|
}
|
|
2967
3000
|
|
|
2968
3001
|
function App() {
|
|
3002
|
+
const { states: formStates } = useOlonForms();
|
|
2969
3003
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
2970
3004
|
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
2971
3005
|
const isHotSaveMode = isCloudMode && !isSave2RepoMode;
|
|
@@ -3413,6 +3447,7 @@ function App() {
|
|
|
3413
3447
|
themeConfig,
|
|
3414
3448
|
menuConfig,
|
|
3415
3449
|
refDocuments,
|
|
3450
|
+
iconRegistry: iconMap,
|
|
3416
3451
|
themeCss: { tenant: resolvedTenantCss },
|
|
3417
3452
|
addSection: addSectionConfig,
|
|
3418
3453
|
webmcp: {
|
|
@@ -3575,6 +3610,7 @@ function App() {
|
|
|
3575
3610
|
|
|
3576
3611
|
return (
|
|
3577
3612
|
<ThemeProvider>
|
|
3613
|
+
<OlonFormsContext.Provider value={formStates}>
|
|
3578
3614
|
<>
|
|
3579
3615
|
{isCloudMode && showTopProgress ? (
|
|
3580
3616
|
<>
|
|
@@ -3696,6 +3732,7 @@ function App() {
|
|
|
3696
3732
|
onRetry={retryCloudSave}
|
|
3697
3733
|
/>
|
|
3698
3734
|
</>
|
|
3735
|
+
</OlonFormsContext.Provider>
|
|
3699
3736
|
</ThemeProvider>
|
|
3700
3737
|
);
|
|
3701
3738
|
}
|
|
@@ -3706,90 +3743,89 @@ END_OF_FILE_CONTENT
|
|
|
3706
3743
|
mkdir -p "src/components"
|
|
3707
3744
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
3708
3745
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
|
|
3709
|
-
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3710
|
-
|
|
3711
|
-
type Theme = 'dark' | 'light'
|
|
3712
|
-
|
|
3713
|
-
interface ThemeContextValue {
|
|
3714
|
-
theme: Theme
|
|
3715
|
-
toggleTheme: () => void
|
|
3716
|
-
setTheme: (t: Theme) => void
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
const ThemeContext = createContext<ThemeContextValue>({
|
|
3720
|
-
theme: 'dark',
|
|
3721
|
-
toggleTheme: () => {},
|
|
3722
|
-
setTheme: () => {},
|
|
3723
|
-
})
|
|
3724
|
-
|
|
3725
|
-
const STORAGE_KEY = 'olon:theme'
|
|
3726
|
-
|
|
3727
|
-
function isTheme(value: unknown): value is Theme {
|
|
3728
|
-
return value === 'dark' || value === 'light'
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
function resolveInitialTheme(): Theme {
|
|
3732
|
-
if (typeof window === 'undefined') return 'dark'
|
|
3733
|
-
|
|
3734
|
-
const fromDom = document.documentElement.getAttribute('data-theme')
|
|
3735
|
-
if (isTheme(fromDom)) return fromDom
|
|
3736
|
-
|
|
3737
|
-
const fromStorage = window.localStorage.getItem(STORAGE_KEY)
|
|
3738
|
-
if (isTheme(fromStorage)) return fromStorage
|
|
3739
|
-
|
|
3740
|
-
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
|
|
3741
|
-
return prefersLight ? 'light' : 'dark'
|
|
3742
|
-
}
|
|
3743
|
-
|
|
3744
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
3745
|
-
const [theme, setThemeState] = useState<Theme>(resolveInitialTheme)
|
|
3746
|
-
|
|
3747
|
-
useEffect(() => {
|
|
3748
|
-
document.documentElement.setAttribute('data-theme', theme)
|
|
3749
|
-
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
3750
|
-
}, [theme])
|
|
3751
|
-
|
|
3752
|
-
function setTheme(t: Theme) {
|
|
3753
|
-
setThemeState(t)
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
function toggleTheme() {
|
|
3757
|
-
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
|
-
const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme])
|
|
3761
|
-
|
|
3762
|
-
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
export function useTheme() {
|
|
3766
|
-
return useContext(ThemeContext)
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
END_OF_FILE_CONTENT
|
|
3770
|
-
mkdir -p "src/components/empty-tenant"
|
|
3771
|
-
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
3772
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/View.tsx"
|
|
3773
|
-
import type { EmptyTenantData } from './types';
|
|
3746
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3774
3747
|
|
|
3775
|
-
type
|
|
3776
|
-
data?: EmptyTenantData;
|
|
3777
|
-
};
|
|
3748
|
+
type Theme = 'dark' | 'light'
|
|
3778
3749
|
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3750
|
+
interface ThemeContextValue {
|
|
3751
|
+
theme: Theme
|
|
3752
|
+
toggleTheme: () => void
|
|
3753
|
+
setTheme: (t: Theme) => void
|
|
3754
|
+
}
|
|
3782
3755
|
|
|
3756
|
+
const ThemeContext = createContext<ThemeContextValue>({
|
|
3757
|
+
theme: 'dark',
|
|
3758
|
+
toggleTheme: () => {},
|
|
3759
|
+
setTheme: () => {},
|
|
3760
|
+
})
|
|
3783
3761
|
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
<p className="mt-3 text-sm text-muted-foreground">{description}</p>
|
|
3789
|
-
</section>
|
|
3790
|
-
</main>
|
|
3791
|
-
);
|
|
3762
|
+
const STORAGE_KEY = 'olon:theme'
|
|
3763
|
+
|
|
3764
|
+
function isTheme(value: unknown): value is Theme {
|
|
3765
|
+
return value === 'dark' || value === 'light'
|
|
3792
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)
|
|
3804
|
+
}
|
|
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
|
+
}
|
|
3793
3829
|
|
|
3794
3830
|
END_OF_FILE_CONTENT
|
|
3795
3831
|
echo "Creating src/components/empty-tenant/index.ts..."
|
|
@@ -3807,7 +3843,6 @@ import { BaseSectionData } from '@/lib/base-schemas';
|
|
|
3807
3843
|
export const EmptyTenantSchema = BaseSectionData.extend({
|
|
3808
3844
|
title: z.string().optional().describe('ui:text'),
|
|
3809
3845
|
description: z.string().optional().describe('ui:textarea'),
|
|
3810
|
-
|
|
3811
3846
|
});
|
|
3812
3847
|
|
|
3813
3848
|
export const EmptyTenantSettingsSchema = z.object({});
|
|
@@ -3821,6 +3856,196 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
3821
3856
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
3822
3857
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
3823
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
|
+
|
|
3824
4049
|
END_OF_FILE_CONTENT
|
|
3825
4050
|
mkdir -p "src/components/save-drawer"
|
|
3826
4051
|
echo "Creating src/components/save-drawer/DeployConnector.tsx..."
|
|
@@ -7734,6 +7959,33 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
|
7734
7959
|
}
|
|
7735
7960
|
END_OF_FILE_CONTENT
|
|
7736
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
|
|
7737
7989
|
mkdir -p "src/emails"
|
|
7738
7990
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
7739
7991
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -9185,11 +9437,13 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
|
9185
9437
|
import type { SectionType } from '@/types';
|
|
9186
9438
|
import type { SectionComponentPropsMap } from '@/types';
|
|
9187
9439
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
9440
|
+
import { FormDemoView } from '@/components/form-demo';
|
|
9188
9441
|
|
|
9189
9442
|
export const ComponentRegistry: {
|
|
9190
9443
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
9191
9444
|
} = {
|
|
9192
9445
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
9446
|
+
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
9193
9447
|
};
|
|
9194
9448
|
|
|
9195
9449
|
END_OF_FILE_CONTENT
|
|
@@ -9207,10 +9461,11 @@ import {
|
|
|
9207
9461
|
X,
|
|
9208
9462
|
Sparkles,
|
|
9209
9463
|
Zap,
|
|
9464
|
+
Mail,
|
|
9210
9465
|
type LucideIcon
|
|
9211
9466
|
} from 'lucide-react';
|
|
9212
9467
|
|
|
9213
|
-
const iconMap = {
|
|
9468
|
+
export const iconMap = {
|
|
9214
9469
|
'layers': Layers,
|
|
9215
9470
|
'github': Github,
|
|
9216
9471
|
'arrow-right': ArrowRight,
|
|
@@ -9221,6 +9476,7 @@ const iconMap = {
|
|
|
9221
9476
|
'x': X,
|
|
9222
9477
|
'sparkles': Sparkles,
|
|
9223
9478
|
'zap': Zap,
|
|
9479
|
+
'mail': Mail,
|
|
9224
9480
|
} as const satisfies Record<string, LucideIcon>;
|
|
9225
9481
|
|
|
9226
9482
|
export type IconName = keyof typeof iconMap;
|
|
@@ -9250,15 +9506,46 @@ export const Icon: React.FC<IconProps> = ({ name, size = 20, className }) => {
|
|
|
9250
9506
|
|
|
9251
9507
|
|
|
9252
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
|
+
|
|
9253
9539
|
END_OF_FILE_CONTENT
|
|
9254
9540
|
echo "Creating src/lib/addSectionConfig.ts..."
|
|
9255
9541
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
9256
9542
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
9257
9543
|
|
|
9258
|
-
const addableSectionTypes = ['empty-tenant'] as const;
|
|
9544
|
+
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
9259
9545
|
|
|
9260
9546
|
const sectionTypeLabels: Record<string, string> = {
|
|
9261
9547
|
'empty-tenant': 'Empty Tenant',
|
|
9548
|
+
'form-demo': 'Form Demo',
|
|
9262
9549
|
};
|
|
9263
9550
|
|
|
9264
9551
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
@@ -9267,9 +9554,17 @@ function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
|
9267
9554
|
return {
|
|
9268
9555
|
title: 'Your tenant is empty.',
|
|
9269
9556
|
description: 'Create your first page to start building your site.',
|
|
9270
|
-
ctaLabel: 'Open Studio',
|
|
9271
9557
|
};
|
|
9272
|
-
|
|
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 {};
|
|
9273
9568
|
}
|
|
9274
9569
|
}
|
|
9275
9570
|
|
|
@@ -9322,6 +9617,16 @@ export const CtaSchema = z.object({
|
|
|
9322
9617
|
variant: z.enum(['primary', 'secondary', 'accent']).default('primary').describe('ui:select'),
|
|
9323
9618
|
});
|
|
9324
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
|
+
|
|
9325
9630
|
END_OF_FILE_CONTENT
|
|
9326
9631
|
echo "Creating src/lib/cloudSaveStream.ts..."
|
|
9327
9632
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
|
|
@@ -9572,9 +9877,11 @@ END_OF_FILE_CONTENT
|
|
|
9572
9877
|
echo "Creating src/lib/schemas.ts..."
|
|
9573
9878
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
9574
9879
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
9880
|
+
import { FormDemoSchema } from '@/components/form-demo';
|
|
9575
9881
|
|
|
9576
9882
|
export const SECTION_SCHEMAS = {
|
|
9577
9883
|
'empty-tenant': EmptyTenantSchema,
|
|
9884
|
+
'form-demo': FormDemoSchema,
|
|
9578
9885
|
} as const;
|
|
9579
9886
|
|
|
9580
9887
|
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
@@ -9676,6 +9983,137 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
9676
9983
|
|
|
9677
9984
|
return { submit, status, message, reset };
|
|
9678
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
|
+
|
|
9679
10117
|
END_OF_FILE_CONTENT
|
|
9680
10118
|
echo "Creating src/lib/utils.ts..."
|
|
9681
10119
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
|
|
@@ -9741,17 +10179,21 @@ mkdir -p "src/types"
|
|
|
9741
10179
|
echo "Creating src/types.ts..."
|
|
9742
10180
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
9743
10181
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
10182
|
+
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
9744
10183
|
|
|
9745
10184
|
export type SectionComponentPropsMap = {
|
|
9746
10185
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
10186
|
+
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
9747
10187
|
};
|
|
9748
10188
|
|
|
9749
10189
|
declare module '@olonjs/core' {
|
|
9750
10190
|
export interface SectionDataRegistry {
|
|
9751
10191
|
'empty-tenant': EmptyTenantData;
|
|
10192
|
+
'form-demo': FormDemoData;
|
|
9752
10193
|
}
|
|
9753
10194
|
export interface SectionSettingsRegistry {
|
|
9754
10195
|
'empty-tenant': EmptyTenantSettings;
|
|
10196
|
+
'form-demo': FormDemoSettings;
|
|
9755
10197
|
}
|
|
9756
10198
|
}
|
|
9757
10199
|
|