@lub-crm/forms 1.0.0
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/README.md +298 -0
- package/dist/lub-forms.css +1 -0
- package/dist/lub-forms.es.js +5848 -0
- package/dist/lub-forms.es.js.map +1 -0
- package/dist/lub-forms.standalone.js +10 -0
- package/dist/lub-forms.standalone.js.map +1 -0
- package/dist/lub-forms.umd.js +227 -0
- package/dist/lub-forms.umd.js.map +1 -0
- package/package.json +68 -0
- package/src/api/client.ts +115 -0
- package/src/api/index.ts +2 -0
- package/src/api/types.ts +202 -0
- package/src/core/FormProvider.tsx +228 -0
- package/src/core/FormRenderer.tsx +134 -0
- package/src/core/LubForm.tsx +476 -0
- package/src/core/StepManager.tsx +199 -0
- package/src/core/index.ts +4 -0
- package/src/embed.ts +188 -0
- package/src/fields/CheckboxField.tsx +62 -0
- package/src/fields/CheckboxGroupField.tsx +57 -0
- package/src/fields/CountryField.tsx +43 -0
- package/src/fields/DateField.tsx +33 -0
- package/src/fields/DateTimeField.tsx +33 -0
- package/src/fields/DividerField.tsx +16 -0
- package/src/fields/FieldWrapper.tsx +60 -0
- package/src/fields/FileField.tsx +45 -0
- package/src/fields/HiddenField.tsx +18 -0
- package/src/fields/HtmlField.tsx +17 -0
- package/src/fields/NumberField.tsx +39 -0
- package/src/fields/RadioField.tsx +57 -0
- package/src/fields/RecaptchaField.tsx +137 -0
- package/src/fields/SelectField.tsx +49 -0
- package/src/fields/StateField.tsx +84 -0
- package/src/fields/TextField.tsx +51 -0
- package/src/fields/TextareaField.tsx +37 -0
- package/src/fields/TimeField.tsx +33 -0
- package/src/fields/index.ts +84 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useConditionalLogic.ts +59 -0
- package/src/hooks/useFormApi.ts +118 -0
- package/src/hooks/useFormDesign.ts +48 -0
- package/src/hooks/useMultiStep.ts +98 -0
- package/src/index.ts +101 -0
- package/src/main.tsx +40 -0
- package/src/styles/index.css +707 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/countries.ts +163 -0
- package/src/utils/css-variables.ts +63 -0
- package/src/utils/index.ts +3 -0
- package/src/validation/conditional.ts +170 -0
- package/src/validation/index.ts +2 -0
- package/src/validation/schema-builder.ts +327 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { FormField } from "@/api/types";
|
|
3
|
+
import {
|
|
4
|
+
evaluateFieldVisibility,
|
|
5
|
+
evaluateFieldRequired,
|
|
6
|
+
getVisibleFields,
|
|
7
|
+
getRequiredFieldNames,
|
|
8
|
+
} from "@/validation/conditional";
|
|
9
|
+
|
|
10
|
+
interface UseConditionalLogicOptions {
|
|
11
|
+
fields: FormField[];
|
|
12
|
+
formValues: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseConditionalLogicReturn {
|
|
16
|
+
visibleFields: FormField[];
|
|
17
|
+
requiredFieldNames: Set<string>;
|
|
18
|
+
isFieldVisible: (fieldName: string) => boolean;
|
|
19
|
+
isFieldRequired: (fieldName: string) => boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useConditionalLogic({
|
|
23
|
+
fields,
|
|
24
|
+
formValues,
|
|
25
|
+
}: UseConditionalLogicOptions): UseConditionalLogicReturn {
|
|
26
|
+
const visibleFields = useMemo(
|
|
27
|
+
() => getVisibleFields(fields, formValues),
|
|
28
|
+
[fields, formValues],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const requiredFieldNames = useMemo(
|
|
32
|
+
() => getRequiredFieldNames(fields, formValues),
|
|
33
|
+
[fields, formValues],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const fieldMap = useMemo(
|
|
37
|
+
() => new Map(fields.map((f) => [f.name, f])),
|
|
38
|
+
[fields],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const isFieldVisible = (fieldName: string): boolean => {
|
|
42
|
+
const field = fieldMap.get(fieldName);
|
|
43
|
+
if (!field) return false;
|
|
44
|
+
return evaluateFieldVisibility(field, formValues);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isFieldRequired = (fieldName: string): boolean => {
|
|
48
|
+
const field = fieldMap.get(fieldName);
|
|
49
|
+
if (!field) return false;
|
|
50
|
+
return evaluateFieldRequired(field, formValues);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
visibleFields,
|
|
55
|
+
requiredFieldNames,
|
|
56
|
+
isFieldVisible,
|
|
57
|
+
isFieldRequired,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
PublicFormResponse,
|
|
4
|
+
SubmitFormRequest,
|
|
5
|
+
SubmitFormResponse,
|
|
6
|
+
ApiError,
|
|
7
|
+
} from "@/api/types";
|
|
8
|
+
import { LubFormsClient } from "@/api/client";
|
|
9
|
+
|
|
10
|
+
interface UseFormApiOptions {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
formId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UseFormApiReturn {
|
|
16
|
+
form: PublicFormResponse | null;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isSubmitting: boolean;
|
|
19
|
+
error: ApiError | null;
|
|
20
|
+
submitResponse: SubmitFormResponse | null;
|
|
21
|
+
fetchForm: () => Promise<void>;
|
|
22
|
+
submitForm: (data: Record<string, unknown>) => Promise<SubmitFormResponse>;
|
|
23
|
+
reset: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useFormApi({
|
|
27
|
+
baseUrl,
|
|
28
|
+
formId,
|
|
29
|
+
}: UseFormApiOptions): UseFormApiReturn {
|
|
30
|
+
const [form, setForm] = useState<PublicFormResponse | null>(null);
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
32
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
33
|
+
const [error, setError] = useState<ApiError | null>(null);
|
|
34
|
+
const [submitResponse, setSubmitResponse] =
|
|
35
|
+
useState<SubmitFormResponse | null>(null);
|
|
36
|
+
|
|
37
|
+
const client = new LubFormsClient(baseUrl);
|
|
38
|
+
|
|
39
|
+
const fetchForm = useCallback(async () => {
|
|
40
|
+
setIsLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const data = await client.getForm(formId);
|
|
45
|
+
setForm(data);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setError(err as ApiError);
|
|
48
|
+
} finally {
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, [formId, baseUrl]);
|
|
52
|
+
|
|
53
|
+
const submitForm = useCallback(
|
|
54
|
+
async (data: Record<string, unknown>): Promise<SubmitFormResponse> => {
|
|
55
|
+
setIsSubmitting(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const request: SubmitFormRequest = {
|
|
60
|
+
data,
|
|
61
|
+
referrer: typeof window !== "undefined" ? window.location.href : "",
|
|
62
|
+
utm_parameters: getUTMParameters(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const response = await client.submitForm(formId, request);
|
|
66
|
+
setSubmitResponse(response);
|
|
67
|
+
return response;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const apiError = err as ApiError;
|
|
70
|
+
setError(apiError);
|
|
71
|
+
throw apiError;
|
|
72
|
+
} finally {
|
|
73
|
+
setIsSubmitting(false);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[formId, baseUrl],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const reset = useCallback(() => {
|
|
80
|
+
setError(null);
|
|
81
|
+
setSubmitResponse(null);
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
form,
|
|
86
|
+
isLoading,
|
|
87
|
+
isSubmitting,
|
|
88
|
+
error,
|
|
89
|
+
submitResponse,
|
|
90
|
+
fetchForm,
|
|
91
|
+
submitForm,
|
|
92
|
+
reset,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getUTMParameters() {
|
|
97
|
+
if (typeof window === "undefined") return undefined;
|
|
98
|
+
|
|
99
|
+
const params = new URLSearchParams(window.location.search);
|
|
100
|
+
const utm: Record<string, string> = {};
|
|
101
|
+
|
|
102
|
+
const utmParams = [
|
|
103
|
+
"utm_source",
|
|
104
|
+
"utm_medium",
|
|
105
|
+
"utm_campaign",
|
|
106
|
+
"utm_term",
|
|
107
|
+
"utm_content",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
for (const param of utmParams) {
|
|
111
|
+
const value = params.get(param);
|
|
112
|
+
if (value) {
|
|
113
|
+
utm[param.replace("utm_", "")] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Object.keys(utm).length > 0 ? utm : undefined;
|
|
118
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useMemo, type CSSProperties } from "react";
|
|
2
|
+
import type { FormDesign } from "@/api/types";
|
|
3
|
+
import { formDesignToCssVariables } from "@/utils/css-variables";
|
|
4
|
+
|
|
5
|
+
interface UseFormDesignOptions {
|
|
6
|
+
design: FormDesign;
|
|
7
|
+
overrides?: Partial<FormDesign>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseFormDesignReturn {
|
|
11
|
+
mergedDesign: FormDesign;
|
|
12
|
+
cssVariables: Record<string, string>;
|
|
13
|
+
style: CSSProperties;
|
|
14
|
+
theme: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useFormDesign({
|
|
18
|
+
design,
|
|
19
|
+
overrides,
|
|
20
|
+
}: UseFormDesignOptions): UseFormDesignReturn {
|
|
21
|
+
const mergedDesign = useMemo(
|
|
22
|
+
(): FormDesign => ({
|
|
23
|
+
...design,
|
|
24
|
+
...overrides,
|
|
25
|
+
button_style: {
|
|
26
|
+
...design.button_style,
|
|
27
|
+
...overrides?.button_style,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
[design, overrides],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const cssVariables = useMemo(
|
|
34
|
+
() => formDesignToCssVariables(mergedDesign),
|
|
35
|
+
[mergedDesign],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const style = useMemo(() => cssVariables as CSSProperties, [cssVariables]);
|
|
39
|
+
|
|
40
|
+
const theme = mergedDesign.theme || "light";
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
mergedDesign,
|
|
44
|
+
cssVariables,
|
|
45
|
+
style,
|
|
46
|
+
theme,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import type { FormStep, FormField } from "@/api/types";
|
|
3
|
+
|
|
4
|
+
interface UseMultiStepOptions {
|
|
5
|
+
steps: FormStep[];
|
|
6
|
+
fields: FormField[];
|
|
7
|
+
onStepChange?: (step: number, total: number) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UseMultiStepReturn {
|
|
11
|
+
currentStep: number;
|
|
12
|
+
totalSteps: number;
|
|
13
|
+
isFirstStep: boolean;
|
|
14
|
+
isLastStep: boolean;
|
|
15
|
+
currentStepData: FormStep | null;
|
|
16
|
+
currentStepFields: FormField[];
|
|
17
|
+
progress: number;
|
|
18
|
+
goToStep: (step: number) => void;
|
|
19
|
+
nextStep: () => void;
|
|
20
|
+
prevStep: () => void;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useMultiStep({
|
|
25
|
+
steps,
|
|
26
|
+
fields,
|
|
27
|
+
onStepChange,
|
|
28
|
+
}: UseMultiStepOptions): UseMultiStepReturn {
|
|
29
|
+
const [currentStep, setCurrentStep] = useState(0);
|
|
30
|
+
|
|
31
|
+
const totalSteps = steps.length || 1;
|
|
32
|
+
const isFirstStep = currentStep === 0;
|
|
33
|
+
const isLastStep = currentStep >= totalSteps - 1;
|
|
34
|
+
|
|
35
|
+
const currentStepData = useMemo(
|
|
36
|
+
() => steps[currentStep] ?? null,
|
|
37
|
+
[steps, currentStep],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const currentStepFields = useMemo(() => {
|
|
41
|
+
if (!currentStepData) {
|
|
42
|
+
return fields.filter((f) => f.is_active);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stepFieldIds = new Set(currentStepData.field_ids);
|
|
46
|
+
return fields.filter((f) => f.is_active && stepFieldIds.has(f.id));
|
|
47
|
+
}, [currentStepData, fields]);
|
|
48
|
+
|
|
49
|
+
const progress = useMemo(() => {
|
|
50
|
+
if (totalSteps <= 1) return 100;
|
|
51
|
+
return Math.round(((currentStep + 1) / totalSteps) * 100);
|
|
52
|
+
}, [currentStep, totalSteps]);
|
|
53
|
+
|
|
54
|
+
const goToStep = useCallback(
|
|
55
|
+
(step: number) => {
|
|
56
|
+
if (step >= 0 && step < totalSteps) {
|
|
57
|
+
setCurrentStep(step);
|
|
58
|
+
onStepChange?.(step, totalSteps);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[totalSteps, onStepChange],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const nextStep = useCallback(() => {
|
|
65
|
+
if (!isLastStep) {
|
|
66
|
+
const newStep = currentStep + 1;
|
|
67
|
+
setCurrentStep(newStep);
|
|
68
|
+
onStepChange?.(newStep, totalSteps);
|
|
69
|
+
}
|
|
70
|
+
}, [currentStep, isLastStep, totalSteps, onStepChange]);
|
|
71
|
+
|
|
72
|
+
const prevStep = useCallback(() => {
|
|
73
|
+
if (!isFirstStep) {
|
|
74
|
+
const newStep = currentStep - 1;
|
|
75
|
+
setCurrentStep(newStep);
|
|
76
|
+
onStepChange?.(newStep, totalSteps);
|
|
77
|
+
}
|
|
78
|
+
}, [currentStep, isFirstStep, totalSteps, onStepChange]);
|
|
79
|
+
|
|
80
|
+
const reset = useCallback(() => {
|
|
81
|
+
setCurrentStep(0);
|
|
82
|
+
onStepChange?.(0, totalSteps);
|
|
83
|
+
}, [totalSteps, onStepChange]);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
currentStep,
|
|
87
|
+
totalSteps,
|
|
88
|
+
isFirstStep,
|
|
89
|
+
isLastStep,
|
|
90
|
+
currentStepData,
|
|
91
|
+
currentStepFields,
|
|
92
|
+
progress,
|
|
93
|
+
goToStep,
|
|
94
|
+
nextStep,
|
|
95
|
+
prevStep,
|
|
96
|
+
reset,
|
|
97
|
+
};
|
|
98
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Main ESM entry point for @lub/forms
|
|
2
|
+
|
|
3
|
+
// Core component
|
|
4
|
+
export { LubForm, type LubFormProps } from "./core/LubForm";
|
|
5
|
+
|
|
6
|
+
// Context and providers
|
|
7
|
+
export { LubFormProvider, useLubFormContext } from "./core/FormProvider";
|
|
8
|
+
|
|
9
|
+
// Rendering components
|
|
10
|
+
export { FormRenderer } from "./core/FormRenderer";
|
|
11
|
+
export { StepManager, StepNavigation } from "./core/StepManager";
|
|
12
|
+
|
|
13
|
+
// Field components
|
|
14
|
+
export {
|
|
15
|
+
getFieldComponent,
|
|
16
|
+
registerFieldComponent,
|
|
17
|
+
type FieldComponent,
|
|
18
|
+
FieldWrapper,
|
|
19
|
+
TextField,
|
|
20
|
+
TextareaField,
|
|
21
|
+
NumberField,
|
|
22
|
+
SelectField,
|
|
23
|
+
RadioField,
|
|
24
|
+
CheckboxField,
|
|
25
|
+
CheckboxGroupField,
|
|
26
|
+
DateField,
|
|
27
|
+
TimeField,
|
|
28
|
+
DateTimeField,
|
|
29
|
+
FileField,
|
|
30
|
+
HiddenField,
|
|
31
|
+
CountryField,
|
|
32
|
+
StateField,
|
|
33
|
+
HtmlField,
|
|
34
|
+
DividerField,
|
|
35
|
+
RecaptchaField,
|
|
36
|
+
} from "./fields";
|
|
37
|
+
|
|
38
|
+
// API client
|
|
39
|
+
export {
|
|
40
|
+
LubFormsClient,
|
|
41
|
+
createClient,
|
|
42
|
+
setDefaultClient,
|
|
43
|
+
getDefaultClient,
|
|
44
|
+
} from "./api/client";
|
|
45
|
+
|
|
46
|
+
// Types
|
|
47
|
+
export type {
|
|
48
|
+
FieldType,
|
|
49
|
+
FieldWidth,
|
|
50
|
+
Operator,
|
|
51
|
+
FormLayout,
|
|
52
|
+
ValidationRules,
|
|
53
|
+
SelectOption,
|
|
54
|
+
FieldOptions,
|
|
55
|
+
Condition,
|
|
56
|
+
ConditionalLogic,
|
|
57
|
+
CRMMapping,
|
|
58
|
+
FormField,
|
|
59
|
+
ButtonStyle,
|
|
60
|
+
FormDesign,
|
|
61
|
+
FormStep,
|
|
62
|
+
PublicFormSettings,
|
|
63
|
+
PublicFormResponse,
|
|
64
|
+
UTMParameters,
|
|
65
|
+
SubmitFormRequest,
|
|
66
|
+
SubmitFormResponse,
|
|
67
|
+
ApiError,
|
|
68
|
+
} from "./api/types";
|
|
69
|
+
|
|
70
|
+
// Validation
|
|
71
|
+
export { buildFormSchema, buildFieldSchema } from "./validation/schema-builder";
|
|
72
|
+
export {
|
|
73
|
+
evaluateFieldVisibility,
|
|
74
|
+
evaluateFieldRequired,
|
|
75
|
+
evaluateCondition,
|
|
76
|
+
getVisibleFields,
|
|
77
|
+
getRequiredFieldNames,
|
|
78
|
+
} from "./validation/conditional";
|
|
79
|
+
|
|
80
|
+
// Hooks
|
|
81
|
+
export { useFormApi } from "./hooks/useFormApi";
|
|
82
|
+
export { useConditionalLogic } from "./hooks/useConditionalLogic";
|
|
83
|
+
export { useMultiStep } from "./hooks/useMultiStep";
|
|
84
|
+
export { useFormDesign } from "./hooks/useFormDesign";
|
|
85
|
+
|
|
86
|
+
// Utilities
|
|
87
|
+
export { cn } from "./utils/cn";
|
|
88
|
+
export {
|
|
89
|
+
formDesignToCssVariables,
|
|
90
|
+
applyCssVariables,
|
|
91
|
+
} from "./utils/css-variables";
|
|
92
|
+
export {
|
|
93
|
+
countries,
|
|
94
|
+
usStates,
|
|
95
|
+
caProvinces,
|
|
96
|
+
getStatesForCountry,
|
|
97
|
+
getCountryByCode,
|
|
98
|
+
getStateByCode,
|
|
99
|
+
type Country,
|
|
100
|
+
type State,
|
|
101
|
+
} from "./utils/countries";
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development entry point
|
|
3
|
+
* This file is used for local development with vite dev server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StrictMode } from "react";
|
|
7
|
+
import { createRoot } from "react-dom/client";
|
|
8
|
+
import { LubForm } from "./core/LubForm";
|
|
9
|
+
import "./styles/index.css";
|
|
10
|
+
|
|
11
|
+
// Mock API response for development
|
|
12
|
+
const MOCK_FORM_ID = "demo-123";
|
|
13
|
+
const MOCK_BASE_URL = "http://localhost:8080"; // Your local API server
|
|
14
|
+
|
|
15
|
+
function App() {
|
|
16
|
+
return (
|
|
17
|
+
<div style={{ maxWidth: 600, margin: "40px auto", padding: 20 }}>
|
|
18
|
+
<h1 style={{ marginBottom: 24 }}>Lub Forms Development</h1>
|
|
19
|
+
<LubForm
|
|
20
|
+
formId={MOCK_FORM_ID}
|
|
21
|
+
baseUrl={MOCK_BASE_URL}
|
|
22
|
+
onSuccess={(data) => {
|
|
23
|
+
console.log("Form submitted successfully:", data);
|
|
24
|
+
}}
|
|
25
|
+
onError={(error) => {
|
|
26
|
+
console.error("Form error:", error);
|
|
27
|
+
}}
|
|
28
|
+
onStepChange={(step, total) => {
|
|
29
|
+
console.log(`Step ${step + 1} of ${total}`);
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createRoot(document.getElementById("root")!).render(
|
|
37
|
+
<StrictMode>
|
|
38
|
+
<App />
|
|
39
|
+
</StrictMode>,
|
|
40
|
+
);
|