+
+
+ }
+ />
+ {/* Public form submission route */}
+ } />
+ {/* Public form edit route */}
+ } />
diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js
index a5090cb..479ca43 100644
--- a/frontend/src/api/api.js
+++ b/frontend/src/api/api.js
@@ -262,4 +262,62 @@ export const profileAPI = {
api.put('/api/profile', profileData)
};
+export const formsAPI = {
+ createForm: async (formData) => {
+ const response = await api.post('/api/forms', formData);
+ return response.data;
+ },
+
+ getUserForms: async () => {
+ const response = await api.get('/api/forms/my-forms');
+ // Handle both old and new response format
+ return response.data.data || response.data;
+ },
+
+ getFormForManage: async (formUrl) => {
+ const response = await api.get(`/api/forms/manage/${formUrl}`);
+ return response.data;
+ },
+
+ getFormForEdit: async (formId) => {
+ const response = await api.get(`/api/forms/edit/${formId}`);
+ return response.data;
+ },
+
+ getFormForDisplay: async (formUrl) => {
+ const response = await api.get(`/api/forms/display/${formUrl}`);
+ return response.data;
+ },
+
+ updateForm: async (formId, formData) => {
+ const response = await api.put(`/api/forms/${formId}`, formData);
+ return response.data;
+ },
+
+ deleteForm: async (formId) => {
+ const response = await api.delete(`/api/forms/${formId}`);
+ return response.data;
+ },
+
+ submitFormResponse: async (formUrl, responseData) => {
+ const response = await api.post(`/api/forms/submit/${formUrl}`, responseData);
+ return response.data;
+ },
+
+ updateFormResponse: async (formUrl, responseData) => {
+ const response = await api.put(`/api/forms/submit/${formUrl}`, responseData);
+ return response.data;
+ },
+
+ getFormResponses: async (formUrl, credentials) => {
+ const response = await api.post(`/api/forms/responses/${formUrl}`, credentials);
+ return response.data;
+ },
+
+ getFormResponsesById: async (formId) => {
+ const response = await api.get(`/api/forms/responses/${formId}`);
+ return response.data;
+ }
+};
+
export default api;
diff --git a/frontend/src/components/FormBuilder.jsx b/frontend/src/components/FormBuilder.jsx
new file mode 100644
index 0000000..2ff6304
--- /dev/null
+++ b/frontend/src/components/FormBuilder.jsx
@@ -0,0 +1,489 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+import FormFieldEditor from './FormFieldEditor';
+import FormPreview from './FormPreview';
+import Button from './Button';
+
+const FormBuilder = ({ form = null, onSave, onCancel }) => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+
+ // Redirect if not authenticated
+ useEffect(() => {
+ if (!isAuthenticated) {
+ navigate('/');
+ return;
+ }
+ }, [isAuthenticated, navigate]);
+
+ const [formData, setFormData] = useState({
+ title: '',
+ description: '',
+ fields: [],
+ contributors: [],
+ isEditable: false
+ });
+ const [isPreviewMode, setIsPreviewMode] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [activeFieldId, setActiveFieldId] = useState(null);
+ const [draggedField, setDraggedField] = useState(null);
+
+ // Load existing form data if editing
+ useEffect(() => {
+ if (form) {
+ setFormData({
+ title: form.title || '',
+ description: form.description || '',
+ fields: form.fields || [],
+ contributors: form.contributors || [],
+ isEditable: form.isEditable || false
+ });
+ }
+ }, [form]);
+
+ // Don't render if not authenticated
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ // Add new field
+ const addField = (fieldType) => {
+ const newField = {
+ id: `temp_${Date.now()}`,
+ label: `New ${fieldType.replace('_', ' ')} Field`,
+ fieldType,
+ position: formData.fields.length + 1,
+ isRequired: false,
+ placeholder: '',
+ helpText: '',
+ options: ['MULTIPLE_CHOICE', 'DROPDOWN', 'CHECKBOX', 'SINGLE_CHOICE'].includes(fieldType) ?
+ [{ label: 'Option 1', value: 'option1' }, { label: 'Option 2', value: 'option2' }] : null,
+ validation: null,
+ conditions: null
+ };
+
+ setFormData(prev => ({
+ ...prev,
+ fields: [...prev.fields, newField]
+ }));
+ setActiveFieldId(newField.id);
+ };
+
+ // Update field
+ const updateField = (fieldId, updatedField) => {
+ setFormData(prev => ({
+ ...prev,
+ fields: prev.fields.map(field =>
+ field.id === fieldId ? { ...field, ...updatedField } : field
+ )
+ }));
+ };
+
+ // Delete field
+ const deleteField = (fieldId) => {
+ setFormData(prev => ({
+ ...prev,
+ fields: prev.fields.filter(field => field.id !== fieldId)
+ .map((field, index) => ({ ...field, position: index + 1 }))
+ }));
+ setActiveFieldId(null);
+ };
+
+ // Duplicate field
+ const duplicateField = (fieldId) => {
+ const fieldToDuplicate = formData.fields.find(f => f.id === fieldId);
+ if (fieldToDuplicate) {
+ const duplicatedField = {
+ ...fieldToDuplicate,
+ id: `temp_${Date.now()}`,
+ label: `Copy of ${fieldToDuplicate.label}`,
+ position: formData.fields.length + 1
+ };
+
+ setFormData(prev => ({
+ ...prev,
+ fields: [...prev.fields, duplicatedField]
+ }));
+ }
+ };
+
+ // Handle drag start
+ const handleDragStart = (e, fieldId) => {
+ setDraggedField(fieldId);
+ e.dataTransfer.effectAllowed = 'move';
+ };
+
+ // Handle drag over
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ };
+
+ // Handle drop
+ const handleDrop = (e, targetFieldId) => {
+ e.preventDefault();
+
+ if (!draggedField || draggedField === targetFieldId) return;
+
+ const fields = [...formData.fields];
+ const draggedIndex = fields.findIndex(f => f.id === draggedField);
+ const targetIndex = fields.findIndex(f => f.id === targetFieldId);
+
+ if (draggedIndex === -1 || targetIndex === -1) return;
+
+ // Remove dragged item and insert at target position
+ const [draggedItem] = fields.splice(draggedIndex, 1);
+ fields.splice(targetIndex, 0, draggedItem);
+
+ // Update positions
+ const updatedFields = fields.map((field, index) => ({
+ ...field,
+ position: index + 1
+ }));
+
+ setFormData(prev => ({ ...prev, fields: updatedFields }));
+ setDraggedField(null);
+ };
+
+ // Save form - Connected to backend API
+ const handleSave = async () => {
+ if (!formData.title.trim()) {
+ alert('Please enter a form title');
+ return;
+ }
+
+ setIsSaving(true);
+ try {
+ // Prepare data for backend
+ const formPayload = {
+ title: formData.title,
+ description: formData.description,
+ isEditable: formData.isEditable,
+ fields: formData.fields.map(field => ({
+ label: field.label,
+ fieldType: field.fieldType,
+ isRequired: field.isRequired || false,
+ allowMultiple: field.allowMultiple || false,
+ options: field.options ? JSON.stringify(field.options) : null,
+ position: field.position,
+ placeholder: field.placeholder || null,
+ helpText: field.helpText || null,
+ validation: field.validation ? JSON.stringify(field.validation) : null,
+ conditions: field.conditions ? JSON.stringify(field.conditions) : null
+ })),
+ contributors: formData.contributors || []
+ };
+
+ let result;
+ if (form) {
+ // Update existing form
+ result = await formsAPI.updateForm(form.id, formPayload);
+ alert('Form updated successfully!');
+ } else {
+ // Create new form
+ result = await formsAPI.createForm(formPayload);
+ alert(`Form created successfully! ${result.data ? `Form URL: ${result.data.fullUrl}` : ''}`);
+ // Redirect to forms list after creation
+ navigate('/forms');
+ }
+
+ onSave && onSave(result.data || formData);
+ } catch (error) {
+ console.error('Error saving form:', error);
+ // Display more specific error message
+ const errorMsg = error.response?.data?.message || error.message || 'Failed to save form';
+ alert(`Error: ${errorMsg}`);
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const fieldTypes = [
+ { type: 'TEXT', label: 'Text', description: 'Single line text input' },
+ { type: 'NUMBER', label: 'Number', description: 'Numeric input' },
+ { type: 'EMAIL', label: 'Email', description: 'Email address input' },
+ { type: 'MULTIPLE_CHOICE', label: 'Multiple Choice', description: 'Single selection from options' },
+ { type: 'CHECKBOX', label: 'Checkboxes', description: 'Multiple selections' },
+ { type: 'SINGLE_CHOICE', label: 'Single Choice', description: 'Radio button selection' },
+ { type: 'FILE', label: 'File Upload', description: 'File attachment' },
+ { type: 'DATE', label: 'Date', description: 'Date picker' },
+ { type: 'STAR_RATING', label: 'Star Rating', description: 'Star rating scale' },
+ { type: 'DROPDOWN', label: 'Dropdown', description: 'Dropdown selection' }
+ ];
+
+ if (isPreviewMode) {
+ return (
+
+ {/* Fixed Header Bar for Preview */}
+
+
+
+
+
+
+ Form Preview
+
+
See how your form will look to respondents
+
+
+
setIsPreviewMode(false)}
+ variant="secondary"
+ className="flex items-center space-x-2"
+ >
+ ←
+ Back to Editor
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {form ? 'Edit Form' : 'Create New Form'}
+
+
+ Build your form by adding fields and customizing their properties
+
+
+
+
+ setIsPreviewMode(true)}
+ variant="secondary"
+ className="px-4 py-2"
+ >
+ Preview
+
+
+ {isSaving ? 'Saving...' : 'Save Form'}
+
+ {onCancel && (
+
+ Cancel
+
+ )}
+
+
+
+
+
+ {/* Main Content with top margin to account for fixed header */}
+
+
+
+ {/* Field Types Sidebar */}
+
+
+
+
+ Form Elements
+
+
+
+
+ {fieldTypes.map((fieldType) => (
+
addField(fieldType.type)}
+ className="w-full group flex items-center space-x-3 p-3 text-left border border-gray-200 rounded-lg hover:border-black hover:bg-gray-50 transition-all duration-200 hover:shadow-sm"
+ >
+
+
+ {fieldType.label}
+
+
+ {fieldType.description}
+
+
+
+ ))}
+
+
+
+
+
+ {/* Form Builder */}
+
+
+ {/* Form Header Section */}
+
+
+ setFormData(prev => ({ ...prev, title: e.target.value }))}
+ className="w-full text-2xl font-semibold text-black placeholder-gray-400 border-none outline-none focus:bg-gray-50 p-3 rounded-lg transition-colors"
+ />
+
+
+
+ {/* Form Fields Section */}
+
+ {formData.fields.length > 0 ? (
+
+ {formData.fields.map((field, index) => (
+
handleDragStart(e, field.id)}
+ onDragOver={handleDragOver}
+ onDrop={(e) => handleDrop(e, field.id)}
+ className={`transition-all duration-200 ${
+ draggedField === field.id ? 'opacity-50 scale-95' : ''
+ }`}
+ >
+ updateField(field.id, updatedField)}
+ onDelete={() => deleteField(field.id)}
+ onDuplicate={() => duplicateField(field.id)}
+ onFocus={() => setActiveFieldId(field.id)}
+ onBlur={() => setActiveFieldId(null)}
+ />
+
+ ))}
+
+ ) : (
+
+
📝
+
+ Start building your form
+
+
+ Click on a field type from the sidebar to add your first question
+
+
+ 💡
+ Tip: Drag fields to reorder them
+
+
+ )}
+
+
+
+
+ {/* Properties Panel */}
+
+
+
+
+ Form Settings
+
+
+
+
+
+
+ Total Fields
+ {formData.fields.length}
+
+
+
+
+
+ Required Fields
+
+ {formData.fields.filter(f => f.isRequired).length}
+
+
+
+
+
+
+ {/* Form Settings */}
+
+
+ Form Settings
+
+
+
+ setFormData(prev => ({ ...prev, isEditable: e.target.checked }))}
+ className="rounded border-gray-300 text-black focus:ring-black focus:ring-offset-0 focus:ring-1"
+ />
+ Allow response editing
+
+
+ Users can edit their responses after submission
+
+
+
+
+
+
+ Created By
+
+
+ {user?.username || user?.email || 'Unknown User'}
+
+
+
+ {form && form.formUrl && (
+
+
+ Form URL
+
+
+ /form/{form.formUrl}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FormBuilder;
\ No newline at end of file
diff --git a/frontend/src/components/FormEdit.jsx b/frontend/src/components/FormEdit.jsx
new file mode 100644
index 0000000..5bede71
--- /dev/null
+++ b/frontend/src/components/FormEdit.jsx
@@ -0,0 +1,286 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+import FormPreview from './FormPreview';
+
+const FormEdit = () => {
+ const { formUrl } = useParams();
+ const navigate = useNavigate();
+
+ const [form, setForm] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [editCode, setEditCode] = useState('');
+ const [showCodeInput, setShowCodeInput] = useState(true);
+ const [existingResponse, setExistingResponse] = useState(null);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [isUpdated, setIsUpdated] = useState(false);
+
+ // Load form data
+ useEffect(() => {
+ const loadForm = async () => {
+ try {
+ const response = await formsAPI.getFormForDisplay(formUrl);
+ const formData = response.form || response;
+
+ if (!formData.isEditable) {
+ setError('This form does not allow editing responses.');
+ return;
+ }
+
+ setForm(formData);
+ } catch (err) {
+ console.error('Error loading form:', err);
+ setError('Form not found or no longer available');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (formUrl) {
+ loadForm();
+ }
+ }, [formUrl]);
+
+ // Load existing response with edit code
+ const handleLoadResponse = async () => {
+ if (!editCode.trim()) {
+ alert('Please enter your edit code');
+ return;
+ }
+
+ setLoading(true);
+ try {
+ const response = await formsAPI.getFormResponses(formUrl, {
+ anonymousId: editCode.trim()
+ });
+
+ if (response.responseWise && response.responseWise.length > 0) {
+ setExistingResponse(response.responseWise[0]);
+ setShowCodeInput(false);
+ } else {
+ alert('No response found with this edit code. Please check your code and try again.');
+ }
+ } catch (error) {
+ console.error('Error loading response:', error);
+ alert('Failed to load your response. Please check your edit code and try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Update existing response
+ const handleUpdate = async (responses) => {
+ if (!existingResponse) return;
+
+ setIsUpdating(true);
+ try {
+ // Convert responses to the format expected by backend
+ const answers = responses.map(({ fieldId, value }) => {
+ // Handle arrays (for CHECKBOX, MULTIPLE_CHOICE)
+ if (Array.isArray(value)) {
+ return {
+ fieldId: fieldId.toString(), // Ensure it's a string
+ answerJson: value,
+ answerValue: null
+ };
+ }
+ // Handle single values (for TEXT, DROPDOWN, etc.)
+ return {
+ fieldId: fieldId.toString(), // Ensure it's a string
+ answerValue: value,
+ answerJson: null
+ };
+ });
+
+ await formsAPI.updateFormResponse(formUrl, {
+ answers,
+ anonymousId: editCode
+ });
+
+ setIsUpdated(true);
+ } catch (error) {
+ console.error('Error updating response:', error);
+ throw error; // Re-throw to be handled by FormPreview
+ } finally {
+ setIsUpdating(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️
+
+ Cannot Edit Form
+
+
+ {error}
+
+
navigate(`/form/${formUrl}`)}
+ className="px-6 py-3 bg-black text-white rounded-xl hover:bg-gray-800 transition-colors font-medium"
+ >
+ Back to Form
+
+
+
+ );
+ }
+
+ if (isUpdated) {
+ return (
+
+
+
✅
+
+ Response Updated!
+
+
+ Your response has been successfully updated.
+
+
+ window.location.reload()}
+ className="px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors font-medium"
+ >
+ Edit Again
+
+ navigate(`/form/${formUrl}`)}
+ className="px-6 py-3 bg-gray-600 text-white rounded-xl hover:bg-gray-700 transition-colors font-medium"
+ >
+ Back to Form
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {form?.title || 'Edit Form Response'}
+
+
+ {showCodeInput
+ ? 'Enter your edit code to modify your previous response.'
+ : 'Update your response and submit the changes.'
+ }
+
+
+
+ {showCodeInput ? (
+ /* Edit Code Input */
+
+
+
+
🔑
+
+ Enter Your Edit Code
+
+
+ Use the code provided when you first submitted this form
+
+
+
+
+
+
+ Edit Code
+
+ setEditCode(e.target.value.toUpperCase())}
+ placeholder="Enter your edit code (e.g. ABC123)"
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:border-black focus:ring-1 focus:ring-black focus:outline-none transition-all text-center font-mono text-lg tracking-wider"
+ maxLength={20}
+ />
+
+
+
+ {loading ? 'Loading...' : 'Load My Response'}
+
+
+
+
+
+ Don't have an edit code? You can only edit responses if the form creator enabled editing when you first submitted.
+
+
+
+
+ ) : (
+ /* Form Preview for Editing */
+
+
+
+
+ 📝 Editing response from {new Date(existingResponse?.submittedAt).toLocaleDateString()}
+
+
+
+
+
+
+
+ {
+ setShowCodeInput(true);
+ setExistingResponse(null);
+ }}
+ className="px-4 py-2 text-gray-600 hover:text-black transition-colors"
+ >
+ ← Use Different Edit Code
+
+
+
+ )}
+
+ {/* Footer Info */}
+
+
+ This form editing is powered by Orbis Forms.
+ •
+ Your data is secure and protected.
+
+
+
+
+ );
+};
+
+export default FormEdit;
\ No newline at end of file
diff --git a/frontend/src/components/FormFieldEditor.jsx b/frontend/src/components/FormFieldEditor.jsx
new file mode 100644
index 0000000..9e31f03
--- /dev/null
+++ b/frontend/src/components/FormFieldEditor.jsx
@@ -0,0 +1,391 @@
+import React, { useState } from 'react';
+import Button from './Button';
+
+
+const getFieldTypeName = (fieldType) => {
+ const names = {
+ 'TEXT': 'Text',
+ 'NUMBER': 'Number',
+ 'EMAIL': 'Email',
+ 'MULTIPLE_CHOICE': 'Multiple Choice',
+ 'CHECKBOX': 'Checkboxes',
+ 'SINGLE_CHOICE': 'Single Choice',
+ 'FILE': 'File Upload',
+ 'DATE': 'Date',
+ 'STAR_RATING': 'Star Rating',
+ 'DROPDOWN': 'Dropdown'
+ };
+ return names[fieldType] || fieldType.replace('_', ' ');
+};
+
+const FormFieldEditor = ({
+ field,
+ isActive,
+ onUpdate,
+ onDelete,
+ onDuplicate,
+ onFocus,
+ onBlur
+}) => {
+ const [showValidation, setShowValidation] = useState(false);
+ const [showConditions, setShowConditions] = useState(false);
+
+ const handleFieldChange = (property, value) => {
+ onUpdate({ [property]: value });
+ };
+
+ const handleOptionChange = (index, property, value) => {
+ const newOptions = [...(field.options || [])];
+ newOptions[index] = { ...newOptions[index], [property]: value };
+ handleFieldChange('options', newOptions);
+ };
+
+ const addOption = () => {
+ const newOptions = [...(field.options || [])];
+ newOptions.push({
+ label: `Option ${newOptions.length + 1}`,
+ value: `option${newOptions.length + 1}`
+ });
+ handleFieldChange('options', newOptions);
+ };
+
+ const removeOption = (index) => {
+ const newOptions = field.options.filter((_, i) => i !== index);
+ handleFieldChange('options', newOptions);
+ };
+
+ const renderFieldPreview = () => {
+ const baseInputClass = "w-full p-3 border border-gray-200 rounded-lg focus:border-black focus:outline-none transition-colors";
+
+ switch (field.fieldType) {
+ case 'TEXT':
+ return (
+
+ );
+
+ case 'EMAIL':
+ return (
+
+ );
+
+ case 'NUMBER':
+ return (
+
+ );
+
+ case 'DATE':
+ return (
+
+ );
+
+ case 'MULTIPLE_CHOICE':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'CHECKBOX':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'SINGLE_CHOICE':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'DROPDOWN':
+ return (
+
+ Choose an option...
+ {(field.options || []).map((option, index) => (
+
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'FILE':
+ return (
+
+
Click or drag files here to upload
+
Max file size: 10MB
+
+ );
+
+ case 'STAR_RATING':
+ return (
+
+ {[1, 2, 3, 4, 5].map((star) => (
+
+ ⭐
+
+ ))}
+
+ );
+
+ default:
+ return (
+
+ );
+ }
+ };
+
+ return (
+
+ {/* Field Header */}
+
+
+
+
+
+ {getFieldTypeName(field.fieldType)}
+
+
+
+
+ {isActive && (
+
+
{
+ e.stopPropagation();
+ onDuplicate();
+ }}
+ className="p-2 text-gray-500 hover:text-blue-600 hover:bg-white rounded-md transition-colors"
+ title="Duplicate field"
+ >
+
+
+
+
+
+
{
+ e.stopPropagation();
+ onDelete();
+ }}
+ className="p-2 text-gray-500 hover:text-red-600 hover:bg-white rounded-md transition-colors"
+ title="Delete field"
+ >
+
+
+
+
+
+ )}
+
+
+ {/* Field Configuration */}
+ {isActive && (
+
+
+ {/* Field Label */}
+
+
+ Field Label *
+
+ handleFieldChange('label', e.target.value)}
+ className="w-full p-3 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100 focus:outline-none transition-all"
+ placeholder="Enter field label"
+ />
+
+
+ {/* Help Text */}
+
+
+ Help Text
+
+ handleFieldChange('helpText', e.target.value)}
+ className="w-full p-3 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100 focus:outline-none transition-all"
+ placeholder="Add helpful description (optional)"
+ />
+
+
+ {/* Placeholder (for text inputs) */}
+ {['TEXT', 'EMAIL', 'NUMBER'].includes(field.fieldType) && (
+
+
+ Placeholder Text
+
+ handleFieldChange('placeholder', e.target.value)}
+ className="w-full p-3 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-100 focus:outline-none transition-all"
+ placeholder="Placeholder text for users"
+ />
+
+ )}
+
+ {/* Options (for choice fields) */}
+ {['MULTIPLE_CHOICE', 'CHECKBOX', 'SINGLE_CHOICE', 'DROPDOWN'].includes(field.fieldType) && (
+
+
+ Options
+
+
+ {(field.options || []).map((option, index) => (
+
+
handleOptionChange(index, 'label', e.target.value)}
+ className="flex-1 p-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-100 focus:outline-none transition-all"
+ placeholder={`Option ${index + 1}`}
+ />
+
removeOption(index)}
+ className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
+ disabled={field.options.length <= 1}
+ title="Remove option"
+ >
+
+
+
+
+
+ ))}
+
+ +
+ Add Option
+
+
+
+ )}
+
+ {/* Required Toggle */}
+
+
+
+ Required Field
+
+
Make this field mandatory
+
+
handleFieldChange('isRequired', !field.isRequired)}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
+ field.isRequired ? 'bg-blue-600' : 'bg-gray-200'
+ }`}
+ >
+
+
+
+
+
+ )}
+
+ {/* Field Preview */}
+
+
+
+
+ {field.label || 'Untitled Field'}
+
+ {field.isRequired && (
+ *
+ )}
+
+
+ {field.helpText && (
+
{field.helpText}
+ )}
+
+
+ {renderFieldPreview()}
+
+
+ {!isActive && (
+
+ Click to edit this field
+
+ )}
+
+
+
+ );
+};
+
+export default FormFieldEditor;
\ No newline at end of file
diff --git a/frontend/src/components/FormPreview.jsx b/frontend/src/components/FormPreview.jsx
new file mode 100644
index 0000000..237aa41
--- /dev/null
+++ b/frontend/src/components/FormPreview.jsx
@@ -0,0 +1,375 @@
+import React, { useState, useEffect } from 'react';
+import Button from './Button';
+
+const FormPreview = ({
+ form,
+ isPublic = false,
+ onSubmit,
+ existingResponse = null,
+ submitButtonText = null,
+ disabled = false
+}) => {
+ const [responses, setResponses] = useState({});
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ useEffect(() => {
+ if (existingResponse && existingResponse.answers) {
+ const prefillData = {};
+ existingResponse.answers.forEach(answer => {
+ if (answer.answerJson) {
+ try {
+ prefillData[answer.fieldId] = JSON.parse(answer.answerJson);
+ } catch {
+ prefillData[answer.fieldId] = answer.answerValue;
+ }
+ } else {
+ prefillData[answer.fieldId] = answer.answerValue;
+ }
+ });
+ setResponses(prefillData);
+ }
+ }, [existingResponse]);
+
+ const handleFieldChange = (fieldId, value) => {
+ setResponses(prev => ({
+ ...prev,
+ [fieldId]: value
+ }));
+
+ if (errors[fieldId]) {
+ setErrors(prev => ({
+ ...prev,
+ [fieldId]: null
+ }));
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ form.fields.forEach(field => {
+ if (field.isRequired && !responses[field.id]) {
+ newErrors[field.id] = 'This field is required';
+ }
+ });
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const formattedResponses = form.fields.map(field => ({
+ fieldId: field.id,
+ value: responses[field.id] || null
+ }));
+
+ console.log('Submitting responses:', formattedResponses);
+ onSubmit && onSubmit(formattedResponses);
+ } catch (error) {
+ console.error('Error submitting form:', error);
+ alert('Failed to submit form. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const renderField = (field) => {
+ const hasError = !!errors[field.id];
+ const baseInputClass = `w-full p-4 border rounded-xl transition-all duration-200 ${
+ hasError
+ ? 'border-red-300 focus:border-red-500 focus:ring-2 focus:ring-red-100'
+ : 'border-gray-200 focus:border-black focus:ring-2 focus:ring-gray-100'
+ } focus:outline-none`;
+
+ switch (field.fieldType) {
+ case 'TEXT':
+ return (
+ handleFieldChange(field.id, e.target.value)}
+ className={baseInputClass}
+ />
+ );
+
+ case 'EMAIL':
+ return (
+ handleFieldChange(field.id, e.target.value)}
+ className={baseInputClass}
+ />
+ );
+
+ case 'NUMBER':
+ return (
+ handleFieldChange(field.id, e.target.value)}
+ className={baseInputClass}
+ />
+ );
+
+ case 'DATE':
+ return (
+ handleFieldChange(field.id, e.target.value)}
+ className={baseInputClass}
+ />
+ );
+
+ case 'MULTIPLE_CHOICE':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+ handleFieldChange(field.id, e.target.value)}
+ className="w-4 h-4 text-black focus:ring-black"
+ />
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'CHECKBOX':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+ {
+ const currentValues = responses[field.id] || [];
+ const newValues = e.target.checked
+ ? [...currentValues, option.value]
+ : currentValues.filter(v => v !== option.value);
+ handleFieldChange(field.id, newValues);
+ }}
+ className="w-4 h-4 text-black focus:ring-black rounded"
+ />
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'SINGLE_CHOICE':
+ return (
+
+ {(field.options || []).map((option, index) => (
+
+ handleFieldChange(field.id, e.target.value)}
+ className="w-4 h-4 text-black focus:ring-black"
+ />
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'DROPDOWN':
+ return (
+ handleFieldChange(field.id, e.target.value)}
+ className={baseInputClass}
+ >
+ Choose an option...
+ {(field.options || []).map((option, index) => (
+
+ {option.label}
+
+ ))}
+
+ );
+
+ case 'FILE':
+ return (
+
+
handleFieldChange(field.id, e.target.files[0])}
+ className="hidden"
+ id={`file-${field.id}`}
+ />
+
+ 📎
+ Click to upload or drag and drop
+
+ {responses[field.id] ? responses[field.id].name : 'Max file size: 10MB'}
+
+
+
+ );
+
+ case 'STAR_RATING':
+ return (
+
+ {[1, 2, 3, 4, 5].map((star) => (
+ handleFieldChange(field.id, star)}
+ className={`text-3xl transition-colors ${
+ responses[field.id] >= star ? 'text-yellow-400' : 'text-gray-300 hover:text-yellow-300'
+ }`}
+ >
+ ⭐
+
+ ))}
+ {responses[field.id] && (
+
+ {responses[field.id]} / 5
+
+ )}
+
+ );
+
+ default:
+ return (
+
+ );
+ }
+ };
+
+ return (
+
+
+ {/* Form Header */}
+
+ {existingResponse && (
+
+
+ ✏️
+ Editing Your Response
+
+
+ Your previous answers have been loaded. Make any changes and submit to update.
+
+
+ )}
+
+ {form.title || 'Untitled Form'}
+
+ {form.description && (
+
+ {form.description}
+
+ )}
+
+
+ {/* Form Fields */}
+
+
+ {form.fields.length === 0 && (
+
+
📝
+
+ No fields added yet
+
+
+ Add some fields to see how your form will look
+
+
+ )}
+
+
+ );
+};
+
+export default FormPreview;
\ No newline at end of file
diff --git a/frontend/src/components/FormResponses.jsx b/frontend/src/components/FormResponses.jsx
new file mode 100644
index 0000000..7153e79
--- /dev/null
+++ b/frontend/src/components/FormResponses.jsx
@@ -0,0 +1,157 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+
+const FormResponses = () => {
+ const { formId } = useParams();
+ const [form, setForm] = useState(null);
+ const [responses, setResponses] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const loadFormResponses = async () => {
+ try {
+ setLoading(true);
+ const data = await formsAPI.getFormResponsesById(formId);
+ setForm(data.form);
+ setResponses(data.responses);
+ } catch (err) {
+ console.error('Error loading form responses:', err);
+ setError(err.response?.data?.error || 'Failed to load form responses');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (formId) {
+ loadFormResponses();
+ }
+ }, [formId]);
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleString();
+ };
+
+ const getAnswerDisplay = (answer) => {
+ if (answer.answerJson) {
+ try {
+ const jsonData = JSON.parse(answer.answerJson);
+ if (Array.isArray(jsonData)) {
+ return jsonData.join(', ');
+ }
+ return JSON.stringify(jsonData);
+ } catch {
+ return answer.answerJson;
+ }
+ }
+ return answer.answerValue || 'No answer';
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading form responses...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️
+
Error Loading Responses
+
{error}
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
{form?.title}
+ {form?.description && (
+
{form.description}
+ )}
+
+ 📊 {responses.length} response{responses.length !== 1 ? 's' : ''}
+ 📋 {form?.fields?.length || 0} field{form?.fields?.length !== 1 ? 's' : ''}
+
+
+
+ {responses.length === 0 ? (
+
+
📝
+
No Responses Yet
+
+ Share your form link to start collecting responses!
+
+
+ ) : (
+
+ {responses.map((response, index) => (
+
+ {/* Response Header */}
+
+
+
+ Response #{responses.length - index}
+
+
+ Submitted on {formatDate(response.submittedAt)}
+
+
+
+ {response.submittedBy ? (
+
+
+ {response.submittedBy.username}
+
+
+ {response.submittedBy.email}
+
+
+ ) : (
+
+
Anonymous
+
+ ID: {response.anonymousId}
+
+
+ )}
+
+
+
+ {/* Response Answers */}
+
+ {response.answers.map((answer) => (
+
+
+ {answer.fieldLabel}
+
+
+ {getAnswerDisplay(answer)}
+
+
+ {answer.fieldType}
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default FormResponses;
\ No newline at end of file
diff --git a/frontend/src/components/FormSubmission.jsx b/frontend/src/components/FormSubmission.jsx
new file mode 100644
index 0000000..be8864e
--- /dev/null
+++ b/frontend/src/components/FormSubmission.jsx
@@ -0,0 +1,257 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+import FormPreview from './FormPreview';
+
+const FormSubmission = () => {
+ const { formUrl } = useParams();
+ const [form, setForm] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isSubmitted, setIsSubmitted] = useState(false);
+ const [submissionResult, setSubmissionResult] = useState(null);
+
+ useEffect(() => {
+ const loadForm = async () => {
+ try {
+ console.log('Loading form with URL:', formUrl);
+ const response = await formsAPI.getFormForDisplay(formUrl);
+ console.log('Form response:', response);
+ console.log('Form fields:', response.form?.fields);
+ setForm(response.form);
+ } catch (err) {
+ console.error('Error loading form:', err);
+ console.error('Error details:', err.response?.data);
+ setError(err.response?.data?.error || 'Form not found or no longer available');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (formUrl) {
+ loadForm();
+ }
+ }, [formUrl]);
+
+ const handleSubmit = async (responsesArray) => {
+ try {
+ const answers = responsesArray.map(({ fieldId, value }) => {
+ if (Array.isArray(value)) {
+ return {
+ fieldId: fieldId.toString(),
+ answerJson: value,
+ answerValue: null
+ };
+ }
+ return {
+ fieldId: fieldId.toString(),
+ answerValue: value,
+ answerJson: null
+ };
+ });
+
+ console.log('Sending to backend:', { answers });
+ const result = await formsAPI.submitFormResponse(formUrl, { answers });
+ console.log('Submission result:', result);
+
+ setSubmissionResult(result);
+ setIsSubmitted(true);
+ } catch (error) {
+ console.error('Error submitting form:', error);
+ throw error;
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
⚠️
+
+ Form Not Available
+
+
+ {error}
+
+
window.location.reload()}
+ className="px-6 py-3 bg-black text-white rounded-xl hover:bg-gray-800 transition-colors font-medium"
+ >
+ Try Again
+
+
+
+ );
+ }
+
+ if (!form) {
+ return (
+
+
+
🔍
+
+ Form Not Found
+
+
+ The form you're looking for doesn't exist or has been removed.
+
+
+
+ );
+ }
+
+ if (!form.isActive) {
+ return (
+
+
+
🚫
+
+ Form Closed
+
+
+ This form is no longer accepting responses.
+
+
+
+ );
+ }
+
+ if (isSubmitted) {
+ return (
+
+
+
✅
+
+ Thank You!
+
+
+ Your response has been submitted successfully. We'll get back to you soon.
+
+
+ {/* Show edit code for editable forms */}
+ {submissionResult?.canEdit && submissionResult?.anonymousId && (
+
+
+
🔑
+
+ Your Edit Code
+
+
+ Save this code to edit your response later. This form allows editing!
+
+
+
+ {/* Edit Code Display */}
+
+
+
+ Edit Code
+
+
+ {submissionResult.anonymousId}
+
+
+
+
+ {/* Instructions */}
+
+
📋 How to edit your response:
+
+ 1. Keep this code safe - screenshot or write it down
+ 2. Visit this form URL again anytime
+ 3. Enter your edit code when prompted
+ 4. Make changes and resubmit
+
+
+
+ {/* Action Buttons */}
+
+ {
+ navigator.clipboard.writeText(submissionResult.anonymousId);
+ alert('Edit code copied to clipboard!');
+ }}
+ className="flex-1 px-4 py-3 bg-green-600 text-white text-sm font-semibold rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center gap-2"
+ >
+ 📋 Copy Edit Code
+
+ {
+ const editUrl = `${window.location.origin}/form/${formUrl}/edit`;
+ window.open(editUrl, '_blank');
+ }}
+ className="flex-1 px-4 py-3 bg-blue-600 text-white text-sm font-semibold rounded-lg hover:bg-blue-700 transition-colors flex items-center justify-center gap-2"
+ >
+ 🔧 Edit Now
+
+
+
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ {form.isEditable && (
+
+ )}
+
+
+ {form.title}
+
+
+ {form.description || 'Fill out the form below with accurate information. All required fields must be completed.'}
+
+ {form.isEditable && (
+
+ 💡 You can edit your responses after submission
+
+ )}
+
+
+ {/* Form */}
+
+
+ {/* Footer Info */}
+
+
+ This form is powered by Orbis Forms.
+ •
+ Your data is secure and protected.
+
+
+
+
+ );
+};
+
+export default FormSubmission;
\ No newline at end of file
diff --git a/frontend/src/components/FormsList.jsx b/frontend/src/components/FormsList.jsx
new file mode 100644
index 0000000..e1f1937
--- /dev/null
+++ b/frontend/src/components/FormsList.jsx
@@ -0,0 +1,296 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+import Button from './Button';
+
+const FormsList = () => {
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+ const [forms, setForms] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [filter, setFilter] = useState('all');
+
+ useEffect(() => {
+ if (!isAuthenticated) {
+ navigate('/');
+ return;
+ }
+ }, [isAuthenticated, navigate]);
+
+ useEffect(() => {
+ const loadForms = async () => {
+ try {
+ const formsData = await formsAPI.getUserForms();
+ const formsList = Array.isArray(formsData) ? formsData : [];
+ setForms(formsList);
+ } catch (error) {
+ console.error('Error loading forms:', error);
+ const errorMsg = error.response?.data?.message || 'Failed to load forms';
+ alert(`Error: ${errorMsg}`);
+ setForms([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (isAuthenticated) {
+ loadForms();
+ }
+ }, [isAuthenticated]);
+
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ const filteredForms = forms.filter(form => {
+ const matchesSearch = form.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ form.description.toLowerCase().includes(searchTerm.toLowerCase());
+
+ const matchesFilter = filter === 'all' ||
+ (filter === 'active' && form.isActive) ||
+ (filter === 'inactive' && !form.isActive);
+
+ return matchesSearch && matchesFilter;
+ });
+
+ const handleCreateForm = () => {
+ navigate('/forms/create');
+ };
+
+ const handleEditForm = (formId) => {
+ navigate(`/forms/edit/${formId}`);
+ };
+
+ const handleViewForm = (formUrl) => {
+ window.open(`/form/${formUrl}`, '_blank');
+ };
+
+ const handleViewResponses = (formId) => {
+ navigate(`/forms/${formId}/responses`);
+ };
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+
Loading your forms...
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header Section */}
+
+
+
+
+
+
+ Create, manage, and analyze your forms. Build powerful forms to collect data, feedback, and registrations with ease.
+
+
+
+
+ +
+ Create New Form
+
+
+
+
+
+
+
+ {/* Search and Filters */}
+
+
+ {/* Search */}
+
+
+
setSearchTerm(e.target.value)}
+ className="w-full pl-12 pr-4 py-3 border border-gray-300 rounded-lg focus:border-black focus:ring-1 focus:ring-black focus:outline-none transition-all text-base"
+ />
+
+
+
+
+ {/* Filter */}
+
+ {[
+ { key: 'all', label: 'All' },
+ { key: 'active', label: 'Active' },
+ { key: 'inactive', label: 'Inactive' }
+ ].map((filterOption) => (
+ setFilter(filterOption.key)}
+ className={`px-4 py-2 rounded-md font-medium transition-all text-sm ${
+ filter === filterOption.key
+ ? 'bg-white text-black shadow-sm'
+ : 'text-gray-600 hover:text-black hover:bg-white/50'
+ }`}
+ >
+ {filterOption.label}
+
+ ))}
+
+
+
+
+ {/* Forms Grid */}
+ {filteredForms.length > 0 ? (
+
+ {filteredForms.map((form) => (
+
+ {/* Card Header */}
+
+
+
+
+ {form.isActive ? 'Active' : 'Inactive'}
+
+
+ {form.formUrl}
+
+
+
+ {/* Form Info */}
+
+
+ {form.title}
+
+
+ {form.description || 'No description provided'}
+
+
+ {/* Stats */}
+
+
+
+
+
+
{form.responses?.length || 0}
+
responses
+
+
+ {formatDate(form.updatedAt)}
+
+
+
+
+
+ {/* Card Actions */}
+
+
+
handleEditForm(form.id)}
+ className="flex-1 px-3 py-2.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:text-black transition-all"
+ >
+ Edit
+
+
handleViewForm(form.formUrl)}
+ className="flex-1 px-3 py-2.5 text-sm font-medium text-white bg-black rounded-lg hover:bg-gray-800 transition-all"
+ >
+ View
+
+
handleViewResponses(form.id)}
+ className="px-3 py-2.5 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-lg hover:text-black hover:bg-gray-50 transition-all"
+ title="View Responses"
+ >
+
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+
+ {searchTerm || filter !== 'all' ? 'No forms found' : 'No forms yet'}
+
+
+ {searchTerm || filter !== 'all'
+ ? 'Try adjusting your search terms or filters to find what you\'re looking for.'
+ : 'Get started by creating your first form to collect responses and manage data.'
+ }
+
+ {(!searchTerm && filter === 'all') && (
+
+ +
+ Create Your First Form
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default FormsList;
\ No newline at end of file
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
index be1da38..41d3d14 100644
--- a/frontend/src/components/Navbar.jsx
+++ b/frontend/src/components/Navbar.jsx
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import Button from './Button';
const Navbar = () => {
const { isAuthenticated, logout, user, login } = useAuth();
+ const location = useLocation();
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const canCreateEvent = user?.role === 'ORGANIZER';
@@ -74,6 +75,11 @@ const Navbar = () => {
Events
+ {isAuthenticated && (
+
+ Forms
+
+ )}
Testimonials
@@ -87,6 +93,7 @@ const Navbar = () => {
{isAuthenticated ? (
<>
Profile
+ Create Form
logout()}>Logout
>
) : (
@@ -118,6 +125,15 @@ const Navbar = () => {
>
Events
+ {isAuthenticated && (
+ handleMobileMenuClick()}
+ >
+ Forms
+
+ )}
{
>
Profile
+ handleMobileMenuClick()}
+ >
+ Create Form
+
handleMobileMenuClick(logout)}
diff --git a/frontend/src/pages/CreateForm.jsx b/frontend/src/pages/CreateForm.jsx
new file mode 100644
index 0000000..22949f3
--- /dev/null
+++ b/frontend/src/pages/CreateForm.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import FormBuilder from '../components/FormBuilder';
+
+const CreateForm = () => {
+ const navigate = useNavigate();
+
+ const handleSave = (formData) => {
+ console.log('Form saved:', formData);
+ // After successful save, redirect to forms list
+ navigate('/forms');
+ };
+
+ const handleCancel = () => {
+ navigate('/forms');
+ };
+
+ return (
+
+ );
+};
+
+export default CreateForm;
\ No newline at end of file
diff --git a/frontend/src/pages/EditForm.jsx b/frontend/src/pages/EditForm.jsx
new file mode 100644
index 0000000..5bf1ad7
--- /dev/null
+++ b/frontend/src/pages/EditForm.jsx
@@ -0,0 +1,69 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { formsAPI } from '../api/api';
+import FormBuilder from '../components/FormBuilder';
+
+const EditForm = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [form, setForm] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadForm = async () => {
+ try {
+ console.log('Loading form for editing with ID:', id);
+ const response = await formsAPI.getFormForEdit(id);
+ console.log('Form loaded successfully:', response);
+
+ // Handle both old and new response format
+ const formData = response.success ? response : response.data || response;
+ setForm(formData);
+ } catch (error) {
+ console.error('Error loading form:', error);
+ console.error('Error details:', error.response?.data);
+
+ const errorMsg = error.response?.data?.message || error.message || 'Failed to load form for editing';
+ alert(`Error: ${errorMsg}`);
+ navigate('/forms');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (id) {
+ loadForm();
+ }
+ }, [id, navigate]);
+
+ const handleSave = (formData) => {
+ console.log('Form updated:', formData);
+ // After successful save, redirect to forms list
+ navigate('/forms');
+ };
+
+ const handleCancel = () => {
+ navigate('/forms');
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default EditForm;
\ No newline at end of file
diff --git a/frontend/src/pages/FormsPage.jsx b/frontend/src/pages/FormsPage.jsx
new file mode 100644
index 0000000..dab1b6a
--- /dev/null
+++ b/frontend/src/pages/FormsPage.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import FormsList from '../components/FormsList';
+
+const FormsPage = () => {
+ return ;
+};
+
+export default FormsPage;
\ No newline at end of file