@lucifer91299/create-portal-app 1.1.21 → 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.js +731 -7
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -31,7 +31,10 @@ var p = __toESM(require("@clack/prompts"));
31
31
  // src/templates/index.ts
32
32
  function genPackageJson(o) {
33
33
  const deps = {
34
- "@lucifer91299/ui": o.localUiPath ? `file:${o.localUiPath}` : "^1.1.34",
34
+ "@lucifer91299/ui": o.localUiPath ? `file:${o.localUiPath}` : "^1.1.37",
35
+ "@dnd-kit/core": "^6.3.1",
36
+ "@dnd-kit/sortable": "^10.0.0",
37
+ "@dnd-kit/utilities": "^3.2.2",
35
38
  "next": "^16.2.6",
36
39
  "react": "^19.0.0",
37
40
  "react-dom": "^19.0.0",
@@ -423,7 +426,7 @@ export default api
423
426
  `;
424
427
  }
425
428
  function genNavConfig(o) {
426
- return `import { LayoutDashboard, Settings, Users, Layers, ClipboardList } from 'lucide-react'
429
+ return `import { LayoutDashboard, Settings, Users, Layers, ClipboardList, FormInput } from 'lucide-react'
427
430
  import type { NavGroup } from '@lucifer91299/ui'
428
431
 
429
432
  export const navGroups: NavGroup[] = [
@@ -431,10 +434,11 @@ export const navGroups: NavGroup[] = [
431
434
  heading: 'Main',
432
435
  groupIcon: <LayoutDashboard className="h-3.5 w-3.5" />,
433
436
  items: [
434
- { label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> },
435
- { label: 'Users', href: '/dashboard/users', icon: <Users className="h-4 w-4" /> },
436
- { label: 'Components', href: '/dashboard/components', icon: <Layers className="h-4 w-4" /> },
437
- { label: 'Onboarding', href: '/dashboard/onboarding', icon: <ClipboardList className="h-4 w-4" /> },
437
+ { label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> },
438
+ { label: 'Users', href: '/dashboard/users', icon: <Users className="h-4 w-4" /> },
439
+ { label: 'Form Builder', href: '/dashboard/form-builder', icon: <FormInput className="h-4 w-4" /> },
440
+ { label: 'Components', href: '/dashboard/components', icon: <Layers className="h-4 w-4" /> },
441
+ { label: 'Onboarding', href: '/dashboard/onboarding', icon: <ClipboardList className="h-4 w-4" /> },
438
442
  ],
439
443
  },
440
444
  {
@@ -876,7 +880,7 @@ function genComponentsShowcasePage() {
876
880
 
877
881
  import React, { useState } from 'react'
878
882
  import {
879
- PageShell, Breadcrumbs, Card, TricolorBar,
883
+ PageShell, Breadcrumbs, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, TricolorBar,
880
884
  Badge, StatusBadge, AlertBanner, LoadingSpinner,
881
885
  Button, Input, Select, Textarea, Switch, Checkbox, RadioGroup, DatePicker, DateTimePicker,
882
886
  Dialog, Tooltip, Tabs, TabsList, TabsTrigger, TabsContent,
@@ -1083,6 +1087,39 @@ export default function ComponentsPage() {
1083
1087
  </div>
1084
1088
  </Section>
1085
1089
 
1090
+ {/* \u2500\u2500 Card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
1091
+ <Section id="cards" title="Card" subtitle="Composable card layout \u2014 root \xB7 header \xB7 title \xB7 description \xB7 content \xB7 footer.">
1092
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
1093
+ <Card>
1094
+ <CardHeader>
1095
+ <CardTitle>Basic card</CardTitle>
1096
+ <CardDescription>Header with title and description</CardDescription>
1097
+ </CardHeader>
1098
+ <CardContent>
1099
+ <p className="text-callout text-label-secondary">Body content goes inside CardContent. Combine with CardHeader and CardFooter as needed.</p>
1100
+ </CardContent>
1101
+ <CardFooter className="flex justify-end gap-2">
1102
+ <Button variant="ghost" size="sm">Cancel</Button>
1103
+ <Button variant="primary" size="sm">Save</Button>
1104
+ </CardFooter>
1105
+ </Card>
1106
+
1107
+ <Card variant="elevated">
1108
+ <CardHeader>
1109
+ <CardTitle>Elevated variant</CardTitle>
1110
+ <CardDescription>Use variant="elevated" for a prominent shadow</CardDescription>
1111
+ </CardHeader>
1112
+ <CardContent className="space-y-3">
1113
+ <Input label="Full name" placeholder="Priya Mehta" />
1114
+ <Input label="Email" type="email" placeholder="priya@example.com" />
1115
+ </CardContent>
1116
+ <CardFooter>
1117
+ <Button variant="primary" size="sm" className="w-full">Submit</Button>
1118
+ </CardFooter>
1119
+ </Card>
1120
+ </div>
1121
+ </Section>
1122
+
1086
1123
  {/* \u2500\u2500 Avatar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */}
1087
1124
  <Section id="avatar" title="Avatar & AvatarGroup" subtitle="User profile images with initials fallback.">
1088
1125
  <Card className="p-5 space-y-5">
@@ -1831,6 +1868,692 @@ export default function ComponentsPage() {
1831
1868
  }
1832
1869
  `;
1833
1870
  }
1871
+ function genFormBuilderPage() {
1872
+ return `'use client'
1873
+
1874
+ import React, { useState, useRef, useEffect } from 'react'
1875
+ import {
1876
+ DndContext, closestCenter, KeyboardSensor, PointerSensor,
1877
+ useSensor, useSensors,
1878
+ } from '@dnd-kit/core'
1879
+ import type { DragEndEvent } from '@dnd-kit/core'
1880
+ import {
1881
+ arrayMove, SortableContext, sortableKeyboardCoordinates,
1882
+ verticalListSortingStrategy, useSortable,
1883
+ } from '@dnd-kit/sortable'
1884
+ import { CSS } from '@dnd-kit/utilities'
1885
+ import {
1886
+ GripVertical, Copy, Trash2, Plus, X,
1887
+ AlignLeft, AlignJustify, Calendar, Paperclip,
1888
+ SeparatorHorizontal, CircleDot, CheckSquare, ChevronDownSquare,
1889
+ ChevronDown, ChevronLeft, ChevronRight,
1890
+ BookOpen, Save, Send, Settings, ListChecks, ShieldCheck, MapPin,
1891
+ } from 'lucide-react'
1892
+ import { Card, CardContent, Button, Input, Select, Textarea, Switch } from '@lucifer91299/ui'
1893
+
1894
+ // \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1895
+
1896
+ type QuestionType = 'short_answer' | 'paragraph' | 'multiple_choice' | 'checkboxes' | 'dropdown' | 'date' | 'file_upload' | 'section_header'
1897
+ type PresetType = 'name' | 'email' | 'contact_number' | 'aadhar_number' | 'residential_address' | 'date_of_birth' | 'state' | 'gender'
1898
+
1899
+ interface QuestionObject {
1900
+ id: string
1901
+ type: QuestionType
1902
+ label: string
1903
+ description?: string
1904
+ required: boolean
1905
+ options?: string[]
1906
+ order: number
1907
+ preset?: PresetType
1908
+ }
1909
+
1910
+ interface PresetSelection {
1911
+ type: QuestionType
1912
+ preset?: PresetType
1913
+ }
1914
+
1915
+ // \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1916
+
1917
+ const INDIAN_STATES = [
1918
+ 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh',
1919
+ 'Goa', 'Gujarat', 'Haryana', 'Himachal Pradesh', 'Jharkhand', 'Karnataka',
1920
+ 'Kerala', 'Madhya Pradesh', 'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram',
1921
+ 'Nagaland', 'Odisha', 'Punjab', 'Rajasthan', 'Sikkim', 'Tamil Nadu',
1922
+ 'Telangana', 'Tripura', 'Uttar Pradesh', 'Uttarakhand', 'West Bengal',
1923
+ 'Delhi', 'Jammu & Kashmir', 'Ladakh',
1924
+ ]
1925
+
1926
+ const GENDER_OPTIONS = ['male', 'female', 'other']
1927
+
1928
+ const PRESET_DEFAULT_LABELS: Record<string, string> = {
1929
+ name: 'Full Name', email: 'Email Address', contact_number: 'Contact Number',
1930
+ aadhar_number: 'Aadhar Card Number', residential_address: 'Residential Address',
1931
+ date_of_birth: 'Date of Birth', state: 'State', gender: 'Gender',
1932
+ }
1933
+
1934
+ const PRESET_VALIDATION_INFO: Record<PresetType, string> = {
1935
+ name: 'Letters, spaces, hyphens, apostrophes, and periods only',
1936
+ email: 'Must be a valid email address',
1937
+ contact_number: '10-digit mobile number starting with 6, 7, 8, or 9',
1938
+ aadhar_number: '12-digit Aadhar number',
1939
+ residential_address: 'Free-text address',
1940
+ date_of_birth: 'Valid past date required',
1941
+ state: 'Must match an entry from the states list',
1942
+ gender: 'Auto-filled from the user profile',
1943
+ }
1944
+
1945
+ // \u2500\u2500 Type options & icons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1946
+
1947
+ interface TypeOption { value: QuestionType; label: string; subItems?: { preset: PresetType; label: string }[] }
1948
+
1949
+ const TYPE_OPTIONS: TypeOption[] = [
1950
+ { value: 'short_answer', label: 'Short Answer', subItems: [{ preset: 'name', label: 'Name' }, { preset: 'email', label: 'Email' }, { preset: 'contact_number', label: 'Contact Number' }, { preset: 'aadhar_number', label: 'Aadhar Number' }] },
1951
+ { value: 'paragraph', label: 'Paragraph', subItems: [{ preset: 'residential_address', label: 'Residential Address' }] },
1952
+ { value: 'dropdown', label: 'Dropdown', subItems: [{ preset: 'state', label: 'State' }] },
1953
+ { value: 'multiple_choice', label: 'Multiple Choice', subItems: [{ preset: 'gender', label: 'Gender' }] },
1954
+ { value: 'date', label: 'Date', subItems: [{ preset: 'date_of_birth', label: 'Date of Birth' }] },
1955
+ { value: 'checkboxes', label: 'Checkboxes' },
1956
+ { value: 'file_upload', label: 'File Upload' },
1957
+ { value: 'section_header', label: 'Section Header' },
1958
+ ]
1959
+
1960
+ const TYPE_ICONS: Record<QuestionType, React.ReactNode> = {
1961
+ short_answer: <AlignLeft className="w-3.5 h-3.5" />,
1962
+ paragraph: <AlignJustify className="w-3.5 h-3.5" />,
1963
+ multiple_choice: <CircleDot className="w-3.5 h-3.5" />,
1964
+ checkboxes: <CheckSquare className="w-3.5 h-3.5" />,
1965
+ dropdown: <ChevronDownSquare className="w-3.5 h-3.5" />,
1966
+ date: <Calendar className="w-3.5 h-3.5" />,
1967
+ file_upload: <Paperclip className="w-3.5 h-3.5" />,
1968
+ section_header: <SeparatorHorizontal className="w-3.5 h-3.5" />,
1969
+ }
1970
+
1971
+ // \u2500\u2500 PresetTypeSelector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1972
+
1973
+ function PresetTypeSelector({ currentType, currentPreset, onSelect }: {
1974
+ currentType: QuestionType
1975
+ currentPreset?: PresetType
1976
+ onSelect: (s: PresetSelection) => void
1977
+ }) {
1978
+ const [open, setOpen] = useState(false)
1979
+ const [hoveredType, setHoveredType] = useState<QuestionType | null>(null)
1980
+ const [flyoutSide, setFlyoutSide] = useState<'left' | 'right'>('left')
1981
+ const [expandedType, setExpandedType] = useState<QuestionType | null>(null)
1982
+ const [isTouch, setIsTouch] = useState(false)
1983
+ const containerRef = useRef<HTMLDivElement>(null)
1984
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
1985
+
1986
+ useEffect(() => {
1987
+ const mq = window.matchMedia('(hover: none) and (pointer: coarse)')
1988
+ setIsTouch(mq.matches)
1989
+ const h = (e: MediaQueryListEvent) => setIsTouch(e.matches)
1990
+ mq.addEventListener('change', h)
1991
+ return () => mq.removeEventListener('change', h)
1992
+ }, [])
1993
+
1994
+ useEffect(() => {
1995
+ if (!open) return
1996
+ const h = (e: PointerEvent) => {
1997
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
1998
+ setOpen(false); setHoveredType(null); setExpandedType(null)
1999
+ }
2000
+ }
2001
+ document.addEventListener('pointerdown', h)
2002
+ return () => document.removeEventListener('pointerdown', h)
2003
+ }, [open])
2004
+
2005
+ useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current) }, [])
2006
+
2007
+ const subLabel = TYPE_OPTIONS.find(o => o.value === currentType)?.subItems?.find(s => s.preset === currentPreset)?.label
2008
+ const btnLabel = subLabel
2009
+ ? \`\${TYPE_OPTIONS.find(o => o.value === currentType)?.label} \u203A \${subLabel}\`
2010
+ : (TYPE_OPTIONS.find(o => o.value === currentType)?.label ?? currentType)
2011
+
2012
+ return (
2013
+ <div ref={containerRef} className="relative">
2014
+ <button type="button"
2015
+ onClick={() => { setOpen(o => !o); setHoveredType(null); setExpandedType(null) }}
2016
+ className="flex items-center gap-2 pl-2.5 pr-2 py-1.5 text-sm font-medium border border-gray-200 rounded-lg bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50 transition-all min-w-[180px] focus:outline-none"
2017
+ >
2018
+ <span className="text-gray-400">{TYPE_ICONS[currentType]}</span>
2019
+ <span className="flex-1 text-left truncate">{btnLabel}</span>
2020
+ <ChevronDown className={\`w-3.5 h-3.5 text-gray-400 flex-shrink-0 transition-transform duration-200 \${open ? 'rotate-180' : ''}\`} />
2021
+ </button>
2022
+
2023
+ {open && (
2024
+ <div className="absolute right-0 top-full mt-1 z-50 w-52 bg-white border border-gray-200 rounded-xl shadow-lg py-1">
2025
+ {TYPE_OPTIONS.map((opt, idx) => {
2026
+ const isActive = currentType === opt.value && !currentPreset
2027
+ const hasSubs = !!(opt.subItems?.length)
2028
+ const showSep = idx > 0 && !!(TYPE_OPTIONS[idx - 1].subItems?.length) && !hasSubs
2029
+ const isExpanded = isTouch && expandedType === opt.value
2030
+ return (
2031
+ <div key={opt.value}>
2032
+ {showSep && <div className="my-1 border-t border-gray-100" />}
2033
+ <div
2034
+ className="relative"
2035
+ onMouseEnter={!isTouch ? (e) => {
2036
+ if (timerRef.current) clearTimeout(timerRef.current)
2037
+ if (hasSubs) {
2038
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
2039
+ setFlyoutSide(rect.left - 184 > 8 ? 'left' : 'right')
2040
+ timerRef.current = setTimeout(() => setHoveredType(opt.value), 80)
2041
+ } else setHoveredType(null)
2042
+ } : undefined}
2043
+ onMouseLeave={!isTouch ? () => { if (timerRef.current) clearTimeout(timerRef.current) } : undefined}
2044
+ >
2045
+ <button type="button"
2046
+ onClick={() => {
2047
+ onSelect({ type: opt.value, preset: undefined })
2048
+ if (isTouch && hasSubs) { setExpandedType(p => p === opt.value ? null : opt.value) }
2049
+ else { setOpen(false); setHoveredType(null); setExpandedType(null) }
2050
+ }}
2051
+ className={\`w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors \${isActive ? 'bg-gray-100 text-gray-800 font-medium' : 'text-gray-600 hover:bg-gray-50'}\`}
2052
+ >
2053
+ <span className={isActive ? 'text-gray-600' : 'text-gray-400'}>{TYPE_ICONS[opt.value]}</span>
2054
+ <span className="flex-1 text-left">{opt.label}</span>
2055
+ {hasSubs && (isTouch
2056
+ ? <ChevronRight className={\`w-3 h-3 text-gray-300 flex-shrink-0 transition-transform \${isExpanded ? 'rotate-90' : ''}\`} />
2057
+ : <ChevronLeft className="w-3 h-3 text-gray-300 flex-shrink-0" />
2058
+ )}
2059
+ </button>
2060
+
2061
+ {/* Desktop flyout */}
2062
+ {!isTouch && hasSubs && hoveredType === opt.value && (
2063
+ <div
2064
+ className={\`absolute top-0 w-44 bg-white border border-gray-200 rounded-xl shadow-lg py-1 \${flyoutSide === 'left' ? 'right-full mr-1' : 'left-full ml-1'}\`}
2065
+ onMouseEnter={() => { if (timerRef.current) clearTimeout(timerRef.current); setHoveredType(opt.value) }}
2066
+ onMouseLeave={() => setHoveredType(null)}
2067
+ >
2068
+ {opt.subItems!.map(sub => {
2069
+ const isSub = currentType === opt.value && currentPreset === sub.preset
2070
+ return (
2071
+ <button key={sub.preset} type="button"
2072
+ onClick={() => { onSelect({ type: opt.value, preset: sub.preset }); setOpen(false); setHoveredType(null) }}
2073
+ className={\`w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors \${isSub ? 'bg-gray-100 text-gray-800 font-medium' : 'text-gray-600 hover:bg-gray-50'}\`}
2074
+ >
2075
+ <span className={isSub ? 'text-gray-600' : 'text-gray-400'}>{TYPE_ICONS[opt.value]}</span>
2076
+ {sub.label}
2077
+ </button>
2078
+ )
2079
+ })}
2080
+ </div>
2081
+ )}
2082
+ </div>
2083
+
2084
+ {/* Mobile accordion */}
2085
+ {isTouch && hasSubs && isExpanded && (
2086
+ <div className="border-t border-gray-100 bg-gray-50/60">
2087
+ {opt.subItems!.map(sub => {
2088
+ const isSub = currentType === opt.value && currentPreset === sub.preset
2089
+ return (
2090
+ <button key={sub.preset} type="button"
2091
+ onClick={() => { onSelect({ type: opt.value, preset: sub.preset }); setOpen(false); setExpandedType(null) }}
2092
+ className={\`w-full flex items-center gap-2.5 pl-8 pr-3 py-2 text-sm transition-colors \${isSub ? 'bg-gray-100 text-gray-800 font-medium' : 'text-gray-500 hover:bg-gray-100'}\`}
2093
+ >
2094
+ <span className={isSub ? 'text-gray-600' : 'text-gray-400'}>{TYPE_ICONS[opt.value]}</span>
2095
+ {sub.label}
2096
+ </button>
2097
+ )
2098
+ })}
2099
+ </div>
2100
+ )}
2101
+ </div>
2102
+ )
2103
+ })}
2104
+ </div>
2105
+ )}
2106
+ </div>
2107
+ )
2108
+ }
2109
+
2110
+ // \u2500\u2500 SortableQuestionCard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2111
+
2112
+ function SortableQuestionCard({ question, onUpdate, onDuplicate, onDelete, hasError }: {
2113
+ question: QuestionObject
2114
+ onUpdate: (u: Partial<QuestionObject>) => void
2115
+ onDuplicate: () => void
2116
+ onDelete: () => void
2117
+ hasError?: boolean
2118
+ }) {
2119
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
2120
+ useSortable({ id: question.id })
2121
+ const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1 }
2122
+ const isSectionHeader = question.type === 'section_header'
2123
+
2124
+ const addOption = () => onUpdate({ options: [...(question.options ?? []), \`Option \${(question.options?.length ?? 0) + 1}\`] })
2125
+ const updateOption = (i: number, v: string) => { const o = [...(question.options ?? [])]; o[i] = v; onUpdate({ options: o }) }
2126
+ const removeOption = (i: number) => onUpdate({ options: (question.options ?? []).filter((_, idx) => idx !== i) })
2127
+
2128
+ return (
2129
+ <div
2130
+ ref={setNodeRef}
2131
+ style={style}
2132
+ className={\`bg-white border rounded-xl shadow-sm transition-shadow \${isDragging ? 'shadow-lg' : 'hover:shadow-md'} \${hasError ? 'border-red-300' : 'border-gray-200'}\`}
2133
+ >
2134
+ {/* Top bar: drag handle + type selector */}
2135
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50/60 rounded-t-xl">
2136
+ <button type="button" {...attributes} {...listeners}
2137
+ className="text-gray-300 hover:text-gray-500 cursor-grab active:cursor-grabbing touch-none p-0.5"
2138
+ title="Drag to reorder"
2139
+ >
2140
+ <GripVertical className="w-4 h-4" />
2141
+ </button>
2142
+ <span className="text-xs font-medium text-gray-400 select-none">
2143
+ {isSectionHeader ? 'Section Header' : 'Question'}
2144
+ </span>
2145
+ <div className="flex-1" />
2146
+ {!isSectionHeader && (
2147
+ <PresetTypeSelector
2148
+ currentType={question.type}
2149
+ currentPreset={question.preset}
2150
+ onSelect={({ type, preset }) => onUpdate({ type, preset })}
2151
+ />
2152
+ )}
2153
+ </div>
2154
+
2155
+ {/* Label */}
2156
+ <div className="px-4 pt-4 pb-3">
2157
+ {isSectionHeader ? (
2158
+ <input type="text" placeholder="Section title" value={question.label}
2159
+ onChange={e => onUpdate({ label: e.target.value })}
2160
+ className="w-full text-lg font-bold border-0 border-b-2 border-gray-200 focus:border-blue-500 outline-none pb-1.5 bg-transparent placeholder-gray-300"
2161
+ />
2162
+ ) : (
2163
+ <>
2164
+ <label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-400 mb-1.5">
2165
+ Question Label
2166
+ </label>
2167
+ <input type="text" placeholder="e.g. What is your full name?" value={question.label}
2168
+ onChange={e => onUpdate({ label: e.target.value })}
2169
+ className={\`w-full text-sm font-medium rounded-lg px-3 py-2.5 outline-none transition-all border \${
2170
+ hasError && !question.label.trim()
2171
+ ? 'bg-red-50 border-red-300 placeholder-red-300 focus:ring-2 focus:ring-red-100'
2172
+ : 'bg-gray-50 border-gray-200 placeholder-gray-300 focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/10'
2173
+ }\`}
2174
+ />
2175
+ {hasError && !question.label.trim() && (
2176
+ <p className="text-xs text-red-500 mt-1.5 px-0.5">Question label is required before publishing.</p>
2177
+ )}
2178
+ {question.preset && (
2179
+ <div className="flex items-center gap-1.5 mt-2 px-2.5 py-1.5 rounded-lg" style={{ background: 'var(--primary-soft, rgba(0,0,128,0.06))', border: '1px solid var(--primary-border, rgba(0,0,128,0.12))' }}>
2180
+ <ShieldCheck className="w-3.5 h-3.5 flex-shrink-0" style={{ color: 'var(--primary, #000080)' }} />
2181
+ <span className="text-[11px] font-medium" style={{ color: 'var(--primary, #000080)' }}>Validation:</span>
2182
+ <span className="text-[11px]" style={{ color: 'var(--primary, #000080)', opacity: 0.7 }}>{PRESET_VALIDATION_INFO[question.preset]}</span>
2183
+ </div>
2184
+ )}
2185
+ </>
2186
+ )}
2187
+ </div>
2188
+
2189
+ {/* Body */}
2190
+ {!isSectionHeader && (
2191
+ <div className="px-4 pb-3 space-y-3">
2192
+ <div>
2193
+ <label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-400 mb-1.5">
2194
+ Description <span className="normal-case font-normal text-gray-300">(optional)</span>
2195
+ </label>
2196
+ <Textarea
2197
+ value={question.description ?? ''}
2198
+ onChange={e => onUpdate({ description: e.target.value || undefined })}
2199
+ placeholder="Add helper text visible to the applicant..."
2200
+ rows={2}
2201
+ />
2202
+ </div>
2203
+
2204
+ {/* Answer preview */}
2205
+ <div className="pt-1">
2206
+ <div className="flex items-center gap-2 mb-2.5">
2207
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">Answer preview</span>
2208
+ <div className="flex-1 h-px bg-gray-100" />
2209
+ </div>
2210
+
2211
+ {question.type === 'short_answer' && (
2212
+ <div className="flex items-center gap-2.5 px-3.5 py-2.5 border border-gray-300 rounded-lg bg-white shadow-sm">
2213
+ <AlignLeft className="w-4 h-4 text-gray-400 flex-shrink-0" />
2214
+ <span className="text-sm text-gray-400 italic">Short answer text</span>
2215
+ </div>
2216
+ )}
2217
+
2218
+ {question.type === 'paragraph' && (
2219
+ <div className="px-3.5 py-3 border border-gray-300 rounded-lg bg-white shadow-sm space-y-2 min-h-[70px]">
2220
+ {['w-full', 'w-5/6', 'w-3/4', 'w-2/5'].map((w, i) => (
2221
+ <div key={i} className={\`h-2 bg-gray-200 rounded-full \${w}\`} />
2222
+ ))}
2223
+ </div>
2224
+ )}
2225
+
2226
+ {question.type === 'date' && (
2227
+ <div className="inline-flex items-center gap-2.5 px-3.5 py-2.5 border border-gray-300 rounded-lg bg-white shadow-sm text-sm text-gray-500">
2228
+ <Calendar className="w-4 h-4 text-gray-400" />
2229
+ <span>DD / MM / YYYY</span>
2230
+ </div>
2231
+ )}
2232
+
2233
+ {question.type === 'file_upload' && (
2234
+ <div className="py-5 px-4 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50 text-center space-y-1.5">
2235
+ <Paperclip className="w-5 h-5 text-gray-400 mx-auto" />
2236
+ <p className="text-sm font-medium text-gray-500">Click to upload or drag &amp; drop</p>
2237
+ <p className="text-xs text-gray-400">Any file type accepted</p>
2238
+ </div>
2239
+ )}
2240
+
2241
+ {(question.type === 'multiple_choice' || question.type === 'checkboxes') && (
2242
+ <div className="space-y-1.5">
2243
+ {(question.preset === 'gender' ? GENDER_OPTIONS : question.options ?? []).map((opt, i) => (
2244
+ <div key={i} className="flex items-center gap-2.5 bg-white border border-gray-200 rounded-lg px-3 py-2 hover:border-gray-300">
2245
+ {question.type === 'multiple_choice'
2246
+ ? <span className="w-4 h-4 rounded-full border-2 border-gray-300 flex-shrink-0" />
2247
+ : <span className="w-4 h-4 rounded border-2 border-gray-300 flex-shrink-0" />
2248
+ }
2249
+ <input type="text" value={opt} placeholder={\`Option \${i + 1}\`}
2250
+ onChange={e => updateOption(i, e.target.value)}
2251
+ disabled={question.preset === 'gender'}
2252
+ className="flex-1 text-sm outline-none bg-transparent text-gray-700 placeholder-gray-300"
2253
+ />
2254
+ {question.preset !== 'gender' && (
2255
+ <button type="button" onClick={() => removeOption(i)}
2256
+ disabled={(question.options?.length ?? 0) <= 1}
2257
+ className="w-6 h-6 flex items-center justify-center rounded-md text-gray-300 hover:text-red-400 hover:bg-red-50 disabled:opacity-0 transition-all flex-shrink-0"
2258
+ >
2259
+ <X className="w-3.5 h-3.5" />
2260
+ </button>
2261
+ )}
2262
+ </div>
2263
+ ))}
2264
+ {question.preset === 'gender'
2265
+ ? <p className="text-xs text-gray-400 px-1">Gender options are auto-populated.</p>
2266
+ : <button type="button" onClick={addOption}
2267
+ className="w-full flex items-center justify-center gap-1.5 py-2 border border-dashed border-gray-200 rounded-lg text-xs font-medium text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-all"
2268
+ ><Plus className="w-3.5 h-3.5" /> Add option</button>
2269
+ }
2270
+ </div>
2271
+ )}
2272
+
2273
+ {question.type === 'dropdown' && (
2274
+ question.preset === 'state'
2275
+ ? <div className="flex items-center gap-2 px-3 py-2.5 border border-gray-200 rounded-lg bg-gray-50 text-sm text-gray-500">
2276
+ <MapPin className="w-4 h-4 text-gray-400 flex-shrink-0" />
2277
+ <span>States list ({INDIAN_STATES.length} options) \u2014 auto-populated</span>
2278
+ </div>
2279
+ : <div className="space-y-1.5">
2280
+ {(question.options ?? []).map((opt, i) => (
2281
+ <div key={i} className="flex items-center gap-2.5 bg-white border border-gray-200 rounded-lg px-3 py-2 hover:border-gray-300">
2282
+ <span className="w-5 h-5 flex items-center justify-center rounded bg-gray-100 text-[11px] font-semibold text-gray-400 flex-shrink-0">{i + 1}</span>
2283
+ <input type="text" value={opt} placeholder={\`Option \${i + 1}\`}
2284
+ onChange={e => updateOption(i, e.target.value)}
2285
+ className="flex-1 text-sm outline-none bg-transparent text-gray-700 placeholder-gray-300"
2286
+ />
2287
+ <button type="button" onClick={() => removeOption(i)}
2288
+ disabled={(question.options?.length ?? 0) <= 1}
2289
+ className="w-6 h-6 flex items-center justify-center rounded-md text-gray-300 hover:text-red-400 hover:bg-red-50 disabled:opacity-0 transition-all flex-shrink-0"
2290
+ ><X className="w-3.5 h-3.5" /></button>
2291
+ </div>
2292
+ ))}
2293
+ <button type="button" onClick={addOption}
2294
+ className="w-full flex items-center justify-center gap-1.5 py-2 border border-dashed border-gray-200 rounded-lg text-xs font-medium text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-all"
2295
+ ><Plus className="w-3.5 h-3.5" /> Add option</button>
2296
+ </div>
2297
+ )}
2298
+ </div>
2299
+ </div>
2300
+ )}
2301
+
2302
+ {/* Footer */}
2303
+ <div className="flex items-center justify-end gap-1 px-3 py-2 border-t border-gray-100">
2304
+ {!isSectionHeader && (
2305
+ <label className="flex items-center gap-1.5 text-xs text-gray-500 cursor-pointer mr-2 select-none">
2306
+ <input type="checkbox" className="w-3.5 h-3.5"
2307
+ checked={question.required}
2308
+ onChange={e => onUpdate({ required: e.target.checked })}
2309
+ />
2310
+ Required
2311
+ </label>
2312
+ )}
2313
+ <button type="button" onClick={onDuplicate}
2314
+ className="p-1.5 text-gray-400 hover:text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
2315
+ title="Duplicate"
2316
+ ><Copy className="w-4 h-4" /></button>
2317
+ <button type="button" onClick={onDelete}
2318
+ className="p-1.5 text-gray-400 hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors"
2319
+ title="Delete"
2320
+ ><Trash2 className="w-4 h-4" /></button>
2321
+ </div>
2322
+ </div>
2323
+ )
2324
+ }
2325
+
2326
+ // \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2327
+
2328
+ function createQuestion(order: number): QuestionObject {
2329
+ return { id: crypto.randomUUID(), type: 'short_answer', label: '', required: false, order }
2330
+ }
2331
+
2332
+ // \u2500\u2500 FormBuilderPage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2333
+
2334
+ export default function FormBuilderPage() {
2335
+ const [activeTab, setActiveTab] = useState<'questions' | 'settings'>('questions')
2336
+ const [title, setTitle] = useState('')
2337
+ const [description, setDescription] = useState('')
2338
+ const [targetAudience, setTarget] = useState('all')
2339
+ const [paymentRequired, setPayment] = useState(false)
2340
+ const [price, setPrice] = useState('')
2341
+ const [questions, setQuestions] = useState<QuestionObject[]>([createQuestion(0)])
2342
+ const [questionErrors, setQErrors] = useState<Set<string>>(new Set())
2343
+ const [formErrors, setFormErrors] = useState<{ title?: string; price?: string; questions?: string }>({})
2344
+ const [saved, setSaved] = useState(false)
2345
+
2346
+ const sensors = useSensors(
2347
+ useSensor(PointerSensor),
2348
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
2349
+ )
2350
+
2351
+ const handleDragEnd = ({ active, over }: DragEndEvent) => {
2352
+ if (!over || active.id === over.id) return
2353
+ const from = questions.findIndex(q => q.id === active.id)
2354
+ const to = questions.findIndex(q => q.id === over.id)
2355
+ setQuestions(arrayMove(questions, from, to).map((q, i) => ({ ...q, order: i })))
2356
+ }
2357
+
2358
+ const addQuestion = () => setQuestions(p => [...p, createQuestion(p.length)])
2359
+
2360
+ const updateQuestion = (id: string, updates: Partial<QuestionObject>) => {
2361
+ if (updates.label?.trim()) {
2362
+ setQErrors(p => { const n = new Set(p); n.delete(id); return n })
2363
+ if (formErrors.questions) setFormErrors(p => ({ ...p, questions: undefined }))
2364
+ }
2365
+ setQuestions(p => p.map(q => {
2366
+ if (q.id !== id) return q
2367
+ const u = { ...q, ...updates }
2368
+ if (updates.type && updates.type !== q.type && !('preset' in updates)) u.preset = undefined
2369
+ if (!['multiple_choice', 'checkboxes', 'dropdown'].includes(u.type)) u.options = undefined
2370
+ if (['multiple_choice', 'checkboxes', 'dropdown'].includes(u.type) && !u.options?.length && u.preset !== 'state') u.options = ['Option 1']
2371
+ if ('preset' in updates) {
2372
+ if (updates.preset === 'state') u.options = undefined
2373
+ if (updates.preset === 'gender') u.options = GENDER_OPTIONS
2374
+ if (!updates.preset && (q.preset === 'state' || q.preset === 'gender') && ['dropdown', 'multiple_choice'].includes(u.type)) u.options = ['Option 1']
2375
+ const isDefault = !q.label.trim() || Object.values(PRESET_DEFAULT_LABELS).includes(q.label.trim())
2376
+ if (updates.preset && isDefault) u.label = PRESET_DEFAULT_LABELS[updates.preset] ?? u.label
2377
+ else if (!updates.preset && isDefault) u.label = ''
2378
+ }
2379
+ return u
2380
+ }))
2381
+ }
2382
+
2383
+ const duplicateQuestion = (id: string) => {
2384
+ const idx = questions.findIndex(q => q.id === id)
2385
+ const clone = { ...questions[idx], id: crypto.randomUUID(), order: idx + 1 }
2386
+ setQuestions([...questions.slice(0, idx + 1), clone, ...questions.slice(idx + 1)].map((q, i) => ({ ...q, order: i })))
2387
+ }
2388
+
2389
+ const deleteQuestion = (id: string) =>
2390
+ setQuestions(p => p.filter(q => q.id !== id).map((q, i) => ({ ...q, order: i })))
2391
+
2392
+ const validate = (forPublish: boolean) => {
2393
+ const e: typeof formErrors = {}
2394
+ if (!title.trim()) e.title = 'Please enter a form title.'
2395
+ if (paymentRequired && (!price || Number(price) <= 0)) e.price = 'Please enter a valid fee greater than 0.'
2396
+ const emptyIds = forPublish
2397
+ ? questions.filter(q => q.type !== 'section_header' && !q.label.trim()).map(q => q.id)
2398
+ : []
2399
+ if (emptyIds.length) e.questions = 'All questions must have a label before publishing.'
2400
+ return { errors: e, emptyIds }
2401
+ }
2402
+
2403
+ const handleSave = () => {
2404
+ const { errors } = validate(false)
2405
+ if (Object.keys(errors).length) { setFormErrors(errors); return }
2406
+ setFormErrors({})
2407
+ setSaved(true)
2408
+ setTimeout(() => setSaved(false), 3500)
2409
+ // Replace with real API call:
2410
+ console.log('Draft payload:', { title, description, target_audience: targetAudience, payment_required: paymentRequired, price: paymentRequired ? Number(price) : undefined, questions })
2411
+ }
2412
+
2413
+ const handlePublish = () => {
2414
+ const { errors, emptyIds } = validate(true)
2415
+ if (Object.keys(errors).length || emptyIds.length) {
2416
+ setFormErrors(errors); setQErrors(new Set(emptyIds))
2417
+ if (emptyIds.length && activeTab !== 'questions') setActiveTab('questions')
2418
+ return
2419
+ }
2420
+ setFormErrors({}); setQErrors(new Set())
2421
+ setSaved(true)
2422
+ setTimeout(() => setSaved(false), 3500)
2423
+ // Replace with real API call:
2424
+ console.log('Publish payload:', { title, description, target_audience: targetAudience, payment_required: paymentRequired, price: paymentRequired ? Number(price) : undefined, questions })
2425
+ }
2426
+
2427
+ return (
2428
+ <div className="space-y-5 max-w-3xl mx-auto p-4 sm:p-6 pb-24">
2429
+ {/* Header */}
2430
+ <div className="flex flex-wrap items-center gap-3">
2431
+ <div className="w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0"
2432
+ style={{ background: 'var(--primary-soft, rgba(0,0,128,0.07))' }}>
2433
+ <BookOpen className="w-[18px] h-[18px]" style={{ color: 'var(--primary, #000080)' }} />
2434
+ </div>
2435
+ <div>
2436
+ <h1 className="text-xl font-bold text-label-primary leading-tight">Form Builder</h1>
2437
+ <p className="text-xs text-label-tertiary">Build dynamic forms with drag &amp; drop questions</p>
2438
+ </div>
2439
+ </div>
2440
+
2441
+ {saved && (
2442
+ <div className="px-4 py-3 bg-green-50 border border-green-200 rounded-xl text-sm text-green-700 font-medium">
2443
+ \u2713 Form saved \u2014 check the browser console for the payload. Wire up your API in handleSave / handlePublish.
2444
+ </div>
2445
+ )}
2446
+
2447
+ {/* Form title + description card */}
2448
+ <Card className="overflow-hidden">
2449
+ <div className="h-1" style={{ background: 'var(--primary, #000080)' }} />
2450
+ <CardContent className="pt-5 space-y-4">
2451
+ <div>
2452
+ <input type="text" placeholder="Form Title" value={title}
2453
+ onChange={e => { setTitle(e.target.value); if (formErrors.title) setFormErrors(p => ({ ...p, title: undefined })) }}
2454
+ className={\`w-full text-2xl font-bold border-0 border-b-2 focus:outline-none pb-2 bg-transparent placeholder-gray-300 \${formErrors.title ? 'border-red-400' : 'border-gray-200'}\`}
2455
+ />
2456
+ {formErrors.title && <p className="text-red-500 text-sm mt-1">{formErrors.title}</p>}
2457
+ </div>
2458
+ <Textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="Form description (optional)" rows={2} />
2459
+ </CardContent>
2460
+ </Card>
2461
+
2462
+ {/* Tab bar */}
2463
+ <div className="flex border-b border-gray-200">
2464
+ {([
2465
+ { id: 'questions' as const, Icon: ListChecks, label: 'Questions' },
2466
+ { id: 'settings' as const, Icon: Settings, label: 'Settings' },
2467
+ ]).map(({ id, Icon, label }) => (
2468
+ <button key={id} type="button" onClick={() => setActiveTab(id)}
2469
+ className={\`flex items-center gap-2 px-5 py-2.5 text-sm font-medium border-b-2 transition-colors \${activeTab === id ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}\`}
2470
+ style={activeTab === id ? { borderColor: 'var(--primary, #000080)', color: 'var(--primary, #000080)' } : undefined}
2471
+ >
2472
+ <Icon className="w-4 h-4" />{label}
2473
+ </button>
2474
+ ))}
2475
+ </div>
2476
+
2477
+ {/* Questions tab */}
2478
+ {activeTab === 'questions' && (
2479
+ <div className="space-y-5">
2480
+ {formErrors.questions && <p className="text-red-500 text-sm px-1">{formErrors.questions}</p>}
2481
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
2482
+ <SortableContext items={questions.map(q => q.id)} strategy={verticalListSortingStrategy}>
2483
+ {questions.map(q => (
2484
+ <SortableQuestionCard
2485
+ key={q.id}
2486
+ question={q}
2487
+ onUpdate={u => updateQuestion(q.id, u)}
2488
+ onDuplicate={() => duplicateQuestion(q.id)}
2489
+ onDelete={() => deleteQuestion(q.id)}
2490
+ hasError={questionErrors.has(q.id)}
2491
+ />
2492
+ ))}
2493
+ </SortableContext>
2494
+ </DndContext>
2495
+
2496
+ <button type="button" onClick={addQuestion}
2497
+ className="w-full py-3 border-2 border-dashed border-gray-200 rounded-xl text-gray-500 hover:text-blue-600 transition-colors flex items-center justify-center gap-2 text-sm font-medium"
2498
+ onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--primary, #000080)')}
2499
+ onMouseLeave={e => (e.currentTarget.style.borderColor = '')}
2500
+ >
2501
+ <Plus className="w-4 h-4" /> Add Question
2502
+ </button>
2503
+ </div>
2504
+ )}
2505
+
2506
+ {/* Settings tab */}
2507
+ {activeTab === 'settings' && (
2508
+ <Card>
2509
+ <CardContent className="pt-5 space-y-5">
2510
+ <Select
2511
+ label="Target Audience"
2512
+ value={targetAudience}
2513
+ onChange={setTarget}
2514
+ options={[
2515
+ { value: 'all', label: 'All Users' },
2516
+ { value: 'admin', label: 'Admins Only' },
2517
+ { value: 'member', label: 'Members Only' },
2518
+ ]}
2519
+ />
2520
+
2521
+ <Switch
2522
+ label="Payment Required"
2523
+ description="Users must pay to submit this form"
2524
+ checked={paymentRequired}
2525
+ onChange={setPayment}
2526
+ />
2527
+
2528
+ {paymentRequired && (
2529
+ <Input
2530
+ label="Fee Amount"
2531
+ type="number"
2532
+ min="0"
2533
+ placeholder="e.g. 500"
2534
+ value={price}
2535
+ onChange={e => { setPrice(e.target.value); if (formErrors.price) setFormErrors(p => ({ ...p, price: undefined })) }}
2536
+ error={formErrors.price}
2537
+ />
2538
+ )}
2539
+ </CardContent>
2540
+ </Card>
2541
+ )}
2542
+
2543
+ {/* Sticky action footer */}
2544
+ <div className="sticky bottom-0 z-10 bg-white border-t border-gray-200 py-3 flex justify-end gap-3">
2545
+ <Button variant="outline" onClick={handleSave} className="flex items-center gap-2">
2546
+ <Save className="w-4 h-4" /> Save as Draft
2547
+ </Button>
2548
+ <Button variant="primary" onClick={handlePublish} className="flex items-center gap-2">
2549
+ <Send className="w-4 h-4" /> Publish Now
2550
+ </Button>
2551
+ </div>
2552
+ </div>
2553
+ )
2554
+ }
2555
+ `;
2556
+ }
1834
2557
  function genOnboardingPage() {
1835
2558
  return `'use client'
1836
2559
 
@@ -2355,6 +3078,7 @@ function scaffold(opts) {
2355
3078
  w(f("src/app/dashboard/users/page.tsx"), genUsersPage(opts));
2356
3079
  w(f("src/app/dashboard/settings/page.tsx"), genSettingsPage(opts));
2357
3080
  w(f("src/app/dashboard/components/page.tsx"), genComponentsShowcasePage());
3081
+ w(f("src/app/dashboard/form-builder/page.tsx"), genFormBuilderPage());
2358
3082
  w(f("src/app/dashboard/onboarding/page.tsx"), genOnboardingPage());
2359
3083
  w(f("src/app/api/auth/login/route.ts"), genLoginRoute(opts));
2360
3084
  w(f("src/app/api/auth/user/route.ts"), genUserRoute(opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucifer91299/create-portal-app",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "description": "Scaffold a Next.js authenticated portal with full design system in one command",
5
5
  "license": "MIT",
6
6
  "author": "Aakash Kanojiya",