diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/forms')
| -rw-r--r-- | packages/exchange-backoffice-ui/src/forms/InputArray.tsx | 191 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/forms/InputFile.tsx | 99 | ||||
| -rw-r--r-- | packages/exchange-backoffice-ui/src/forms/forms.ts | 4 |
3 files changed, 206 insertions, 88 deletions
diff --git a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx index 2447c9989..b2905a2a7 100644 --- a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx +++ b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx @@ -4,6 +4,72 @@ import { FormProvider } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; import { useField } from "./useField.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +function Option({ + label, + disabled, + isFirst, + isLast, + isSelected, + onClick, +}: { + label: TranslatedString; + isFirst?: boolean; + isLast?: boolean; + isSelected?: boolean; + disabled?: boolean; + onClick: () => void; +}): VNode { + let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey"; + if (isFirst) { + clazz += " rounded-tl-md rounded-tr-md "; + } + if (isLast) { + clazz += " rounded-bl-md rounded-br-md "; + } + if (isSelected) { + clazz += " z-10 border-indigo-200 bg-indigo-50 "; + } else { + clazz += " border-gray-200"; + } + if (disabled) { + clazz += + " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; + } else { + clazz += " cursor-pointer"; + } + return ( + <label class={clazz}> + <input + type="radio" + name="privacy-setting" + checked={isSelected} + disabled={disabled} + onClick={onClick} + class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" + aria-labelledby="privacy-setting-0-label" + aria-describedby="privacy-setting-0-description" + /> + <span class="ml-3 flex flex-col"> + <span + id="privacy-setting-0-label" + disabled + class="block text-sm font-medium" + > + {label} + </span> + {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} + {/* <span + id="privacy-setting-0-description" + class="block text-sm" + > + This project would be available to anyone who has the link + </span> */} + </span> + </label> + ); +} export function InputArray( props: { @@ -17,10 +83,7 @@ export function InputArray( const [selectedIndex, setSelected] = useState<number | undefined>(undefined); const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; - const [subForm, updateSubForm] = useState(selected ?? {}); - useEffect(() => { - updateSubForm(selected); - }, [selected]); + return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -29,95 +92,47 @@ export function InputArray( tooltip={tooltip} /> - <div class="flex mb-4 items-center pt-3"> - <div class="flex-auto"> - {selectedIndex !== undefined && ( - <button - type="button" - onClick={() => { - setSelected(undefined); - }} - class="block rounded-md bg-white px-3 py-2 text-center text-sm font-semibold shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - Cancel - </button> - )} - </div> - <div class="flex-none"> - {selectedIndex === undefined && ( - <button - type="button" - onClick={() => { - setSelected(list.length); - }} - class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-indigo-500 " - > - Add - </button> - )} - </div> - </div> <div class="-space-y-px rounded-md bg-white "> {list.map((v, idx) => { - const isFirst = idx === 0; - const isLast = idx === list.length - 1; - const isSelected = selectedIndex === idx; - const disabled = selectedIndex !== undefined && !isSelected; - let clazz = - "relative flex border p-4 focus:outline-none disabled:text-grey"; - if (isFirst) { - clazz += " rounded-tl-md rounded-tr-md "; - } - if (isLast) { - clazz += " rounded-bl-md rounded-br-md "; - } - if (isSelected) { - clazz += " z-10 border-indigo-200 bg-indigo-50 "; - } else { - clazz += " border-gray-200"; - } - if (disabled) { - clazz += - " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; - } else { - clazz += " cursor-pointer"; - } return ( - <label class={clazz}> - <Fragment> - <input - type="radio" - name="privacy-setting" - checked={isSelected} - disabled={disabled} - onClick={() => setSelected(idx)} - class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" - aria-labelledby="privacy-setting-0-label" - aria-describedby="privacy-setting-0-description" - /> - <span class="ml-3 flex flex-col"> - <span - id="privacy-setting-0-label" - disabled - class="block text-sm font-medium" - > - {v[labelField]} - </span> - {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} - {/* <span - id="privacy-setting-0-description" - class="block text-sm" - > - This project would be available to anyone who has the link - </span> */} - </span> - </Fragment> - </label> + <Option + label={v[labelField]} + isSelected={selectedIndex === idx} + isLast={idx === list.length - 1} + disabled={selectedIndex !== undefined && selectedIndex !== idx} + isFirst={idx === 0} + onClick={() => { + setSelected(selectedIndex === idx ? undefined : idx); + }} + /> ); })} + <div class="pt-2"> + <Option + label={"Add..." as TranslatedString} + isSelected={selectedIndex === list.length} + isLast + isFirst + disabled={ + selectedIndex !== undefined && selectedIndex !== list.length + } + onClick={() => { + setSelected( + selectedIndex === list.length ? undefined : list.length, + ); + }} + /> + </div> </div> {selectedIndex !== undefined && ( - <FormProvider initialValue={subForm} onUpdate={updateSubForm}> + <FormProvider + initialValue={selected} + onUpdate={(v) => { + const newValue = [...list]; + newValue.splice(selectedIndex, 1, v); + onChange(newValue); + }} + > <div class="px-4 py-6"> <div class="grid grid-cols-1 gap-y-8 "> <RenderAllFieldsByUiConfig fields={fields} /> @@ -143,7 +158,7 @@ export function InputArray( </button> )} </div> - <div class="flex-none"> + {/* <div class="flex-none"> <button type="button" onClick={() => { @@ -156,7 +171,7 @@ export function InputArray( > Confirm </button> - </div> + </div> */} </div> )} </div> diff --git a/packages/exchange-backoffice-ui/src/forms/InputFile.tsx b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx new file mode 100644 index 000000000..749c8b264 --- /dev/null +++ b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx @@ -0,0 +1,99 @@ +import { VNode, h } from "preact"; +import { + InputLine, + LabelWithTooltipMaybeRequired, + UIFormProps, +} from "./InputLine.js"; +import { useField } from "./useField.js"; + +export function InputFile( + props: { maxBites: number; accept?: string } & UIFormProps<string>, +): VNode { + const { + name, + label, + placeholder, + tooltip, + required, + help, + maxBites, + accept, + } = props; + const { value, onChange } = useField<{ [s: string]: string }>(name); + + return ( + <div class="col-span-full"> + <LabelWithTooltipMaybeRequired + label={label} + tooltip={tooltip} + required={required} + /> + {!value || !value.startsWith("data:image/") ? ( + <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1"> + <div class="text-center"> + <svg + class="mx-auto h-12 w-12 text-gray-300" + viewBox="0 0 24 24" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" + clip-rule="evenodd" + /> + </svg> + <div class="my-2 flex text-sm leading-6 text-gray-600"> + <label + for="file-upload" + class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500" + > + <span>Upload a file</span> + <input + id="file-upload" + name="file-upload" + type="file" + class="sr-only" + accept={accept} + onChange={(e) => { + const f: FileList | null = e.currentTarget.files; + if (!f || f.length != 1) { + return onChange(undefined!); + } + if (f[0].size > maxBites) { + return onChange(undefined!); + } + return f[0].arrayBuffer().then((b) => { + const b64 = window.btoa( + new Uint8Array(b).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + return onChange(`data:${f[0].type};base64,${b64}` as any); + }); + }} + /> + </label> + {/* <p class="pl-1">or drag and drop</p> */} + </div> + </div> + </div> + ) : ( + <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative"> + <img src={value} class=" h-24 w-full object-cover relative" /> + + <div + class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer " + onClick={() => { + onChange(undefined!); + }} + > + Clear + </div> + </div> + )} + {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>} + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/forms/forms.ts b/packages/exchange-backoffice-ui/src/forms/forms.ts index 1e59e0cf2..c2cb0799e 100644 --- a/packages/exchange-backoffice-ui/src/forms/forms.ts +++ b/packages/exchange-backoffice-ui/src/forms/forms.ts @@ -7,6 +7,7 @@ import { InputChoiceStacked } from "./InputChoice.js"; import { InputArray } from "./InputArray.js"; import { InputSelectMultiple } from "./InputSelectMultiple.js"; import { InputTextArea } from "./InputTextArea.js"; +import { InputFile } from "./InputFile.js"; export type DoubleColumnForm = DoubleColumnFormSection[]; @@ -22,6 +23,7 @@ type DoubleColumnFormSection = { type FieldType = { separator: {}; array: Parameters<typeof InputArray>[0]; + file: Parameters<typeof InputFile>[0]; selectMultiple: Parameters<typeof InputSelectMultiple>[0]; text: Parameters<typeof InputText>[0]; textArea: Parameters<typeof InputTextArea>[0]; @@ -36,6 +38,7 @@ type FieldType = { export type UIFormField = | { type: "separator"; props: FieldType["separator"] } | { type: "array"; props: FieldType["array"] } + | { type: "file"; props: FieldType["file"] } | { type: "selectMultiple"; props: FieldType["selectMultiple"] } | { type: "text"; props: FieldType["text"] } | { type: "textArea"; props: FieldType["textArea"] } @@ -62,6 +65,7 @@ const UIFormConfiguration: UIFormFieldMap = { separator: Separator, array: InputArray, text: InputText, + file: InputFile, textArea: InputTextArea, date: InputDate, choiceStacked: InputChoiceStacked, |
