From 43bacc442caba5fcdfac424f7075be772e07c424 Mon Sep 17 00:00:00 2001 From: younesbenallal Date: Tue, 8 Apr 2025 10:14:39 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20"HTML=20Form=20Generator"?= =?UTF-8?q?=20blog=20post=20(#2011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blog/components/HtmlFormGenerator.tsx | 685 +++++++++++++++ .../app/features/blog/components/mdx.tsx | 2 + .../app/features/pricing/TiersDialog.tsx | 4 +- apps/landing-page/app/lib/router-dev-tool.ts | 2 +- .../blog/best-chatbot-for-wordpress.mdx | 2 +- .../content/blog/chatfuel-alternatives.mdx | 2 +- .../landing-page/content/blog/faq-chatbot.mdx | 2 +- .../content/blog/html-form-generator.mdx | 359 ++++++++ .../content/blog/open-source-chatbots.mdx | 2 +- .../content/blog/webflow-free-templates.mdx | 2 +- .../blog/whatsapp-chatbot-use-cases.mdx | 4 +- apps/landing-page/package.json | 6 +- .../presentation-typebot-save-responses.gif | Bin 0 -> 2468537 bytes bun.lock | 802 ++++-------------- 14 files changed, 1219 insertions(+), 655 deletions(-) create mode 100644 apps/landing-page/app/features/blog/components/HtmlFormGenerator.tsx create mode 100644 apps/landing-page/content/blog/html-form-generator.mdx create mode 100644 apps/landing-page/public/blog-assets/presentation-typebot-save-responses.gif diff --git a/apps/landing-page/app/features/blog/components/HtmlFormGenerator.tsx b/apps/landing-page/app/features/blog/components/HtmlFormGenerator.tsx new file mode 100644 index 000000000..6bbdca90b --- /dev/null +++ b/apps/landing-page/app/features/blog/components/HtmlFormGenerator.tsx @@ -0,0 +1,685 @@ +import { Card } from "@/components/Card"; +import { IconButton } from "@/components/IconButton"; +import { createListCollection } from "@ark-ui/react"; +import { Dialog } from "@ark-ui/react/dialog"; +import { Portal } from "@ark-ui/react/portal"; +import { Button } from "@typebot.io/ui/components/Button"; +import { Input } from "@typebot.io/ui/components/Input"; +import { Select, SelectItem } from "@typebot.io/ui/components/Select"; +import { CloseIcon } from "@typebot.io/ui/icons/CloseIcon"; +import { cx } from "@typebot.io/ui/lib/cva"; +import { useState } from "react"; + +// Types for our form elements +interface FormElement { + id: string; + type: + | "text" + | "email" + | "phone" + | "select" + | "checkbox" + | "textarea" + | "radio" + | "multicheck"; + label: string; + placeholder?: string; + required?: boolean; + width?: "25" | "50" | "75" | "100"; + options?: string[]; // For select/radio/multicheck +} +const WIDTH_OPTIONS = ["25", "50", "75", "100"]; + +// Main component +export const HtmlFormGenerator = () => { + const [formElements, setFormElements] = useState([]); + const [selectedElement, setSelectedElement] = useState(null); + const [showExportModal, setShowExportModal] = useState(false); + const [showLivePreview, setShowLivePreview] = useState(false); + + const addElement = (item: Omit) => { + const newElement: FormElement = { + ...item, + id: Math.random().toString(36).substring(2, 15), + }; + setFormElements([...formElements, newElement]); + }; + + const removeElement = (id: string) => { + setFormElements((elements) => elements.filter((el) => el.id !== id)); + if (selectedElement === id) { + setSelectedElement(null); + } + }; + + const generateHtmlCode = () => { + let html = '
\n'; + formElements.forEach((element) => { + html += '
\n'; + html += ` \n`; + + switch (element.type) { + case "textarea": + html += ` \n`; + break; + case "select": + html += ` \n"; + break; + case "checkbox": + html += ` \n`; + break; + default: + html += ` \n`; + } + + html += "
\n"; + }); + html += + ' \n'; + html += "
"; + return html; + }; + + return ( +
+
+ +

Add input

+ +
+ +
+

Layout

+
+ + +
+
+ +
+ {selectedElement && ( + + el.id === selectedElement) || null + : null + } + onUpdate={(updates) => { + setFormElements((prev) => + prev.map((el) => + el.id === selectedElement ? { ...el, ...updates } : el, + ), + ); + }} + /> + + )} +
+ + setShowExportModal(false)} + /> + + setShowLivePreview(false)} + /> +
+ ); +}; + +// Export Modal Component +const ExportModal = ({ + isOpened, + html, + onClose, +}: { + isOpened: boolean; + html: string; + onClose: () => void; +}) => { + const copyToClipboard = () => { + navigator.clipboard.writeText(html); + }; + + return ( + (!e.open ? onClose() : null)} + > + + + + + Generated HTML + + + + + + +
+                {html}
+              
+ +
+
+
+
+
+ ); +}; + +// Components Palette +const ComponentsPalette = ({ + items, + onAddElement, +}: { + items: Omit[]; + onAddElement: (item: Omit) => void; +}) => { + return ( +
+ {items.map((item, index) => ( + onAddElement(item)} + > +
+ {item.label} +
+
+ ))} +
+ ); +}; + +// Form Canvas with updated styles... +const FormCanvas = ({ + elements, + selectedElement, + setSelectedElement, + onRemoveElement, +}: { + elements: FormElement[]; + selectedElement: string | null; + setSelectedElement: (id: string | null) => void; + onRemoveElement: (id: string) => void; +}) => { + return ( +
+
+ {elements.map((element) => ( + setSelectedElement(element.id)} + onRemove={() => onRemoveElement(element.id)} + /> + ))} + {elements.length === 0 && ( +
+

Your form is empty

+

+ Click on components from the left panel to add them here +

+
+ )} +
+
+ ); +}; + +// Form Element +const FormElement = ({ + element, + isSelected, + onClick, + onRemove, +}: { + element: FormElement; + isSelected: boolean; + onClick: () => void; + onRemove: () => void; +}) => { + return ( + + + + {element.label} + + {element.type === "radio" ? ( +
+ {element.options?.map((option, index) => ( +
+ + + {option} + +
+ ))} +
+ ) : element.type === "multicheck" ? ( +
+ {element.options?.map((option, index) => ( +
+ + + {option} + +
+ ))} +
+ ) : ( + + )} +
+ ); +}; + +// Properties Panel +const PropertiesPanel = ({ + selectedElement, + onUpdate, +}: { + selectedElement: FormElement | null; + onUpdate: (updates: Partial) => void; +}) => { + if (!selectedElement) { + return ( +
+

+ Select an element to edit its properties +

+
+ ); + } + + // Function to handle options changes for select elements + const handleOptionsChange = (optionIndex: number, newValue: string) => { + if (!selectedElement.options) return; + const newOptions = [...selectedElement.options]; + newOptions[optionIndex] = newValue; + onUpdate({ options: newOptions }); + }; + + const addOption = () => { + const newOptions = [ + ...(selectedElement.options || []), + `Option ${(selectedElement.options?.length || 0) + 1}`, + ]; + onUpdate({ options: newOptions }); + }; + + const removeOption = (index: number) => { + if (!selectedElement.options) return; + const newOptions = selectedElement.options.filter((_, i) => i !== index); + onUpdate({ options: newOptions }); + }; + + return ( +
+

Properties

+
+ {/* label Field */} +
+ + onUpdate({ label: e.target.value })} + className="mt-1" + /> +
+ + {/* Placeholder Field (for text, email, phone inputs) */} + {["text", "email", "phone"].includes(selectedElement.type) && ( +
+ + onUpdate({ placeholder: e.target.value })} + className="mt-1" + /> +
+ )} + + {/* Required Field */} +
+ onUpdate({ required: e.target.checked })} + className="h-4 w-4 text-blue-600" + /> + +
+ + {/* Width Selection */} +
+ + Width + + +
+ + {/* Options for Select/Radio/MultiCheck */} + {(selectedElement.type === "select" || + selectedElement.type === "radio" || + selectedElement.type === "multicheck") && ( +
+ + Options + +
+ {selectedElement.options?.map((option, index) => ( +
+ handleOptionsChange(index, e.target.value)} + className="flex-1" + /> + +
+ ))} + +
+
+ )} +
+
+ ); +}; + +// Add new LivePreviewModal component +const LivePreviewModal = ({ + isOpened, + elements, + onClose, +}: { + isOpened: boolean; + elements: FormElement[]; + onClose: () => void; +}) => { + return ( + (!e.open ? onClose() : null)} + > + + + + + + Live Form Preview + + + + + + + +
e.preventDefault()}> + {elements.map((element) => ( +
+ + {element.type === "textarea" ? ( +