-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* wip * Working portal * Persist custom field * Load custom field * delete custom field * Update type
- Loading branch information
1 parent
1641545
commit 13a4b6b
Showing
20 changed files
with
2,018 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
261 changes: 261 additions & 0 deletions
261
app/(authenticated)/dynamic-portal/custom-field-builder.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,261 @@ | ||
import { OptionBuilder } from "./option-builder"; | ||
import { FormEvent } from "react"; | ||
import { | ||
CustomField, | ||
DATE_FORMATS, | ||
FIELD_TYPES, | ||
INITIAL_OPTIONS, | ||
} from "@/lib/dynamic/field-options"; | ||
import { useToast } from "@/components/ui/use-toast"; | ||
|
||
type Props = { | ||
customField: CustomField; | ||
setCustomField: (customField: CustomField) => void; | ||
}; | ||
|
||
export const CustomFieldBuilder = ({ customField, setCustomField }: Props) => { | ||
const { toast } = useToast(); | ||
|
||
if (!customField.enumOptions) { | ||
customField.enumOptions = INITIAL_OPTIONS; | ||
} | ||
|
||
const options = customField.enumOptions; | ||
|
||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { | ||
e.preventDefault(); | ||
|
||
const formData = new FormData(e.target as HTMLFormElement); | ||
const { name, type, required, dateFormat, decimals, enumOptions } = | ||
JSON.parse(formData.get("customField") as string); | ||
|
||
try { | ||
const response = await fetch("/api/custom-field", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
name, | ||
type, | ||
required, | ||
dateFormat, | ||
decimals, | ||
enumOptions, | ||
}), | ||
}); | ||
|
||
const json = await response.json(); | ||
const data = json.customField; | ||
|
||
const customField = { | ||
name: data.name, | ||
type: data.type, | ||
required: data.required, | ||
dateFormat: data.dateFormat, | ||
decimals: data.decimals, | ||
enumOptions: data.enumOptions, | ||
}; | ||
|
||
console.log("custom field saved", customField); | ||
} catch (error) { | ||
console.error("Error saving custom field:", error); | ||
} | ||
}; | ||
|
||
return ( | ||
<div | ||
className="card-bg card-sm space-y-4" | ||
style={{ | ||
boxShadow: | ||
"8.74046516418457px 9.711627960205078px 18.45209312438965px 0px rgba(61, 73, 100, 0.3) inset", | ||
backgroundColor: "white", | ||
}} | ||
> | ||
<div className="grid grid-cols-3 space-x-2 text-sm items-center"> | ||
<input | ||
name="custom-field-name" | ||
type="text" | ||
className="text-dark border border-dark text-sm rounded px-2 py-2" | ||
placeholder="Birthdate" | ||
value={customField.name} | ||
onChange={(e) => { | ||
setCustomField({ ...customField, name: e.target.value }); | ||
}} | ||
/> | ||
|
||
<select | ||
name="custom-field-type" | ||
className="border border-dark text-dark text-sm rounded px-2 py-2" | ||
value={customField.type} | ||
onChange={(e) => { | ||
setCustomField({ | ||
...customField, | ||
type: e.target.value as keyof typeof FIELD_TYPES, | ||
}); | ||
}} | ||
> | ||
{Object.keys(FIELD_TYPES).map((key) => { | ||
return ( | ||
<option key={key} value={key}> | ||
{FIELD_TYPES[key as keyof typeof FIELD_TYPES]} | ||
</option> | ||
); | ||
})} | ||
</select> | ||
|
||
<div className="flex flex-row items-center justify-between"> | ||
<input | ||
id="custom-field-required-validation" | ||
name="custom-field-required-validation" | ||
className="inline-flex justify-self-start border border-dark" | ||
type="checkbox" | ||
checked={customField.required} | ||
onChange={(e) => { | ||
setCustomField({ ...customField, required: e.target.checked }); | ||
}} | ||
/> | ||
|
||
<form onSubmit={handleSubmit}> | ||
<input | ||
type="hidden" | ||
id="customField" | ||
name="customField" | ||
value={JSON.stringify(customField)} | ||
/> | ||
|
||
<div className="flex flex-row items-center space-x-2"> | ||
<button | ||
onClick={() => { | ||
toast({ | ||
title: "Saved custom field", | ||
}); | ||
}} | ||
className="bg-dynamic-portal px-5 md:px-12 py-1 md:py-2 rounded-xl" | ||
style={{ | ||
boxShadow: | ||
"8.74046516418457px 9.711627960205078px 18.45209312438965px 0px rgba(61, 73, 100, 0.3) inset", | ||
}} | ||
> | ||
Save | ||
</button> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
|
||
<div className="text-dark w-full"> | ||
{customField.type === "date" && ( | ||
<div className="flex flex-row items-center justify-start space-x-2"> | ||
<label className="block text-xs font-semibold">Format</label> | ||
<select | ||
name="custom-field-type" | ||
className="border border-dark text-sm rounded px-2 py-2" | ||
value={customField.dateFormat} | ||
onChange={(e) => { | ||
setCustomField({ | ||
...customField, | ||
dateFormat: e.target.value as keyof typeof DATE_FORMATS, | ||
}); | ||
}} | ||
> | ||
{Object.keys(DATE_FORMATS).map((key) => { | ||
return ( | ||
<option key={key} value={key}> | ||
{DATE_FORMATS[key as keyof typeof DATE_FORMATS]} | ||
</option> | ||
); | ||
})} | ||
</select> | ||
</div> | ||
)} | ||
|
||
{customField.type === "number" && ( | ||
<div className="flex flex-row items-center justify-start space-x-2"> | ||
<label className="block text-xs font-semibold"> | ||
Decimal Places | ||
</label> | ||
<input | ||
name="number-decimal-places" | ||
type="number" | ||
min={0} | ||
step={1} | ||
className="border border-dark text-sm rounded px-2 py-2 w-[50px]" | ||
placeholder="2" | ||
defaultValue={customField.decimals} | ||
onChange={(e) => { | ||
setCustomField({ | ||
...customField, | ||
decimals: parseInt(e.target.value), | ||
}); | ||
}} | ||
/> | ||
</div> | ||
)} | ||
|
||
{customField.type === "enum" && ( | ||
<div className="space-y-2"> | ||
<label | ||
htmlFor="custom-field-required-validation" | ||
className="block text-sm font-semibold cursor-pointer" | ||
> | ||
Options | ||
</label> | ||
<OptionBuilder | ||
options={options.sort((a, b) => a.id - b.id)} | ||
updateInput={(option, value) => { | ||
const filteredOptions = options.filter((o) => { | ||
return o.id !== option.id; | ||
}); | ||
|
||
setCustomField({ | ||
...customField, | ||
enumOptions: [ | ||
...filteredOptions, | ||
{ ...option, input: value }, | ||
], | ||
}); | ||
}} | ||
updateOutput={(option, value) => { | ||
const filteredOptions = options.filter((o) => { | ||
return o.id !== option.id; | ||
}); | ||
|
||
setCustomField({ | ||
...customField, | ||
enumOptions: [ | ||
...filteredOptions, | ||
{ ...option, output: value }, | ||
], | ||
}); | ||
}} | ||
addNewOption={() => { | ||
const maxId = options.reduce((max, option) => { | ||
return Math.max(max, option.id); | ||
}, 0); | ||
|
||
setCustomField({ | ||
...customField, | ||
enumOptions: [ | ||
...options, | ||
{ id: maxId + 1, input: "", output: "" }, | ||
], | ||
}); | ||
}} | ||
removeOption={(option) => { | ||
const filteredObjects = options.filter((o) => { | ||
return o.id !== option.id; | ||
}); | ||
|
||
setCustomField({ | ||
...customField, | ||
enumOptions: filteredObjects, | ||
}); | ||
}} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Option } from "@/lib/dynamic/field-options"; | ||
|
||
type Props = { | ||
options: Option[]; | ||
updateInput: (option: Option, value: string) => void; | ||
updateOutput: (option: Option, value: string) => void; | ||
addNewOption: () => void; | ||
removeOption: (option: Option) => void; | ||
}; | ||
|
||
export const OptionBuilder = ({ | ||
options, | ||
updateInput, | ||
updateOutput, | ||
addNewOption, | ||
removeOption, | ||
}: Props) => { | ||
return ( | ||
<div className="space-y-2"> | ||
<div className="flex flex-row justify-between items-center"> | ||
<p className="text-xs w-[45%] md:w-[47.5%]">Sheet Value</p> | ||
<p className="text-xs w-[45%] md:w-[47.5%]">Record Output</p> | ||
<p className="text-xs w-[10%] md:w-[5%]"></p> | ||
</div> | ||
|
||
<div className="space-y-2"> | ||
{options.map((option) => { | ||
return ( | ||
<div | ||
key={option.id} | ||
className="flex flex-row justify-between text-sm items-center space-x-2" | ||
> | ||
<input | ||
type="text" | ||
defaultValue={option.input} | ||
onChange={(e) => { | ||
updateInput(option, e.target.value); | ||
}} | ||
className="text-dark text-xs border border-dark rounded px-2 py-1 w-[45%] md:w-[47.5%]" | ||
/> | ||
|
||
<input | ||
type="text" | ||
defaultValue={option.output} | ||
onChange={(e) => { | ||
updateOutput(option, e.target.value); | ||
}} | ||
className="text-dark text-xs border border-dark rounded px-2 py-1 w-[45%] md:w-[47.5%]" | ||
/> | ||
|
||
<div className="w-[10%] md:w-[5%]"> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
strokeWidth={1.5} | ||
stroke="currentColor" | ||
className="w-6 h-6 text-gray-300 cursor-pointer" | ||
onClick={() => { | ||
removeOption(option); | ||
}} | ||
> | ||
<path | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
d="M6 18L18 6M6 6l12 12" | ||
/> | ||
</svg> | ||
</div> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
|
||
<div | ||
onClick={addNewOption} | ||
className="flex flex-row items-center justify-start text-gray-400 text-xs cursor-pointer" | ||
> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
strokeWidth={1.5} | ||
stroke="currentColor" | ||
className="w-4 h-4" | ||
> | ||
<path | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
d="M12 6v12m6-6H6" | ||
/> | ||
</svg> | ||
|
||
<p>New Option</p> | ||
</div> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.