diff --git a/package.json b/package.json index 859a31a..61c186c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "nanoid": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.22.0", "react-hook-form": "^7.45.1", "react-toastify": "^9.1.3", "tailwindcss": "^3.3.2", diff --git a/src/App.jsx b/src/App.jsx index ee39f78..158d46d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,14 +1,17 @@ import React from 'react'; -import { DndContext } from '@dnd-kit/core'; -import FormBuilder from './components/FormBuilder/FormBuilder'; +import { Routes, Route } from 'react-router-dom'; import { FormProvider } from './context/FormContext'; +import Home from './pages/Home'; +import BuilderPage from './pages/BuilderPage'; function App() { return ( - - - + + } /> + } /> + } /> + ); } diff --git a/src/components/FormBuilder/FormBuilderHeader.jsx b/src/components/FormBuilder/FormBuilderHeader.jsx index 8b6e215..ba0d2f0 100644 --- a/src/components/FormBuilder/FormBuilderHeader.jsx +++ b/src/components/FormBuilder/FormBuilderHeader.jsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { useForm } from '../../context/FormContext'; import { toast } from 'react-toastify'; -import ComponentPreview from '../FormComponents/ComponentPreview'; +import { Link } from 'react-router-dom'; +import FormPreview from '../FormPreview/FormPreview'; const FormBuilderHeader = () => { @@ -12,6 +13,7 @@ const FormBuilderHeader = () => { importFormSchema, undo, redo, + saveFormToStorage, canUndo, canRedo } = useForm(); @@ -99,6 +101,10 @@ const FormBuilderHeader = () => { } }; + const handleSave = () => { + saveFormToStorage(); + }; + return ( @@ -112,6 +118,7 @@ const FormBuilderHeader = () => { + Home {isEditingTitle ? ( { - Save Form @@ -274,11 +282,7 @@ const FormBuilderHeader = () => { {JSON.stringify(formState.components, null, 2)} ) : ( - formState.components.map((component, idx) => ( - - - - )) + )} diff --git a/src/components/FormPreview/FormPreview.jsx b/src/components/FormPreview/FormPreview.jsx new file mode 100644 index 0000000..c73b399 --- /dev/null +++ b/src/components/FormPreview/FormPreview.jsx @@ -0,0 +1,292 @@ +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; + +const getValidationRules = (component) => { + const rules = {}; + const validate = component.validate || {}; + + if (component.required || validate.required) { + rules.required = 'This field is required'; + } + if (validate.minLength) { + rules.minLength = { value: parseInt(validate.minLength, 10), message: `Minimum length is ${validate.minLength}` }; + } + if (validate.maxLength) { + rules.maxLength = { value: parseInt(validate.maxLength, 10), message: `Maximum length is ${validate.maxLength}` }; + } + if (validate.min) { + rules.min = { value: parseFloat(validate.min), message: `Minimum value is ${validate.min}` }; + } + if (validate.max) { + rules.max = { value: parseFloat(validate.max), message: `Maximum value is ${validate.max}` }; + } + if (validate.pattern) { + try { + rules.pattern = { value: new RegExp(validate.pattern), message: 'Invalid format' }; + } catch { + // Ignore invalid regex patterns + } + } + + return rules; +}; + +const TabsContainer = ({ component, path, renderComponent }) => { + const [activeTab, setActiveTab] = useState(0); + const tabs = component.tabs || []; + const depth = path.split('.').length - 1; + + return ( + 0 ? 'ml-4' : ''}`}> + + {tabs.map((tab, idx) => ( + 0 ? 'text-xs' : 'text-sm'} font-medium border-r last:border-r-0 focus:outline-none ${ + idx === activeTab ? 'text-primary border-b-2 border-primary bg-white' : 'text-gray-700' + }`} + onClick={() => setActiveTab(idx)} + > + {tab.label || `Tab ${idx + 1}`} + + ))} + + + {tabs[activeTab] && (tabs[activeTab].components || []).map((child) => ( + + {renderComponent(child, path)} + + ))} + + + ); +}; + +const ColumnsContainer = ({ component, path, renderComponent }) => ( + + {(component.columns || []).map((column, idx) => ( + + {(column.components || []).map((child) => ( + {renderComponent(child, path)} + ))} + + ))} + +); + +const PanelContainer = ({ component, path, renderComponent }) => ( + + {component.title && ( + + {component.title} + + )} + + {(component.components || []).map((child) => ( + {renderComponent(child, path)} + ))} + + +); + +const FormPreview = ({ form }) => { + const { register, handleSubmit, formState: { errors }, reset } = useForm(); + const [submitted, setSubmitted] = useState(false); + + const onSubmit = (data) => { + setSubmitted(true); + console.log('Form data:', data); // eslint-disable-line no-console + reset(); + }; + + const renderComponent = (component, parentPath = '') => { + const name = parentPath ? `${parentPath}.${component.key}` : component.key; + + switch (component.type) { + case 'textfield': + case 'email': + case 'phoneNumber': + return ( + + + {component.label} + + + {component.description && {component.description}} + {errors[name] && {errors[name].message}} + + ); + case 'textarea': + return ( + + + {component.label} + + + {component.description && {component.description}} + {errors[name] && {errors[name].message}} + + ); + case 'number': + return ( + + + {component.label} + + + {component.description && {component.description}} + {errors[name] && {errors[name].message}} + + ); + case 'checkbox': + return ( + + + {component.label} + {errors[name] && {errors[name].message}} + + ); + case 'selectboxes': + return ( + + {component.label} + + {(component.values || []).map((option, idx) => ( + + + {option.label} + + ))} + + {errors[name] && {errors[name].message}} + + ); + case 'select': + return ( + + + {component.label} + + + {component.placeholder || 'Select...'} + {(component.data?.values || []).map((opt, idx) => ( + {opt.label} + ))} + + {component.description && {component.description}} + {errors[name] && {errors[name].message}} + + ); + case 'radio': + return ( + + {component.label} + + {(component.values || []).map((opt, idx) => ( + + + {opt.label} + + ))} + + {errors[name] && {errors[name].message}} + + ); + case 'datetime': + return ( + + + {component.label} + + + {component.description && {component.description}} + {errors[name] && {errors[name].message}} + + ); + case 'address': + return ( + + {component.label} + {Object.entries(component.components || {}).map(([key, cfg]) => ( + + ))} + {errors[name] && {errors[name].message}} + + ); + case 'content': + return ( + + ); + case 'panel': + return ; + case 'columns': + return ; + case 'tabs': + return ; + default: + return Unsupported component: {component.type}; + } + }; + + return ( + + {(form.components || []).map((comp) => ( + {renderComponent(comp)} + ))} + + {form?.settings?.submitButton?.text || 'Submit'} + + {submitted && ( + Form submitted successfully! + )} + + ); +}; + +export default FormPreview; diff --git a/src/context/FormContext.jsx b/src/context/FormContext.jsx index 9011d69..9826b16 100644 --- a/src/context/FormContext.jsx +++ b/src/context/FormContext.jsx @@ -14,8 +14,8 @@ export const useForm = () => { }; export const FormProvider = ({ children }) => { - // Form state with initial form structure - const [formState, setFormState] = useState({ + const createEmptyForm = () => ({ + id: nanoid(), title: 'Untitled Form', description: '', components: [], @@ -29,6 +29,9 @@ export const FormProvider = ({ children }) => { }, }); + // Form state with initial form structure + const [formState, setFormState] = useState(createEmptyForm()); + // History for undo/redo functionality const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); @@ -209,6 +212,42 @@ export const FormProvider = ({ children }) => { } }, [addToHistory]); + const saveFormToStorage = useCallback(() => { + const saved = JSON.parse(localStorage.getItem('savedForms') || '[]'); + const id = formState.id || nanoid(); + const updated = { ...formState, id }; + const idx = saved.findIndex(f => f.id === id); + const entry = { id, title: updated.title, schema: updated }; + if (idx >= 0) { + saved[idx] = entry; + } else { + saved.push(entry); + } + localStorage.setItem('savedForms', JSON.stringify(saved)); + setFormState(updated); + toast.success('Form saved'); + }, [formState]); + + const loadFormFromStorage = useCallback((id) => { + const saved = JSON.parse(localStorage.getItem('savedForms') || '[]'); + const entry = saved.find(f => f.id === id); + if (entry) { + setFormState(entry.schema); + setHistory([entry.schema]); + setHistoryIndex(0); + toast.success('Form loaded'); + } else { + toast.error('Form not found'); + } + }, []); + + const initializeNewForm = useCallback(() => { + const empty = createEmptyForm(); + setFormState(empty); + setHistory([empty]); + setHistoryIndex(0); + }, []); + // Helper function to create a new component based on type, with options override const createComponent = (type, options = {}) => { const baseComponent = { @@ -557,6 +596,9 @@ export const FormProvider = ({ children }) => { addComponentToTab, addComponentToColumn, addComponentToContainer, + saveFormToStorage, + loadFormFromStorage, + initializeNewForm, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1, }; diff --git a/src/main.jsx b/src/main.jsx index 12b9a8f..ae5abc1 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './styles/index.css'; import { ToastContainer } from 'react-toastify'; @@ -7,18 +8,20 @@ import 'react-toastify/dist/ReactToastify.css'; ReactDOM.createRoot(document.getElementById('root')).render( - - - -); \ No newline at end of file + + + + + , +); diff --git a/src/pages/BuilderPage.jsx b/src/pages/BuilderPage.jsx new file mode 100644 index 0000000..e3a1c51 --- /dev/null +++ b/src/pages/BuilderPage.jsx @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useForm } from '../context/FormContext'; +import FormBuilder from '../components/FormBuilder/FormBuilder'; + +const BuilderPage = () => { + const { id } = useParams(); + const { loadFormFromStorage, initializeNewForm } = useForm(); + + useEffect(() => { + if (id) { + loadFormFromStorage(id); + } else { + initializeNewForm(); + } + }, [id, loadFormFromStorage, initializeNewForm]); + + return ; +}; + +export default BuilderPage; diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx new file mode 100644 index 0000000..1bd43e8 --- /dev/null +++ b/src/pages/Home.jsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; + +const Home = () => { + const [forms, setForms] = useState([]); + + useEffect(() => { + const saved = JSON.parse(localStorage.getItem('savedForms') || '[]'); + setForms(saved); + }, []); + + return ( + + Saved Forms + {forms.length === 0 ? ( + No saved forms. + ) : ( + + {forms.map(form => ( + + {form.title} + Edit + + ))} + + )} + + Create New Form + + + ); +}; + +export default Home;
{component.description}
{errors[name].message}
Form submitted successfully!
No saved forms.