Skip to content

Commit

Permalink
Add dynamic portal (#18)
Browse files Browse the repository at this point in the history
* wip

* Working portal

* Persist custom field

* Load custom field

* delete custom field

* Update type
  • Loading branch information
johnmosesman authored Apr 4, 2024
1 parent 1641545 commit 13a4b6b
Show file tree
Hide file tree
Showing 20 changed files with 2,018 additions and 17 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ NEXTAUTH_SECRET="hey"
FLATFILE_API_KEY='sk_...'
FLATFILE_ENVIRONMENT_ID='us_env_...'
FLATFILE_NAMESPACE='space:plmproject'
NEXT_PUBLIC_FLATFILE_PUBLISHABLE_KEY='pk_...'
NEXT_PUBLIC_FLATFILE_ENVIRONMENT_ID='us_env_...'
NEXT_PUBLIC_APP_ID='products-show'
LISTENER_AUTH_TOKEN='token'
261 changes: 261 additions & 0 deletions app/(authenticated)/dynamic-portal/custom-field-builder.tsx
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>
);
};
98 changes: 98 additions & 0 deletions app/(authenticated)/dynamic-portal/option-builder.tsx
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>
);
};
Loading

0 comments on commit 13a4b6b

Please sign in to comment.