From 3395431ed7051e99a299c1cd25574d22adc7e940 Mon Sep 17 00:00:00 2001 From: QUBITABHAY Date: Fri, 27 Mar 2026 18:16:54 +0530 Subject: [PATCH 1/4] ci: Remove development branch from CI workflow triggers. --- .github/workflows/test.yml | 4 ++-- docs/changelog.md => CHANGELOG.md | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/changelog.md => CHANGELOG.md (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb4284c..635af24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Test on: push: - branches: [main, development] + branches: [main] pull_request: - branches: [main, development] + branches: [main] workflow_dispatch: diff --git a/docs/changelog.md b/CHANGELOG.md similarity index 100% rename from docs/changelog.md rename to CHANGELOG.md From 32558d4538b619b57b595043658099b60910d99e Mon Sep 17 00:00:00 2001 From: QUBITABHAY Date: Sat, 28 Mar 2026 00:26:12 +0530 Subject: [PATCH 2/4] feat: add Time Picker field and configurations --- src/components/BuilderCore/Flow/FlowCard.tsx | 12 ++ .../BuilderCore/Single/DraggableField.tsx | 11 ++ .../EditorSections/BasicFieldSettings.tsx | 75 +++++++++++ .../BuilderCore/shared/constants.tsx | 8 ++ src/components/BuilderCore/shared/types.ts | 5 +- src/components/common/TimePicker.tsx | 117 ++++++++++++++++++ src/components/common/icons.tsx | 16 +++ src/layouts/FlowPage.tsx | 15 +++ src/layouts/SinglePage.tsx | 23 ++++ 9 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 src/components/common/TimePicker.tsx diff --git a/src/components/BuilderCore/Flow/FlowCard.tsx b/src/components/BuilderCore/Flow/FlowCard.tsx index 9607163..b3b077f 100644 --- a/src/components/BuilderCore/Flow/FlowCard.tsx +++ b/src/components/BuilderCore/Flow/FlowCard.tsx @@ -8,6 +8,7 @@ import { memo } from "react"; import RadioButton from "../../common/RadioButton"; import Checkbox from "../../common/Checkbox"; import DatePicker from "../../common/DatePicker"; +import TimePicker from "../../common/TimePicker"; import FileUpload from "../../common/FileUpload"; interface FlowCardProps { @@ -112,6 +113,17 @@ const FlowCard = memo(function FlowCard({ /> ); + case "time": + return ( +
+ +
+ ); case "file": return (
diff --git a/src/components/BuilderCore/Single/DraggableField.tsx b/src/components/BuilderCore/Single/DraggableField.tsx index 93baba1..756d3eb 100644 --- a/src/components/BuilderCore/Single/DraggableField.tsx +++ b/src/components/BuilderCore/Single/DraggableField.tsx @@ -8,6 +8,7 @@ import Select from "../../common/Select"; import RadioButton from "../../common/RadioButton"; import Checkbox from "../../common/Checkbox"; import DatePicker from "../../common/DatePicker"; +import TimePicker from "../../common/TimePicker"; import FileUpload from "../../common/FileUpload"; interface DraggableFieldProps { @@ -107,6 +108,16 @@ const DraggableField = memo(function DraggableField({ disabled={true} /> ); + case "time": + return ( + + ); case "file": return ( )} + {field.type === "time" && ( +
+
+
+ + {field.minTime && ( + + )} +
+ onUpdate({ minTime: e.target.value })} + onFocus={(e) => { + e.target.type = "time"; + try { e.target.showPicker?.(); } catch { /* ignore */ } + }} + onBlur={(e) => { + if (!e.target.value) e.target.type = "text"; + }} + onClick={(e) => { + const target = e.target as HTMLInputElement; + if (target.type === "text") target.type = "time"; + try { target.showPicker?.(); } catch { /* ignore */ } + }} + className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 transition-all cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer" + /> +
+
+
+ + {field.maxTime && ( + + )} +
+ onUpdate({ maxTime: e.target.value })} + onFocus={(e) => { + e.target.type = "time"; + try { e.target.showPicker?.(); } catch { /* ignore */ } + }} + onBlur={(e) => { + if (!e.target.value) e.target.type = "text"; + }} + onClick={(e) => { + const target = e.target as HTMLInputElement; + if (target.type === "text") target.type = "time"; + try { target.showPicker?.(); } catch { /* ignore */ } + }} + className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 transition-all cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer" + /> +
+
+ )} +
, + defaultConfig: { + title: "Time", + }, + }, { type: "file", label: "File Upload", diff --git a/src/components/BuilderCore/shared/types.ts b/src/components/BuilderCore/shared/types.ts index cc2f4ab..ddd3eb6 100644 --- a/src/components/BuilderCore/shared/types.ts +++ b/src/components/BuilderCore/shared/types.ts @@ -13,7 +13,8 @@ export type FieldType = | "select" | "date" | "file" - | "page_break"; + | "page_break" + | "time"; export interface FieldOption { label: string; @@ -45,6 +46,8 @@ export interface FormFieldConfig { multiSelect?: boolean; minDate?: string; maxDate?: string; + minTime?: string; + maxTime?: string; logic?: FieldLogic; } diff --git a/src/components/common/TimePicker.tsx b/src/components/common/TimePicker.tsx new file mode 100644 index 0000000..6df3471 --- /dev/null +++ b/src/components/common/TimePicker.tsx @@ -0,0 +1,117 @@ +import { useRef } from "react"; + +interface TimePickerProp { + label: string; + name?: string; + value?: string; + min?: string; + max?: string; + onChange?: (e: React.ChangeEvent) => void; + disabled?: boolean; + required?: boolean; + subtitle?: string; +} + +function TimePicker({ + label, + name, + value, + onChange, + min, + max, + disabled = false, + required = false, + subtitle, +}: TimePickerProp) { + const inputRef = useRef(null); + + const handleContainerClick = () => { + if (!disabled && inputRef.current) { + if (inputRef.current.type === "text") { + inputRef.current.type = "time"; + } + try { + inputRef.current.showPicker?.(); + } catch { + // ignore error in browsers that don't support showPicker + } + inputRef.current.focus(); + } + }; + + const handleBlur = (e: React.FocusEvent) => { + if (!value) { + e.target.type = "text"; + } + }; + + return ( +
+ {label && ( + + )} + {subtitle &&

{subtitle}

} +
+ +
+ + + +
+
+
+ ); +} + +export default TimePicker; diff --git a/src/components/common/icons.tsx b/src/components/common/icons.tsx index 7d584fa..75c6e8c 100644 --- a/src/components/common/icons.tsx +++ b/src/components/common/icons.tsx @@ -315,4 +315,20 @@ export const Icons = { /> ), + + Clock: () => ( + + + + ), }; diff --git a/src/layouts/FlowPage.tsx b/src/layouts/FlowPage.tsx index b7389bb..dc84403 100644 --- a/src/layouts/FlowPage.tsx +++ b/src/layouts/FlowPage.tsx @@ -12,6 +12,7 @@ import Checkbox from "../components/common/Checkbox"; import RadioButton from "../components/common/RadioButton"; import Button from "../components/common/Button"; import DatePicker from "../components/common/DatePicker"; +import TimePicker from "../components/common/TimePicker"; import FileUpload from "../components/common/FileUpload"; import Select from "../components/common/Select"; import TextArea from "../components/common/TextArea"; @@ -440,6 +441,20 @@ function FlowPage({ />
); + case "time": + return ( +
+ handleFieldChange(field.id, e.target.value)} + min={field.minTime} + max={field.maxTime} + subtitle={field.subtitle} + /> +
+ ); case "file": return (
diff --git a/src/layouts/SinglePage.tsx b/src/layouts/SinglePage.tsx index f3af9af..cd770b2 100644 --- a/src/layouts/SinglePage.tsx +++ b/src/layouts/SinglePage.tsx @@ -4,6 +4,7 @@ import Checkbox from "../components/common/Checkbox"; import RadioButton from "../components/common/RadioButton"; import Button from "../components/common/Button"; import DatePicker from "../components/common/DatePicker"; +import TimePicker from "../components/common/TimePicker"; import FileUpload from "../components/common/FileUpload"; import Select from "../components/common/Select"; import TextArea from "../components/common/TextArea"; @@ -374,6 +375,28 @@ function SinglePage({ />
); + case "time": + return ( +
+ + handleFieldChange(field.id, e.target.value)} + min={field.minTime} + max={field.maxTime} + /> +
+ ); case "file": return (
From 881de403f73a5452c581ea9938a4ba14a0f12510 Mon Sep 17 00:00:00 2001 From: QUBITABHAY Date: Sat, 28 Mar 2026 00:26:15 +0530 Subject: [PATCH 3/4] feat: custom DatePicker UI, FieldEditor date configs, SEO tags, and bump version --- CHANGELOG.md | 12 + index.html | 40 ++- package.json | 4 +- .../BuilderCore/shared/FieldEditor.tsx | 68 ++++- src/components/common/DatePicker.tsx | 274 +++++++++++++++--- 5 files changed, 353 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c51d33..ed96684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [Release 0.0.2] + +### Added + +- **Time Picker Field**: Introduced a fully functional "Time" field type with support across single page forms, flow builder, and previews. +- **Time & Date Constraints**: Added editable `minTime`, `maxTime`, `minDate`, and `maxDate` constraints directly within the Builder settings sidebar with easy "Clear" buttons. +- **SEO Optimizations**: Integrated comprehensive SEO tags, OpenGraph (`og:`) properties, and Twitter Card schemas directly into `index.html`. + +### Fixed + +- **Custom DatePicker UI**: Re-architected `DatePicker.tsx` to utilize a tailor-made, interactive dropdown calendar instead of native browser popups. + ## [Release 0.0.1] ### Added diff --git a/index.html b/index.html index 827965b..ca79837 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,46 @@ + + + + + + + + + + + + + + + + + + + + - Formify + Formify | Beautiful Drag & Drop Form Builder diff --git a/package.json b/package.json index accaf60..ef98c28 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "name": "formify", + "description": "Form Builder", + "author": "qubitabhay", "private": true, - "version": "0.0.1", + "version": "0.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src/components/BuilderCore/shared/FieldEditor.tsx b/src/components/BuilderCore/shared/FieldEditor.tsx index 9d82093..10b791d 100644 --- a/src/components/BuilderCore/shared/FieldEditor.tsx +++ b/src/components/BuilderCore/shared/FieldEditor.tsx @@ -139,29 +139,77 @@ const FieldEditor = memo(function FieldEditor({ Date Constraints
- +
+ + {field.minDate && ( + + )} +
onUpdate({ minDate: e.target.value || undefined }) } - className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 focus:border-gray-300 outline-none transition-all text-sm" + onFocus={(e) => { + e.target.type = "date"; + try { e.target.showPicker?.(); } catch { /* ignore */ } + }} + onBlur={(e) => { + if (!e.target.value) e.target.type = "text"; + }} + onClick={(e) => { + const target = e.target as HTMLInputElement; + if (target.type === "text") target.type = "date"; + try { target.showPicker?.(); } catch { /* ignore */ } + }} + className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 focus:border-gray-300 outline-none transition-all text-sm cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer" />
- +
+ + {field.maxDate && ( + + )} +
onUpdate({ maxDate: e.target.value || undefined }) } - className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 focus:border-gray-300 outline-none transition-all text-sm" + onFocus={(e) => { + e.target.type = "date"; + try { e.target.showPicker?.(); } catch { /* ignore */ } + }} + onBlur={(e) => { + if (!e.target.value) e.target.type = "text"; + }} + onClick={(e) => { + const target = e.target as HTMLInputElement; + if (target.type === "text") target.type = "date"; + try { target.showPicker?.(); } catch { /* ignore */ } + }} + className="w-full p-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-300 focus:border-gray-300 outline-none transition-all text-sm cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer" />
diff --git a/src/components/common/DatePicker.tsx b/src/components/common/DatePicker.tsx index 44aef84..783dcd9 100644 --- a/src/components/common/DatePicker.tsx +++ b/src/components/common/DatePicker.tsx @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useState, useRef, useEffect } from "react"; interface DatePickerProp { label: string; @@ -23,17 +23,116 @@ function DatePicker({ required = false, subtitle, }: DatePickerProp) { - const inputRef = useRef(null); - - const handleContainerClick = () => { - if (!disabled && inputRef.current) { - inputRef.current.showPicker?.(); - inputRef.current.focus(); + const [isOpen, setIsOpen] = useState(false); + const [currentMonth, setCurrentMonth] = useState(() => { + if (value) { + const [y, m, d] = value.split("-"); + if (y && m && d) return new Date(Number(y), Number(m) - 1, Number(d)); } + return new Date(); + }); + const containerRef = useRef(null); + + const selectedDate = value + ? (() => { + const [y, m, d] = value.split("-"); + return new Date(Number(y), Number(m) - 1, Number(d)); + })() + : null; + const minDate = min ? new Date(`${min}T00:00:00`) : null; + const maxDate = max ? new Date(`${max}T23:59:59`) : null; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const getDaysInMonth = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + const startingDayOfWeek = firstDay.getDay(); + return { daysInMonth, startingDayOfWeek }; + }; + + const isDateDisabled = (date: Date) => { + if (minDate && date < new Date(minDate.setHours(0, 0, 0, 0))) return true; + if (maxDate && date > new Date(maxDate.setHours(23, 59, 59, 999))) + return true; + return false; + }; + + const handleDateSelect = (day: number) => { + const newDate = new Date( + currentMonth.getFullYear(), + currentMonth.getMonth(), + day, + ); + if (isDateDisabled(newDate)) return; + + const year = newDate.getFullYear(); + const month = String(newDate.getMonth() + 1).padStart(2, "0"); + const dayStr = String(newDate.getDate()).padStart(2, "0"); + const formattedDate = `${year}-${month}-${dayStr}`; + + const syntheticEvent = { + target: { value: formattedDate, name }, + } as React.ChangeEvent; + onChange?.(syntheticEvent); + setIsOpen(false); + }; + + const goToPrevMonth = () => { + setCurrentMonth( + new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1), + ); + }; + + const goToNextMonth = () => { + setCurrentMonth( + new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1), + ); + }; + + const formatDisplayDate = (dateStr: string) => { + const [year, month, day] = dateStr.split("-").map(Number); + const date = new Date(year, month - 1, day); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); }; + const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth); + const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + const dayNames = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; + return ( -
+
{label && (