From 6fb1a71e6ba262df53ff7c629e48050d58fe6ae8 Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 14:07:52 +0100 Subject: [PATCH 01/40] new elemetn added --- .env | 5 ++--- next.config.mjs | 3 --- package-lock.json | 9 +++++---- src/components/FormElements.tsx | 8 ++++++-- src/components/FormElementsSidebar.tsx | 3 ++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.env b/.env index f70fed1..65fc7d5 100644 --- a/.env +++ b/.env @@ -1,6 +1,5 @@ -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_c3BsZW5kaWQtZG9iZXJtYW4tNTkuY2xlcmsuYWNjb3VudHMuZGV2JA -CLERK_SECRET_KEY=sk_test_l2T0mqHo5vjz6jgNgaziHJILNjGytPOmFALnMrIIU9 - +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_aW1wcm92ZWQtZ2FyZmlzaC05Ni5jbGVyay5hY2NvdW50cy5kZXYk +CLERK_SECRET_KEY=sk_test_g77C4CnTgDtW7L8DVj8WGHbwDlCspgYWWO6PWXGLVD NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard diff --git a/next.config.mjs b/next.config.mjs index e709436..0a7c9af 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,8 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - experimental: { - serverActions: true, - }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 77d3a87..c01c4b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2966,9 +2966,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "funding": [ { "type": "opencollective", @@ -2982,7 +2982,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", diff --git a/src/components/FormElements.tsx b/src/components/FormElements.tsx index 799d579..b5d3211 100644 --- a/src/components/FormElements.tsx +++ b/src/components/FormElements.tsx @@ -21,7 +21,8 @@ export type ElementsType = | "TextAreaField" | "DateField" | "SelectField" - | "CheckboxField"; + | "CheckboxField" + | "NewElementField"; // Add your new element type here export type SubmitFunction = (key: string, value: string) => void; @@ -60,6 +61,8 @@ export type FormElementInstance = { type FormElementsType = { [key in ElementsType]: FormElement; }; +import { NewElementFormElement } from "./field/NewElementField"; + export const FormElements: FormElementsType = { TextField: TextFieldFormElement, TitleField: TitleFieldFormElement, @@ -72,4 +75,5 @@ export const FormElements: FormElementsType = { DateField: DateFieldFormElement, SelectField: SelectFieldFormElement, CheckboxField: CheckboxFieldFormElement, -}; \ No newline at end of file + NewElementField: NewElementFormElement, +}; diff --git a/src/components/FormElementsSidebar.tsx b/src/components/FormElementsSidebar.tsx index 97f1efb..19da432 100644 --- a/src/components/FormElementsSidebar.tsx +++ b/src/components/FormElementsSidebar.tsx @@ -23,9 +23,10 @@ function FormElementsSidebar() { + ); } -export default FormElementsSidebar; \ No newline at end of file +export default FormElementsSidebar; From 1ec7b06481b02a1cfc89f2297a01c2c29c7739c8 Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 14:12:42 +0100 Subject: [PATCH 02/40] all files --- .env | 5 +- src/components/field/NewElementField.tsx | 230 +++++++++++++++++++++++ 2 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 src/components/field/NewElementField.tsx diff --git a/.env b/.env index 65fc7d5..f70fed1 100644 --- a/.env +++ b/.env @@ -1,5 +1,6 @@ -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_aW1wcm92ZWQtZ2FyZmlzaC05Ni5jbGVyay5hY2NvdW50cy5kZXYk -CLERK_SECRET_KEY=sk_test_g77C4CnTgDtW7L8DVj8WGHbwDlCspgYWWO6PWXGLVD +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_c3BsZW5kaWQtZG9iZXJtYW4tNTkuY2xlcmsuYWNjb3VudHMuZGV2JA +CLERK_SECRET_KEY=sk_test_l2T0mqHo5vjz6jgNgaziHJILNjGytPOmFALnMrIIU9 + NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard diff --git a/src/components/field/NewElementField.tsx b/src/components/field/NewElementField.tsx new file mode 100644 index 0000000..203a5c1 --- /dev/null +++ b/src/components/field/NewElementField.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { ElementsType, FormElement, FormElementInstance } from "../FormElements"; +import { Label } from "../ui/label"; +import { Input } from "../ui/input"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { BsTextareaResize } from "react-icons/bs"; +import useDesigner from "@/hooks/useDesigner"; + +// Define the extra attributes your element needs +type CustomInstance = FormElementInstance & { + extraAttributes: { + label: string; + helperText: string; + required: boolean; + placeholder: string; + }; +}; + +// Define validation schema for properties form +const propertiesSchema = z.object({ + label: z.string().min(2).max(50), + helperText: z.string().max(200), + required: z.boolean().default(false), + placeholder: z.string().max(50), +}); + +export const NewElementFormElement: FormElement = { + type: "NewElementField", + construct: (id: string) => ({ + id, + type: "NewElementField", + extraAttributes: { + label: "New Element", + helperText: "Helper text", + required: false, + placeholder: "Placeholder", + }, + }), + + designerBtnElement: { + icon: BsTextareaResize, + label: "New Element", + }, + + designerComponent: DesignerComponent, + formComponent: FormComponent, + propertiesComponent: PropertiesComponent, + + validate: (formElement: FormElementInstance, currentValue: string): boolean => { + const element = formElement as CustomInstance; + if (element.extraAttributes.required) { + return currentValue.length > 0; + } + return true; + }, +}; + +function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { label, helperText, required, placeholder } = element.extraAttributes; + return ( +
+ + + {helperText &&

{helperText}

} +
+ ); +} + +function FormComponent({ + elementInstance, + submitValue, + isInvalid, + defaultValue, +}: { + elementInstance: FormElementInstance; + submitValue?: (key: string, value: string) => void; + isInvalid?: boolean; + defaultValue?: string; +}) { + const element = elementInstance as CustomInstance; + const { label, helperText, required, placeholder } = element.extraAttributes; + + return ( +
+ + submitValue?.(element.id, e.target.value)} + defaultValue={defaultValue} + /> + {helperText &&

{helperText}

} +
+ ); +} + +type propertiesFormSchemaType = z.infer; + +function PropertiesComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { updateElement } = useDesigner(); + const form = useForm({ + resolver: zodResolver(propertiesSchema), + mode: "onBlur", + defaultValues: { + label: element.extraAttributes.label, + helperText: element.extraAttributes.helperText, + required: element.extraAttributes.required, + placeholder: element.extraAttributes.placeholder, + }, + }); + + useEffect(() => { + form.reset(element.extraAttributes); + }, [element, form]); + + function applyChanges(values: propertiesFormSchemaType) { + const { label, helperText, required, placeholder } = values; + updateElement(element.id, { + ...element, + extraAttributes: { + label, + helperText, + required, + placeholder, + }, + }); + } + + return ( +
+ { + e.preventDefault(); + }} + className="space-y-3" + > + ( + + Label + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Placeholder + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Helper text + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + +
+ Required +
+ + + +
+ )} + /> + + + ); +} From 94aa252edc6bfe903e2dea2b0c56d1f83333ce7b Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 14:22:24 +0100 Subject: [PATCH 03/40] added element and documenation --- Docs/AddElement.md | 182 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 Docs/AddElement.md diff --git a/Docs/AddElement.md b/Docs/AddElement.md new file mode 100644 index 0000000..c5342f5 --- /dev/null +++ b/Docs/AddElement.md @@ -0,0 +1,182 @@ +# Adding a New Form Element + +This guide explains how to add a new form element type to the Quick Form Builder system. + +## Overview + +Form elements are the building blocks of forms in our system. Each element type (like text fields, checkboxes, etc.) is implemented as a separate component that follows a consistent pattern and interface. + +## Step-by-Step Guide + +### 1. Add Element Type Definition + +In `src/components/FormElements.tsx`, add your new element type to the `ElementsType` union type: + +```typescript +export type ElementsType = + | "TextField" + | "TitleField" + // ... other existing types ... + | "YourNewFieldType"; // Add your new type here +``` + +### 2. Create Element Component File + +Create a new file for your element under `src/components/field/` (e.g., `YourNewField.tsx`). The file should include: + +#### a) Type Definitions +```typescript +type CustomInstance = FormElementInstance & { + extraAttributes: { + label: string; + helperText: string; + required: boolean; + placeholder: string; + // Add any additional attributes your element needs + }; +}; + +const propertiesSchema = z.object({ + label: z.string().min(2).max(50), + helperText: z.string().max(200), + required: z.boolean().default(false), + placeholder: z.string().max(50), + // Add validation for any additional properties +}); +``` + +#### b) Element Implementation +```typescript +export const YourNewFormElement: FormElement = { + type: "YourNewFieldType", + + // Factory function to create new instances + construct: (id: string) => ({ + id, + type: "YourNewFieldType", + extraAttributes: { + label: "Default Label", + helperText: "Default helper text", + required: false, + placeholder: "Default placeholder", + }, + }), + + // Sidebar button configuration + designerBtnElement: { + icon: YourIcon, // Import from react-icons + label: "Your Element", + }, + + // Component references + designerComponent: DesignerComponent, + formComponent: FormComponent, + propertiesComponent: PropertiesComponent, + + // Validation logic + validate: (formElement: FormElementInstance, currentValue: string): boolean => { + const element = formElement as CustomInstance; + if (element.extraAttributes.required) { + return currentValue.length > 0; + } + return true; + }, +}; +``` + +#### c) Required Components + +1. Designer Component (Preview in builder): +```typescript +function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { label, helperText, required, placeholder } = element.extraAttributes; + return ( +
+ + + {helperText &&

{helperText}

} +
+ ); +} +``` + +2. Form Component (Live form element): +```typescript +function FormComponent({ + elementInstance, + submitValue, + isInvalid, + defaultValue, +}: { + elementInstance: FormElementInstance; + submitValue?: (key: string, value: string) => void; + isInvalid?: boolean; + defaultValue?: string; +}) { + const element = elementInstance as CustomInstance; + // Implementation for the actual form input +} +``` + +3. Properties Component (Settings panel): +```typescript +function PropertiesComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { updateElement } = useDesigner(); + // Implementation for property editing +} +``` + +### 3. Register the Element + +In `src/components/FormElements.tsx`, import and add your element to the FormElements object: + +```typescript +import { YourNewFormElement } from "./field/YourNewField"; + +export const FormElements: FormElementsType = { + TextField: TextFieldFormElement, + // ... other elements ... + YourNewFieldType: YourNewFormElement, +}; +``` + +### 4. Add to Sidebar + +In `src/components/FormElementsSidebar.tsx`, add your element to the sidebar: + +```typescript + +``` + +## Database Integration + +No additional database changes are required. The system stores form content as JSON in the Prisma database, and new elements are automatically handled as long as they follow the FormElementInstance interface. + +## Testing Your New Element + +1. Create a new form +2. Verify your element appears in the sidebar +3. Test dragging and dropping the element +4. Test property editing +5. Test form saving and loading +6. Test form publishing and submission + +## Best Practices + +1. Follow the existing naming conventions +2. Implement proper validation +3. Include helpful default values +4. Add appropriate TypeScript types +5. Ensure proper error handling +6. Test all component states (preview, edit, submit) + +## Common Issues + +- If the element doesn't appear in the sidebar, check the FormElements export +- If properties don't update, verify the PropertiesComponent implementation +- If validation fails, check the validate function implementation From a72fa059588cb98248e4eec8a1b687dc87e21433 Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 16:03:51 +0100 Subject: [PATCH 04/40] added image upload --- Examples/ImageUpload.tsx | 157 ++++++++ src/components/FormElements.tsx | 6 +- src/components/FormElementsSidebar.tsx | 2 +- src/components/field/ImageUploadField.tsx | 444 ++++++++++++++++++++++ 4 files changed, 605 insertions(+), 4 deletions(-) create mode 100644 Examples/ImageUpload.tsx create mode 100644 src/components/field/ImageUploadField.tsx diff --git a/Examples/ImageUpload.tsx b/Examples/ImageUpload.tsx new file mode 100644 index 0000000..3e37d09 --- /dev/null +++ b/Examples/ImageUpload.tsx @@ -0,0 +1,157 @@ +import React, { useState, useRef } from 'react'; + +const ImageUpload = ({ + prompt = "Upload an image", + buttonText = "Choose File", + width = "w-96", + height = "h-64", + maxDimension = 800, + onImageSelect = (file) => console.log('Image selected:', file) +}) => { + const [previewUrl, setPreviewUrl] = useState(null); + const [fileName, setFileName] = useState("No file chosen"); + const fileInputRef = useRef(null); + + const resizeImage = (originalFile) => { + return new Promise((resolve) => { + const img = new Image(); + img.src = URL.createObjectURL(originalFile); + + img.onload = () => { + // Calculate new dimensions + let newWidth = img.width; + let newHeight = img.height; + + if (img.width > maxDimension || img.height > maxDimension) { + if (img.width > img.height) { + newWidth = maxDimension; + newHeight = (img.height / img.width) * maxDimension; + } else { + newHeight = maxDimension; + newWidth = (img.width / img.height) * maxDimension; + } + } + + // Create canvas and resize + const canvas = document.createElement('canvas'); + canvas.width = newWidth; + canvas.height = newHeight; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, newWidth, newHeight); + + // Convert to file + canvas.toBlob((blob) => { + // Create new file with original name + const resizedFile = new File([blob], originalFile.name, { + type: originalFile.type, + lastModified: Date.now(), + }); + + resolve(resizedFile); + }, originalFile.type); + + // Clean up + URL.revokeObjectURL(img.src); + }; + }); + }; + + const handleFileSelect = async (event) => { + const file = event.target.files[0]; + if (file && file.type.startsWith('image/')) { + setFileName(file.name); + + // Create preview + const reader = new FileReader(); + reader.onload = () => { + setPreviewUrl(reader.result); + }; + reader.readAsDataURL(file); + + // Process and resize image if needed + const img = new Image(); + img.src = URL.createObjectURL(file); + + img.onload = async () => { + let finalFile = file; + + // Only resize if image exceeds max dimensions + if (img.width > maxDimension || img.height > maxDimension) { + finalFile = await resizeImage(file); + console.log(`Image resized from ${img.width}x${img.height} to ${finalFile.width}x${finalFile.height}`); + } + + URL.revokeObjectURL(img.src); + onImageSelect(finalFile); + }; + } + }; + + const handleButtonClick = () => { + fileInputRef.current?.click(); + }; + + return ( +
+ {/* Image Preview Area */} +
+ {previewUrl ? ( + Preview + ) : ( +
+ + + +
+ )} +
+ + {/* Prompt Text */} +
+ {prompt} +
+ + {/* DaisyUI-style File Input */} +
+ +
+
+ {buttonText} +
+
+ {fileName} +
+
+
+
+ ); +}; + +export default ImageUpload; \ No newline at end of file diff --git a/src/components/FormElements.tsx b/src/components/FormElements.tsx index b5d3211..1216c36 100644 --- a/src/components/FormElements.tsx +++ b/src/components/FormElements.tsx @@ -22,7 +22,7 @@ export type ElementsType = | "DateField" | "SelectField" | "CheckboxField" - | "NewElementField"; // Add your new element type here + | "ImageUploadField"; export type SubmitFunction = (key: string, value: string) => void; @@ -61,7 +61,7 @@ export type FormElementInstance = { type FormElementsType = { [key in ElementsType]: FormElement; }; -import { NewElementFormElement } from "./field/NewElementField"; +import { ImageUploadFormElement } from "./field/ImageUploadField"; export const FormElements: FormElementsType = { TextField: TextFieldFormElement, @@ -75,5 +75,5 @@ export const FormElements: FormElementsType = { DateField: DateFieldFormElement, SelectField: SelectFieldFormElement, CheckboxField: CheckboxFieldFormElement, - NewElementField: NewElementFormElement, + ImageUploadField: ImageUploadFormElement, }; diff --git a/src/components/FormElementsSidebar.tsx b/src/components/FormElementsSidebar.tsx index 19da432..6dcbc92 100644 --- a/src/components/FormElementsSidebar.tsx +++ b/src/components/FormElementsSidebar.tsx @@ -23,7 +23,7 @@ function FormElementsSidebar() { - + ); diff --git a/src/components/field/ImageUploadField.tsx b/src/components/field/ImageUploadField.tsx new file mode 100644 index 0000000..0c07d3b --- /dev/null +++ b/src/components/field/ImageUploadField.tsx @@ -0,0 +1,444 @@ +"use client"; + +import { ElementsType, FormElement, FormElementInstance } from "../FormElements"; +import { Label } from "../ui/label"; +import { Input } from "../ui/input"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState, useRef } from "react"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { BsCardImage } from "react-icons/bs"; +import useDesigner from "@/hooks/useDesigner"; + +type CustomInstance = FormElementInstance & { + extraAttributes: { + label: string; + helperText: string; + required: boolean; + prompt: string; + buttonText: string; + width: string; + height: string; + maxDimension: number; + }; +}; + +const propertiesSchema = z.object({ + label: z.string().min(2).max(50), + helperText: z.string().max(200), + required: z.boolean().default(false), + prompt: z.string().min(2).max(100), + buttonText: z.string().min(2).max(50), + width: z.string(), + height: z.string(), + maxDimension: z.number().min(100).max(2000), +}); + +export const ImageUploadFormElement: FormElement = { + type: "ImageUploadField", + construct: (id: string) => ({ + id, + type: "ImageUploadField", + extraAttributes: { + label: "Image Upload", + helperText: "Upload an image file", + required: false, + prompt: "Upload an image", + buttonText: "Choose File", + width: "w-96", + height: "h-64", + maxDimension: 800, + }, + }), + + designerBtnElement: { + icon: BsCardImage, + label: "Image Upload", + }, + + designerComponent: DesignerComponent, + formComponent: FormComponent, + propertiesComponent: PropertiesComponent, + + validate: (formElement: FormElementInstance, currentValue: string): boolean => { + const element = formElement as CustomInstance; + if (element.extraAttributes.required) { + return currentValue.length > 0; + } + return true; + }, +}; + +function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { label, helperText, required, prompt, buttonText, width, height } = element.extraAttributes; + return ( +
+ +
+
+ + + +
+
+ {helperText &&

{helperText}

} +
+ ); +} + +function FormComponent({ + elementInstance, + submitValue, + isInvalid, + defaultValue, +}: { + elementInstance: FormElementInstance; + submitValue?: (key: string, value: string) => void; + isInvalid?: boolean; + defaultValue?: string; +}) { + const element = elementInstance as CustomInstance; + const [previewUrl, setPreviewUrl] = useState(null); + const [fileName, setFileName] = useState("No file chosen"); + const fileInputRef = useRef(null); + const { prompt, buttonText, width, height, maxDimension } = element.extraAttributes; + + const resizeImage = (originalFile: File): Promise => { + return new Promise((resolve) => { + const img = new Image(); + img.src = URL.createObjectURL(originalFile); + + img.onload = () => { + let newWidth = img.width; + let newHeight = img.height; + + if (img.width > maxDimension || img.height > maxDimension) { + if (img.width > img.height) { + newWidth = maxDimension; + newHeight = (img.height / img.width) * maxDimension; + } else { + newHeight = maxDimension; + newWidth = (img.width / img.height) * maxDimension; + } + } + + const canvas = document.createElement('canvas'); + canvas.width = newWidth; + canvas.height = newHeight; + + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, 0, 0, newWidth, newHeight); + + canvas.toBlob((blob) => { + if (blob) { + const resizedFile = new File([blob], originalFile.name, { + type: originalFile.type, + lastModified: Date.now(), + }); + + resolve(resizedFile); + } + }, originalFile.type); + } + + URL.revokeObjectURL(img.src); + }; + }); + }; + + const handleFileSelect = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file && file.type.startsWith('image/')) { + setFileName(file.name); + + const reader = new FileReader(); + reader.onload = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); + + const img = new Image(); + img.src = URL.createObjectURL(file); + + img.onload = async () => { + let finalFile = file; + + if (img.width > maxDimension || img.height > maxDimension) { + finalFile = await resizeImage(file); + } + + URL.revokeObjectURL(img.src); + submitValue?.(elementInstance.id, finalFile.name); + }; + } + }; + + return ( +
+ +
+ {previewUrl ? ( + Preview + ) : ( +
+ + + +
+ )} +
+
+ {prompt} +
+
+ +
fileInputRef.current?.click()} + > +
+ {buttonText} +
+
+ {fileName} +
+
+
+ {element.extraAttributes.helperText && ( +

+ {element.extraAttributes.helperText} +

+ )} +
+ ); +} + +type propertiesFormSchemaType = z.infer; + +function PropertiesComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { updateElement } = useDesigner(); + const form = useForm({ + resolver: zodResolver(propertiesSchema), + mode: "onBlur", + defaultValues: { + label: element.extraAttributes.label, + helperText: element.extraAttributes.helperText, + required: element.extraAttributes.required, + prompt: element.extraAttributes.prompt, + buttonText: element.extraAttributes.buttonText, + width: element.extraAttributes.width, + height: element.extraAttributes.height, + maxDimension: element.extraAttributes.maxDimension, + }, + }); + + useEffect(() => { + form.reset(element.extraAttributes); + }, [element, form]); + + function applyChanges(values: propertiesFormSchemaType) { + updateElement(element.id, { + ...element, + extraAttributes: values, + }); + } + + return ( +
+ { + e.preventDefault(); + }} + className="space-y-3" + > + ( + + Label + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Prompt Text + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Button Text + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Helper text + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Width (Tailwind class) + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Height (Tailwind class) + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Max Dimension (pixels) + + field.onChange(parseInt(e.target.value))} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + +
+ Required +
+ + + +
+ )} + /> + + + ); +} From 28f20dfd08b5d69f7b84190afa473369aedc50b3 Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 16:21:46 +0100 Subject: [PATCH 05/40] added rating scale --- Examples/RatingScale.tsx | 189 +++++++ src/components/FormElements.tsx | 5 +- src/components/FormElementsSidebar.tsx | 1 + src/components/field/RatingScaleField.tsx | 574 ++++++++++++++++++++++ 4 files changed, 768 insertions(+), 1 deletion(-) create mode 100644 Examples/RatingScale.tsx create mode 100644 src/components/field/RatingScaleField.tsx diff --git a/Examples/RatingScale.tsx b/Examples/RatingScale.tsx new file mode 100644 index 0000000..18035d1 --- /dev/null +++ b/Examples/RatingScale.tsx @@ -0,0 +1,189 @@ +import React, { useState } from 'react'; + +const solidColorSchemes = { + blue: { + selected: 'bg-blue-500 border-blue-600', + hover: 'hover:border-blue-400', + text: 'text-blue-600' + }, + green: { + selected: 'bg-green-500 border-green-600', + hover: 'hover:border-green-400', + text: 'text-green-600' + }, + purple: { + selected: 'bg-purple-500 border-purple-600', + hover: 'hover:border-purple-400', + text: 'text-purple-600' + }, + red: { + selected: 'bg-red-500 border-red-600', + hover: 'hover:border-red-400', + text: 'text-red-600' + }, + amber: { + selected: 'bg-amber-500 border-amber-600', + hover: 'hover:border-amber-400', + text: 'text-amber-600' + } +}; + +const gradientColorSchemes = { + severity: { + colors: ['green', 'yellow', 'red'], + labels: { + start: { color: 'text-green-600', hover: 'hover:border-green-400' }, + middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, + end: { color: 'text-red-600', hover: 'hover:border-red-400' } + } + }, + satisfaction: { + colors: ['red', 'yellow', 'green'], + labels: { + start: { color: 'text-red-600', hover: 'hover:border-red-400' }, + middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, + end: { color: 'text-green-600', hover: 'hover:border-green-400' } + } + }, + temperature: { + colors: ['blue', 'green', 'red'], + labels: { + start: { color: 'text-blue-600', hover: 'hover:border-blue-400' }, + middle: { color: 'text-green-600', hover: 'hover:border-green-400' }, + end: { color: 'text-red-600', hover: 'hover:border-red-400' } + } + } +}; + +const getColorForValue = (value, minValue, maxValue, gradientScheme) => { + const position = (value - minValue) / (maxValue - minValue); + const colors = gradientColorSchemes[gradientScheme]?.colors; + + if (!colors) return null; + + if (position <= 0.33) { + return { + selected: `bg-${colors[0]}-500 border-${colors[0]}-600`, + hover: `hover:border-${colors[0]}-400` + }; + } else if (position <= 0.66) { + return { + selected: `bg-${colors[1]}-500 border-${colors[1]}-600`, + hover: `hover:border-${colors[1]}-400` + }; + } else { + return { + selected: `bg-${colors[2]}-500 border-${colors[2]}-600`, + hover: `hover:border-${colors[2]}-400` + }; + } +}; + +const OneToTen = ({ + question = "Rate your pain level", + minLabel = "Mild", + midLabel = "Moderate", + maxLabel = "Severe", + minValue = 0, + maxValue = 10, + defaultValue = null, + colorScheme = 'blue', + gradientScheme = null, + onChange = (value) => console.log('Selected value:', value) +}) => { + const [selectedValue, setSelectedValue] = useState(defaultValue); + const [hoveredValue, setHoveredValue] = useState(null); + + const useGradient = gradientScheme && gradientColorSchemes[gradientScheme]; + const gradientLabels = useGradient ? gradientColorSchemes[gradientScheme].labels : null; + const solidColors = solidColorSchemes[colorScheme] || solidColorSchemes.blue; + + // Calculate the range of values + const values = Array.from( + { length: maxValue - minValue + 1 }, + (_, i) => i + minValue + ); + + const handleSelection = (value) => { + setSelectedValue(value); + onChange(value); + }; + + const getButtonColors = (value) => { + if (useGradient) { + return getColorForValue(value, minValue, maxValue, gradientScheme); + } + return { + selected: solidColors.selected, + hover: solidColors.hover + }; + }; + + const getLabelColor = (position) => { + if (!useGradient) return solidColors.text; + + switch(position) { + case 'start': return gradientLabels.start.color; + case 'middle': return gradientLabels.middle.color; + case 'end': return gradientLabels.end.color; + default: return solidColors.text; + } + }; + + return ( +
+ {/* Question Label */} +
+

{question}

+
+ + {/* Radio Button Group */} +
+ {values.map((value) => { + const colors = getButtonColors(value); + return ( +
setHoveredValue(value)} + onMouseLeave={() => setHoveredValue(null)} + > + +
+ ); + })} +
+ + {/* Label Row */} +
+ + {minLabel} + + minValue + (maxValue - minValue) / 3 && hoveredValue < maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> + {midLabel} + + = maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> + {maxLabel} + +
+
+ ); +}; + +export default OneToTen; \ No newline at end of file diff --git a/src/components/FormElements.tsx b/src/components/FormElements.tsx index 1216c36..ca86e76 100644 --- a/src/components/FormElements.tsx +++ b/src/components/FormElements.tsx @@ -22,7 +22,8 @@ export type ElementsType = | "DateField" | "SelectField" | "CheckboxField" - | "ImageUploadField"; + | "ImageUploadField" + | "RatingScaleField"; export type SubmitFunction = (key: string, value: string) => void; @@ -62,6 +63,7 @@ type FormElementsType = { [key in ElementsType]: FormElement; }; import { ImageUploadFormElement } from "./field/ImageUploadField"; +import { RatingScaleFormElement } from "./field/RatingScaleField"; export const FormElements: FormElementsType = { TextField: TextFieldFormElement, @@ -76,4 +78,5 @@ export const FormElements: FormElementsType = { SelectField: SelectFieldFormElement, CheckboxField: CheckboxFieldFormElement, ImageUploadField: ImageUploadFormElement, + RatingScaleField: RatingScaleFormElement, }; diff --git a/src/components/FormElementsSidebar.tsx b/src/components/FormElementsSidebar.tsx index 6dcbc92..e210a20 100644 --- a/src/components/FormElementsSidebar.tsx +++ b/src/components/FormElementsSidebar.tsx @@ -24,6 +24,7 @@ function FormElementsSidebar() { + ); diff --git a/src/components/field/RatingScaleField.tsx b/src/components/field/RatingScaleField.tsx new file mode 100644 index 0000000..fe878f9 --- /dev/null +++ b/src/components/field/RatingScaleField.tsx @@ -0,0 +1,574 @@ +"use client"; + +import { ElementsType, FormElement, FormElementInstance } from "../FormElements"; +import { Label } from "../ui/label"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; +import { BsStarFill } from "react-icons/bs"; +import useDesigner from "@/hooks/useDesigner"; +import { Input } from "../ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; + +const SOLID_COLOR_SCHEMES = ["blue", "green", "purple", "red", "amber"] as const; +const GRADIENT_SCHEMES = ["severity", "satisfaction", "temperature"] as const; + +type ColorScheme = { + selected: string; + hover: string; + text: string; +}; + +type SolidColorSchemes = { + [key in typeof SOLID_COLOR_SCHEMES[number]]: ColorScheme; +}; + +const solidColorSchemes: SolidColorSchemes = { + blue: { + selected: 'bg-blue-500 border-blue-600', + hover: 'hover:border-blue-400', + text: 'text-blue-600' + }, + green: { + selected: 'bg-green-500 border-green-600', + hover: 'hover:border-green-400', + text: 'text-green-600' + }, + purple: { + selected: 'bg-purple-500 border-purple-600', + hover: 'hover:border-purple-400', + text: 'text-purple-600' + }, + red: { + selected: 'bg-red-500 border-red-600', + hover: 'hover:border-red-400', + text: 'text-red-600' + }, + amber: { + selected: 'bg-amber-500 border-amber-600', + hover: 'hover:border-amber-400', + text: 'text-amber-600' + } +}; + +type GradientLabel = { + color: string; + hover: string; +}; + +type GradientScheme = { + colors: string[]; + labels: { + start: GradientLabel; + middle: GradientLabel; + end: GradientLabel; + }; +}; + +type GradientColorSchemes = { + [key in typeof GRADIENT_SCHEMES[number]]: GradientScheme; +}; + +const gradientColorSchemes: GradientColorSchemes = { + severity: { + colors: ['green', 'yellow', 'red'], + labels: { + start: { color: 'text-green-600', hover: 'hover:border-green-400' }, + middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, + end: { color: 'text-red-600', hover: 'hover:border-red-400' } + } + }, + satisfaction: { + colors: ['red', 'yellow', 'green'], + labels: { + start: { color: 'text-red-600', hover: 'hover:border-red-400' }, + middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, + end: { color: 'text-green-600', hover: 'hover:border-green-400' } + } + }, + temperature: { + colors: ['blue', 'green', 'red'], + labels: { + start: { color: 'text-blue-600', hover: 'hover:border-blue-400' }, + middle: { color: 'text-green-600', hover: 'hover:border-green-400' }, + end: { color: 'text-red-600', hover: 'hover:border-red-400' } + } + } +}; + +type ColorSchemeType = typeof SOLID_COLOR_SCHEMES[number]; +type GradientSchemeType = typeof GRADIENT_SCHEMES[number] | null; + +type CustomInstance = FormElementInstance & { + extraAttributes: { + label: string; + helperText: string; + required: boolean; + question: string; + minLabel: string; + midLabel: string; + maxLabel: string; + minValue: number; + maxValue: number; + colorScheme: ColorSchemeType; + gradientScheme: GradientSchemeType; + }; +}; + +const propertiesSchema = z.object({ + label: z.string().min(2).max(50), + helperText: z.string().max(200), + required: z.boolean().default(false), + question: z.string().min(2).max(200), + minLabel: z.string().min(1).max(50), + midLabel: z.string().min(1).max(50), + maxLabel: z.string().min(1).max(50), + minValue: z.number().min(0).max(100), + maxValue: z.number().min(0).max(100), + colorScheme: z.enum(SOLID_COLOR_SCHEMES), + gradientScheme: z.enum(['none', ...GRADIENT_SCHEMES]).transform((value: string) => value === 'none' ? null : value), +}); + +export const RatingScaleFormElement: FormElement = { + type: "RatingScaleField", + construct: (id: string) => ({ + id, + type: "RatingScaleField", + extraAttributes: { + label: "Rating Scale", + helperText: "Select a value", + required: false, + question: "Rate your experience", + minLabel: "Poor", + midLabel: "Average", + maxLabel: "Excellent", + minValue: 1, + maxValue: 10, + colorScheme: "blue", + gradientScheme: null, + }, + }), + + designerBtnElement: { + icon: BsStarFill, + label: "Rating Scale", + }, + + designerComponent: DesignerComponent, + formComponent: FormComponent, + propertiesComponent: PropertiesComponent, + + validate: (formElement: FormElementInstance, currentValue: string): boolean => { + const element = formElement as CustomInstance; + if (element.extraAttributes.required) { + return currentValue.length > 0; + } + return true; + }, +}; + +function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { question, minLabel, midLabel, maxLabel, minValue, maxValue, colorScheme } = element.extraAttributes; + const solidColors = solidColorSchemes[colorScheme]; + + return ( +
+ +
+ {Array.from({ length: maxValue - minValue + 1 }, (_, i) => i + minValue).map((value) => ( +
+ +
+ ))} +
+
+ {minLabel} + {midLabel} + {maxLabel} +
+
+ ); +} + +function FormComponent({ + elementInstance, + submitValue, + isInvalid, + defaultValue, +}: { + elementInstance: FormElementInstance; + submitValue?: (key: string, value: string) => void; + isInvalid?: boolean; + defaultValue?: string; +}) { + const element = elementInstance as CustomInstance; + const [selectedValue, setSelectedValue] = useState(defaultValue ? parseInt(defaultValue) : null); + const [hoveredValue, setHoveredValue] = useState(null); + + const { + question, + minLabel, + midLabel, + maxLabel, + minValue, + maxValue, + colorScheme, + gradientScheme + } = element.extraAttributes; + + const useGradient = gradientScheme && gradientColorSchemes[gradientScheme]; + const gradientLabels = useGradient ? gradientColorSchemes[gradientScheme].labels : null; + const solidColors = solidColorSchemes[colorScheme]; + + const values = Array.from( + { length: maxValue - minValue + 1 }, + (_, i) => i + minValue + ); + + const handleSelection = (value: number) => { + setSelectedValue(value); + submitValue?.(elementInstance.id, value.toString()); + }; + + const getButtonColors = (value: number) => { + if (useGradient) { + const position = (value - minValue) / (maxValue - minValue); + if (position <= 0.33) { + return { + selected: `bg-${gradientColorSchemes[gradientScheme].colors[0]}-500 border-${gradientColorSchemes[gradientScheme].colors[0]}-600`, + hover: `hover:border-${gradientColorSchemes[gradientScheme].colors[0]}-400` + }; + } else if (position <= 0.66) { + return { + selected: `bg-${gradientColorSchemes[gradientScheme].colors[1]}-500 border-${gradientColorSchemes[gradientScheme].colors[1]}-600`, + hover: `hover:border-${gradientColorSchemes[gradientScheme].colors[1]}-400` + }; + } else { + return { + selected: `bg-${gradientColorSchemes[gradientScheme].colors[2]}-500 border-${gradientColorSchemes[gradientScheme].colors[2]}-600`, + hover: `hover:border-${gradientColorSchemes[gradientScheme].colors[2]}-400` + }; + } + } + return { + selected: solidColors.selected, + hover: solidColors.hover + }; + }; + + const getLabelColor = (position: 'start' | 'middle' | 'end'): string => { + if (!useGradient || !gradientLabels) return solidColors.text; + return gradientLabels[position].color; + }; + + return ( +
+
+

{question}

+
+ +
+ {values.map((value) => { + const colors = getButtonColors(value); + return ( +
setHoveredValue(value)} + onMouseLeave={() => setHoveredValue(null)} + > + +
+ ); + })} +
+ +
+ + {minLabel} + + minValue + (maxValue - minValue) / 3 && hoveredValue < maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> + {midLabel} + + = maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> + {maxLabel} + +
+
+ ); +} + +function PropertiesComponent({ elementInstance }: { elementInstance: FormElementInstance }) { + const element = elementInstance as CustomInstance; + const { updateElement } = useDesigner(); + const form = useForm({ + resolver: zodResolver(propertiesSchema), + mode: "onBlur", + defaultValues: { + label: element.extraAttributes.label, + helperText: element.extraAttributes.helperText, + required: element.extraAttributes.required, + question: element.extraAttributes.question, + minLabel: element.extraAttributes.minLabel, + midLabel: element.extraAttributes.midLabel, + maxLabel: element.extraAttributes.maxLabel, + minValue: element.extraAttributes.minValue, + maxValue: element.extraAttributes.maxValue, + colorScheme: element.extraAttributes.colorScheme, + gradientScheme: element.extraAttributes.gradientScheme ?? 'none', + }, + }); + + useEffect(() => { + form.reset(element.extraAttributes); + }, [element, form]); + + function applyChanges(values: propertiesFormSchemaType) { + const { colorScheme, gradientScheme, ...rest } = values; + updateElement(element.id, { + ...element, + extraAttributes: { + ...rest, + colorScheme: colorScheme as ColorSchemeType, + gradientScheme: gradientScheme === 'none' ? null : gradientScheme as GradientSchemeType, + }, + }); + } + + return ( +
+ { + e.preventDefault(); + }} + className="space-y-3" + > + ( + + Question + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Minimum Label + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Middle Label + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Maximum Label + + { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Minimum Value + + field.onChange(parseInt(e.target.value))} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Maximum Value + + field.onChange(parseInt(e.target.value))} + onKeyDown={(e) => { + if (e.key === "Enter") e.currentTarget.blur(); + }} + /> + + + + )} + /> + + ( + + Color Scheme + + + + )} + /> + + ( + + Gradient Scheme (Optional) + + + + )} + /> + + ( + +
+ Required +
+ + + +
+ )} + /> + + + ); +} + +type propertiesFormSchemaType = z.infer; From c7215217891f0cba4515403e9a878d56b3288330 Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 16:35:07 +0100 Subject: [PATCH 06/40] clean build --- Examples/ImageUpload.tsx | 157 ---------------- Examples/RatingScale.tsx | 189 ------------------- package-lock.json | 49 +++++ package.json | 1 + src/components/field/NewElementField.tsx | 230 ----------------------- tailwind.config.ts | 2 +- 6 files changed, 51 insertions(+), 577 deletions(-) delete mode 100644 Examples/ImageUpload.tsx delete mode 100644 Examples/RatingScale.tsx delete mode 100644 src/components/field/NewElementField.tsx diff --git a/Examples/ImageUpload.tsx b/Examples/ImageUpload.tsx deleted file mode 100644 index 3e37d09..0000000 --- a/Examples/ImageUpload.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState, useRef } from 'react'; - -const ImageUpload = ({ - prompt = "Upload an image", - buttonText = "Choose File", - width = "w-96", - height = "h-64", - maxDimension = 800, - onImageSelect = (file) => console.log('Image selected:', file) -}) => { - const [previewUrl, setPreviewUrl] = useState(null); - const [fileName, setFileName] = useState("No file chosen"); - const fileInputRef = useRef(null); - - const resizeImage = (originalFile) => { - return new Promise((resolve) => { - const img = new Image(); - img.src = URL.createObjectURL(originalFile); - - img.onload = () => { - // Calculate new dimensions - let newWidth = img.width; - let newHeight = img.height; - - if (img.width > maxDimension || img.height > maxDimension) { - if (img.width > img.height) { - newWidth = maxDimension; - newHeight = (img.height / img.width) * maxDimension; - } else { - newHeight = maxDimension; - newWidth = (img.width / img.height) * maxDimension; - } - } - - // Create canvas and resize - const canvas = document.createElement('canvas'); - canvas.width = newWidth; - canvas.height = newHeight; - - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, newWidth, newHeight); - - // Convert to file - canvas.toBlob((blob) => { - // Create new file with original name - const resizedFile = new File([blob], originalFile.name, { - type: originalFile.type, - lastModified: Date.now(), - }); - - resolve(resizedFile); - }, originalFile.type); - - // Clean up - URL.revokeObjectURL(img.src); - }; - }); - }; - - const handleFileSelect = async (event) => { - const file = event.target.files[0]; - if (file && file.type.startsWith('image/')) { - setFileName(file.name); - - // Create preview - const reader = new FileReader(); - reader.onload = () => { - setPreviewUrl(reader.result); - }; - reader.readAsDataURL(file); - - // Process and resize image if needed - const img = new Image(); - img.src = URL.createObjectURL(file); - - img.onload = async () => { - let finalFile = file; - - // Only resize if image exceeds max dimensions - if (img.width > maxDimension || img.height > maxDimension) { - finalFile = await resizeImage(file); - console.log(`Image resized from ${img.width}x${img.height} to ${finalFile.width}x${finalFile.height}`); - } - - URL.revokeObjectURL(img.src); - onImageSelect(finalFile); - }; - } - }; - - const handleButtonClick = () => { - fileInputRef.current?.click(); - }; - - return ( -
- {/* Image Preview Area */} -
- {previewUrl ? ( - Preview - ) : ( -
- - - -
- )} -
- - {/* Prompt Text */} -
- {prompt} -
- - {/* DaisyUI-style File Input */} -
- -
-
- {buttonText} -
-
- {fileName} -
-
-
-
- ); -}; - -export default ImageUpload; \ No newline at end of file diff --git a/Examples/RatingScale.tsx b/Examples/RatingScale.tsx deleted file mode 100644 index 18035d1..0000000 --- a/Examples/RatingScale.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, { useState } from 'react'; - -const solidColorSchemes = { - blue: { - selected: 'bg-blue-500 border-blue-600', - hover: 'hover:border-blue-400', - text: 'text-blue-600' - }, - green: { - selected: 'bg-green-500 border-green-600', - hover: 'hover:border-green-400', - text: 'text-green-600' - }, - purple: { - selected: 'bg-purple-500 border-purple-600', - hover: 'hover:border-purple-400', - text: 'text-purple-600' - }, - red: { - selected: 'bg-red-500 border-red-600', - hover: 'hover:border-red-400', - text: 'text-red-600' - }, - amber: { - selected: 'bg-amber-500 border-amber-600', - hover: 'hover:border-amber-400', - text: 'text-amber-600' - } -}; - -const gradientColorSchemes = { - severity: { - colors: ['green', 'yellow', 'red'], - labels: { - start: { color: 'text-green-600', hover: 'hover:border-green-400' }, - middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, - end: { color: 'text-red-600', hover: 'hover:border-red-400' } - } - }, - satisfaction: { - colors: ['red', 'yellow', 'green'], - labels: { - start: { color: 'text-red-600', hover: 'hover:border-red-400' }, - middle: { color: 'text-yellow-600', hover: 'hover:border-yellow-400' }, - end: { color: 'text-green-600', hover: 'hover:border-green-400' } - } - }, - temperature: { - colors: ['blue', 'green', 'red'], - labels: { - start: { color: 'text-blue-600', hover: 'hover:border-blue-400' }, - middle: { color: 'text-green-600', hover: 'hover:border-green-400' }, - end: { color: 'text-red-600', hover: 'hover:border-red-400' } - } - } -}; - -const getColorForValue = (value, minValue, maxValue, gradientScheme) => { - const position = (value - minValue) / (maxValue - minValue); - const colors = gradientColorSchemes[gradientScheme]?.colors; - - if (!colors) return null; - - if (position <= 0.33) { - return { - selected: `bg-${colors[0]}-500 border-${colors[0]}-600`, - hover: `hover:border-${colors[0]}-400` - }; - } else if (position <= 0.66) { - return { - selected: `bg-${colors[1]}-500 border-${colors[1]}-600`, - hover: `hover:border-${colors[1]}-400` - }; - } else { - return { - selected: `bg-${colors[2]}-500 border-${colors[2]}-600`, - hover: `hover:border-${colors[2]}-400` - }; - } -}; - -const OneToTen = ({ - question = "Rate your pain level", - minLabel = "Mild", - midLabel = "Moderate", - maxLabel = "Severe", - minValue = 0, - maxValue = 10, - defaultValue = null, - colorScheme = 'blue', - gradientScheme = null, - onChange = (value) => console.log('Selected value:', value) -}) => { - const [selectedValue, setSelectedValue] = useState(defaultValue); - const [hoveredValue, setHoveredValue] = useState(null); - - const useGradient = gradientScheme && gradientColorSchemes[gradientScheme]; - const gradientLabels = useGradient ? gradientColorSchemes[gradientScheme].labels : null; - const solidColors = solidColorSchemes[colorScheme] || solidColorSchemes.blue; - - // Calculate the range of values - const values = Array.from( - { length: maxValue - minValue + 1 }, - (_, i) => i + minValue - ); - - const handleSelection = (value) => { - setSelectedValue(value); - onChange(value); - }; - - const getButtonColors = (value) => { - if (useGradient) { - return getColorForValue(value, minValue, maxValue, gradientScheme); - } - return { - selected: solidColors.selected, - hover: solidColors.hover - }; - }; - - const getLabelColor = (position) => { - if (!useGradient) return solidColors.text; - - switch(position) { - case 'start': return gradientLabels.start.color; - case 'middle': return gradientLabels.middle.color; - case 'end': return gradientLabels.end.color; - default: return solidColors.text; - } - }; - - return ( -
- {/* Question Label */} -
-

{question}

-
- - {/* Radio Button Group */} -
- {values.map((value) => { - const colors = getButtonColors(value); - return ( -
setHoveredValue(value)} - onMouseLeave={() => setHoveredValue(null)} - > - -
- ); - })} -
- - {/* Label Row */} -
- - {minLabel} - - minValue + (maxValue - minValue) / 3 && hoveredValue < maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> - {midLabel} - - = maxValue - (maxValue - minValue) / 3 ? 'scale-110 font-medium' : ''}`}> - {maxLabel} - -
-
- ); -}; - -export default OneToTen; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c01c4b3..dabe951 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "daisyui": "^4.12.23", "eslint": "^8", "eslint-config-next": "14.1.3", "postcss": "^8", @@ -3142,6 +3143,17 @@ "node": ">= 8" } }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3159,6 +3171,36 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "devOptional": true }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.23.tgz", + "integrity": "sha512-EM38duvxutJ5PD65lO/AFMpcw+9qEy6XAZrTpzp7WyaPeO/l+F/Qiq0ECHHmFNcFXh5aVoALY4MGrrxtCiaQCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3983,6 +4025,13 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", diff --git a/package.json b/package.json index 3a90e06..030c75f 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", + "daisyui": "^4.12.23", "eslint": "^8", "eslint-config-next": "14.1.3", "postcss": "^8", diff --git a/src/components/field/NewElementField.tsx b/src/components/field/NewElementField.tsx deleted file mode 100644 index 203a5c1..0000000 --- a/src/components/field/NewElementField.tsx +++ /dev/null @@ -1,230 +0,0 @@ -"use client"; - -import { ElementsType, FormElement, FormElementInstance } from "../FormElements"; -import { Label } from "../ui/label"; -import { Input } from "../ui/input"; -import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form"; -import { BsTextareaResize } from "react-icons/bs"; -import useDesigner from "@/hooks/useDesigner"; - -// Define the extra attributes your element needs -type CustomInstance = FormElementInstance & { - extraAttributes: { - label: string; - helperText: string; - required: boolean; - placeholder: string; - }; -}; - -// Define validation schema for properties form -const propertiesSchema = z.object({ - label: z.string().min(2).max(50), - helperText: z.string().max(200), - required: z.boolean().default(false), - placeholder: z.string().max(50), -}); - -export const NewElementFormElement: FormElement = { - type: "NewElementField", - construct: (id: string) => ({ - id, - type: "NewElementField", - extraAttributes: { - label: "New Element", - helperText: "Helper text", - required: false, - placeholder: "Placeholder", - }, - }), - - designerBtnElement: { - icon: BsTextareaResize, - label: "New Element", - }, - - designerComponent: DesignerComponent, - formComponent: FormComponent, - propertiesComponent: PropertiesComponent, - - validate: (formElement: FormElementInstance, currentValue: string): boolean => { - const element = formElement as CustomInstance; - if (element.extraAttributes.required) { - return currentValue.length > 0; - } - return true; - }, -}; - -function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { - const element = elementInstance as CustomInstance; - const { label, helperText, required, placeholder } = element.extraAttributes; - return ( -
- - - {helperText &&

{helperText}

} -
- ); -} - -function FormComponent({ - elementInstance, - submitValue, - isInvalid, - defaultValue, -}: { - elementInstance: FormElementInstance; - submitValue?: (key: string, value: string) => void; - isInvalid?: boolean; - defaultValue?: string; -}) { - const element = elementInstance as CustomInstance; - const { label, helperText, required, placeholder } = element.extraAttributes; - - return ( -
- - submitValue?.(element.id, e.target.value)} - defaultValue={defaultValue} - /> - {helperText &&

{helperText}

} -
- ); -} - -type propertiesFormSchemaType = z.infer; - -function PropertiesComponent({ elementInstance }: { elementInstance: FormElementInstance }) { - const element = elementInstance as CustomInstance; - const { updateElement } = useDesigner(); - const form = useForm({ - resolver: zodResolver(propertiesSchema), - mode: "onBlur", - defaultValues: { - label: element.extraAttributes.label, - helperText: element.extraAttributes.helperText, - required: element.extraAttributes.required, - placeholder: element.extraAttributes.placeholder, - }, - }); - - useEffect(() => { - form.reset(element.extraAttributes); - }, [element, form]); - - function applyChanges(values: propertiesFormSchemaType) { - const { label, helperText, required, placeholder } = values; - updateElement(element.id, { - ...element, - extraAttributes: { - label, - helperText, - required, - placeholder, - }, - }); - } - - return ( -
- { - e.preventDefault(); - }} - className="space-y-3" - > - ( - - Label - - { - if (e.key === "Enter") e.currentTarget.blur(); - }} - /> - - - - )} - /> - - ( - - Placeholder - - { - if (e.key === "Enter") e.currentTarget.blur(); - }} - /> - - - - )} - /> - - ( - - Helper text - - { - if (e.key === "Enter") e.currentTarget.blur(); - }} - /> - - - - )} - /> - - ( - -
- Required -
- - - -
- )} - /> - - - ); -} diff --git a/tailwind.config.ts b/tailwind.config.ts index 84287e8..6678914 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -74,7 +74,7 @@ const config = { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [require("tailwindcss-animate"),require('daisyui')], } satisfies Config export default config \ No newline at end of file From f417d151a2358c62ac30711832a1e1ac27bc8a2c Mon Sep 17 00:00:00 2001 From: Labb Realheart Date: Thu, 23 Jan 2025 16:58:59 +0100 Subject: [PATCH 07/40] theming --- .../migration.sql | 2 + prisma/schema.prisma | 1 + src/action/form.ts | 7 +++- src/app/submit/[formUrl]/page.tsx | 6 ++- src/components/DesignerSidebar.tsx | 8 ++-- src/components/FormBuilder.tsx | 5 ++- src/components/FormSettings.tsx | 40 +++++++++++++++++++ src/components/FormSubmitComponent.tsx | 26 +++++++++--- src/components/PreviewDialogBtn.tsx | 10 +++-- src/components/PropertiesFormSidebar.tsx | 22 ++++++++-- src/components/SaveFormBtn.tsx | 6 +-- src/components/field/TextField.tsx | 18 +++++++-- src/context/DesignerContext.tsx | 11 +++-- src/schemas/form.ts | 40 +++++++++++++++++++ 14 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 prisma/migrations/20250123154420_add_theme_field/migration.sql create mode 100644 src/components/FormSettings.tsx diff --git a/prisma/migrations/20250123154420_add_theme_field/migration.sql b/prisma/migrations/20250123154420_add_theme_field/migration.sql new file mode 100644 index 0000000..363df71 --- /dev/null +++ b/prisma/migrations/20250123154420_add_theme_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Form" ADD COLUMN "theme" TEXT NOT NULL DEFAULT 'default'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3d5aaa..7c445da 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Form { name String description String @default("") content String @default("[]") + theme String @default("default") // Theme identifier for the form visits Int @default(0) submissions Int @default(0) diff --git a/src/action/form.ts b/src/action/form.ts index 6cdbe0a..61f4825 100644 --- a/src/action/form.ts +++ b/src/action/form.ts @@ -52,13 +52,14 @@ export async function CreateForm(data: formSchemaType) { throw new UserNotFoundErr(); } - const { name, description } = data; + const { name, description, theme } = data; const form = await prisma.form.create({ data: { userId: user.id, name, description, + theme: theme || "default", }, }); @@ -143,7 +144,7 @@ export async function GetFormById(id: number) { }); } -export async function UpdateFormContent(id: number, jsonContent: string) { +export async function UpdateFormContent(id: number, jsonContent: string, theme?: string) { const user = await currentUser(); if (!user) { throw new UserNotFoundErr(); @@ -156,6 +157,7 @@ export async function UpdateFormContent(id: number, jsonContent: string) { }, data: { content: jsonContent, + ...(theme && { theme }), }, }); } @@ -181,6 +183,7 @@ export async function GetFormContentByUrl(formUrl: string) { return await prisma.form.update({ select: { content: true, + theme: true, }, data: { visits: { diff --git a/src/app/submit/[formUrl]/page.tsx b/src/app/submit/[formUrl]/page.tsx index 1ff3ba0..f45c7cc 100644 --- a/src/app/submit/[formUrl]/page.tsx +++ b/src/app/submit/[formUrl]/page.tsx @@ -1,6 +1,7 @@ import { GetFormContentByUrl } from "@/action/form"; import { FormElementInstance } from "@/components/FormElements"; import FormSubmitComponent from "@/components/FormSubmitComponent"; +import { FormTheme } from "@/schemas/form"; import React from "react"; async function SubmitPage({ @@ -17,8 +18,9 @@ async function SubmitPage({ } const formContent = JSON.parse(form.content) as FormElementInstance[]; + const theme = (form.theme || "default") as FormTheme; - return ; + return ; } -export default SubmitPage; \ No newline at end of file +export default SubmitPage; diff --git a/src/components/DesignerSidebar.tsx b/src/components/DesignerSidebar.tsx index cc2e483..cc0f9fb 100644 --- a/src/components/DesignerSidebar.tsx +++ b/src/components/DesignerSidebar.tsx @@ -7,10 +7,12 @@ function DesignerSidebar() { const { selectedElement } = useDesigner(); return ( ); } -export default DesignerSidebar; \ No newline at end of file +export default DesignerSidebar; diff --git a/src/components/FormBuilder.tsx b/src/components/FormBuilder.tsx index b87d65f..f876aa2 100644 --- a/src/components/FormBuilder.tsx +++ b/src/components/FormBuilder.tsx @@ -19,7 +19,7 @@ import useDesigner from "@/hooks/useDesigner"; import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi"; function FormBuilder({ form }: { form: Form }) { - const { setElements, setSelectedElement } = useDesigner(); + const { setElements, setSelectedElement, setTheme } = useDesigner(); const [isReady, setIsReady] = useState(false); const [isSmallScreen, setIsSmallScreen] = useState(false); @@ -62,6 +62,7 @@ function FormBuilder({ form }: { form: Form }) { const elements = JSON.parse(form.content); setElements(elements); setSelectedElement(null); + setTheme(form.theme || "default"); const readyTimeout = setTimeout(() => setIsReady(true), 500); return () => clearTimeout(readyTimeout); }, [form, setElements, isReady, setSelectedElement]); @@ -173,4 +174,4 @@ function FormBuilder({ form }: { form: Form }) { ); } -export default FormBuilder; \ No newline at end of file +export default FormBuilder; diff --git a/src/components/FormSettings.tsx b/src/components/FormSettings.tsx new file mode 100644 index 0000000..246a9a2 --- /dev/null +++ b/src/components/FormSettings.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { formThemes } from "@/schemas/form"; +import { Label } from "./ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; + +interface FormSettingsProps { + theme: keyof typeof formThemes; + onThemeChange: (theme: keyof typeof formThemes) => void; +} + +function FormSettings({ theme, onThemeChange }: FormSettingsProps) { + return ( +
+
+ + +

+ Choose a theme for your form. This will affect how your form looks when shared. +

+
+
+ ); +} + +export default FormSettings; diff --git a/src/components/FormSubmitComponent.tsx b/src/components/FormSubmitComponent.tsx index 2962482..03edba8 100644 --- a/src/components/FormSubmitComponent.tsx +++ b/src/components/FormSubmitComponent.tsx @@ -2,13 +2,23 @@ import React, { useCallback, useRef, useState, useTransition } from "react"; import { FormElementInstance, FormElements } from "./FormElements"; +import { formThemes } from "@/schemas/form"; +import { cn } from "@/lib/utils"; import { Button } from "./ui/button"; import { HiCursorClick } from "react-icons/hi"; import { toast } from "./ui/use-toast"; import { ImSpinner2 } from "react-icons/im"; import { SubmitForm } from "@/action/form"; -function FormSubmitComponent({ formUrl, content }: { content: FormElementInstance[]; formUrl: string }) { +function FormSubmitComponent({ + formUrl, + content, + theme = "default" +}: { + content: FormElementInstance[]; + formUrl: string; + theme?: keyof typeof formThemes; +}) { const formValues = useRef<{ [key: string]: string }>({}); const formErrors = useRef<{ [key: string]: boolean }>({}); const [renderKey, setRenderKey] = useState(new Date().getTime()); @@ -78,7 +88,10 @@ function FormSubmitComponent({ formUrl, content }: { content: FormElementInstanc
{content.map((element) => { const FormElement = FormElements[element.type].formComponent; @@ -92,8 +105,11 @@ function FormSubmitComponent({ formUrl, content }: { content: FormElementInstanc /> ); })} -
-
+
{elements.map((element) => { const elementType = element.type as ElementsType; // Assert type as ElementsType const FormComponent = FormElements[elementType].formComponent; diff --git a/src/components/PropertiesFormSidebar.tsx b/src/components/PropertiesFormSidebar.tsx index 64fc146..78dd001 100644 --- a/src/components/PropertiesFormSidebar.tsx +++ b/src/components/PropertiesFormSidebar.tsx @@ -1,13 +1,29 @@ import React from "react"; -import { FormElements, ElementsType } from "./FormElements"; // Ensure ElementsType is imported +import { FormElements, ElementsType } from "./FormElements"; import { AiOutlineClose } from "react-icons/ai"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; import useDesigner from "@/hooks/useDesigner"; +import FormSettings from "./FormSettings"; +import { MdSettings } from "react-icons/md"; function PropertiesFormSidebar() { - const { selectedElement, setSelectedElement } = useDesigner(); - if (!selectedElement) return null; + const { selectedElement, setSelectedElement, theme, setTheme } = useDesigner(); + + if (!selectedElement) { + return ( +
+
+
+ +

Form settings

+
+
+ + +
+ ); + } const elementType = selectedElement?.type as ElementsType; // Assert type as ElementsType const PropertiesForm = FormElements[elementType].propertiesComponent; diff --git a/src/components/SaveFormBtn.tsx b/src/components/SaveFormBtn.tsx index 9852095..23d8951 100644 --- a/src/components/SaveFormBtn.tsx +++ b/src/components/SaveFormBtn.tsx @@ -7,13 +7,13 @@ import { FaSpinner } from "react-icons/fa"; import { UpdateFormContent } from "@/action/form"; function SaveFormBtn({ id }: { id: number }) { - const { elements } = useDesigner(); + const { elements, theme } = useDesigner(); const [loading, startTransition] = useTransition(); const updateFormContent = async () => { try { const jsonElements = JSON.stringify(elements); - await UpdateFormContent(id, jsonElements); + await UpdateFormContent(id, jsonElements, theme); toast({ title: "Success", description: "Your form has been saved", @@ -42,4 +42,4 @@ function SaveFormBtn({ id }: { id: number }) { ); } -export default SaveFormBtn; \ No newline at end of file +export default SaveFormBtn; diff --git a/src/components/field/TextField.tsx b/src/components/field/TextField.tsx index da64747..44f0816 100644 --- a/src/components/field/TextField.tsx +++ b/src/components/field/TextField.tsx @@ -13,6 +13,7 @@ import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For import { Switch } from "../ui/switch"; import { cn } from "@/lib/utils"; import useDesigner from "@/hooks/useDesigner"; +import { formThemes } from "@/schemas/form"; const type: ElementsType = "TextField"; @@ -61,6 +62,7 @@ type CustomInstance = FormElementInstance & { function DesignerComponent({ elementInstance }: { elementInstance: FormElementInstance }) { const element = elementInstance as CustomInstance; + const { theme } = useDesigner(); const { label, required, placeHolder, helperText } = element.extraAttributes; return (
@@ -68,7 +70,12 @@ function DesignerComponent({ elementInstance }: { elementInstance: FormElementIn {label} {required && "*"} - + {helperText &&

{helperText}

}
); @@ -86,7 +93,7 @@ function FormComponent({ defaultValue?: string; }) { const element = elementInstance as CustomInstance; - + const { theme } = useDesigner(); const [value, setValue] = useState(defaultValue || ""); const [error, setError] = useState(false); @@ -102,7 +109,10 @@ function FormComponent({ {required && "*"} setValue(e.target.value)} onBlur={(e) => { @@ -243,4 +253,4 @@ function PropertiesComponent({ elementInstance }: { elementInstance: FormElement ); -} \ No newline at end of file +} diff --git a/src/context/DesignerContext.tsx b/src/context/DesignerContext.tsx index feae0ba..51c1842 100644 --- a/src/context/DesignerContext.tsx +++ b/src/context/DesignerContext.tsx @@ -1,6 +1,7 @@ "use client"; import { FormElementInstance } from "@/components/FormElements"; +import { formThemes } from "@/schemas/form"; import { Dispatch, ReactNode, SetStateAction, createContext, useState } from "react"; type DesignerContextType = { @@ -13,6 +14,9 @@ type DesignerContextType = { setSelectedElement: Dispatch>; updateElement: (id: string, element: FormElementInstance) => void; + + theme: keyof typeof formThemes; + setTheme: Dispatch>; }; export const DesignerContext = createContext(null); @@ -20,6 +24,7 @@ export const DesignerContext = createContext(null); export default function DesignerContextProvider({ children }: { children: ReactNode }) { const [elements, setElements] = useState([]); const [selectedElement, setSelectedElement] = useState(null); + const [theme, setTheme] = useState("default"); const addElement = (index: number, element: FormElementInstance) => { setElements((prev) => { @@ -49,14 +54,14 @@ export default function DesignerContextProvider({ children }: { children: ReactN setElements, addElement, removeElement, - selectedElement, setSelectedElement, - updateElement, + theme, + setTheme, }} > {children} ); -} \ No newline at end of file +} diff --git a/src/schemas/form.ts b/src/schemas/form.ts index 82b8d85..cd416f1 100644 --- a/src/schemas/form.ts +++ b/src/schemas/form.ts @@ -3,6 +3,46 @@ import { z } from "zod"; export const formSchema = z.object({ name: z.string().min(4), description: z.string().optional(), + theme: z.string().default("default"), }); +// Available themes for forms +export const formThemes = { + default: { + name: "Default", + styles: { + background: "bg-background", + text: "text-foreground", + border: "border-border", + input: "bg-background border-input", + primary: "bg-primary text-primary-foreground", + muted: "bg-muted text-muted-foreground" + } + }, + modern: { + name: "Modern", + styles: { + background: "bg-zinc-50 dark:bg-zinc-900", + text: "text-zinc-900 dark:text-zinc-50", + border: "border-zinc-200 dark:border-zinc-700", + input: "bg-white dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600", + primary: "bg-indigo-600 text-white dark:bg-indigo-500", + muted: "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400" + } + }, + elegant: { + name: "Elegant", + styles: { + background: "bg-stone-50 dark:bg-stone-900", + text: "text-stone-900 dark:text-stone-50", + border: "border-stone-200 dark:border-stone-700", + input: "bg-white dark:bg-stone-800 border-stone-300 dark:border-stone-600", + primary: "bg-amber-600 text-white dark:bg-amber-500", + muted: "bg-stone-100 text-stone-500 dark:bg-stone-800 dark:text-stone-400" + } + } +} as const; + +export type FormTheme = keyof typeof formThemes; + export type formSchemaType = z.infer; From e92dc0b54d6c6e2de5f6b8ac86bd8915e681e34f Mon Sep 17 00:00:00 2001 From: bwedding Date: Sun, 26 Jan 2025 13:04:41 +0100 Subject: [PATCH 08/40] Centered image upload --- src/components/field/ImageUploadField.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/field/ImageUploadField.tsx b/src/components/field/ImageUploadField.tsx index 0c07d3b..40710f2 100644 --- a/src/components/field/ImageUploadField.tsx +++ b/src/components/field/ImageUploadField.tsx @@ -75,11 +75,11 @@ function DesignerComponent({ elementInstance }: { elementInstance: FormElementIn const { label, helperText, required, prompt, buttonText, width, height } = element.extraAttributes; return (
-