From b4522f158048093e4530111ae6078010a1e41c63 Mon Sep 17 00:00:00 2001 From: Vitalii Kiiko Date: Tue, 20 Aug 2024 08:17:42 +0200 Subject: [PATCH] add bando page form; --- src/assets/scss/components/appForm.scss | 84 ++++++ src/assets/scss/components/appPage.scss | 8 + src/assets/scss/theme.scss | 3 + .../FormField/components/Datepicker/index.js | 42 +++ .../components/DatepickerRange/index.js | 42 +++ .../FormField/components/Fileupload/index.js | 50 ++++ .../FormField/components/NumberInput/index.js | 54 ++++ .../FormField/components/TextArea/index.js | 34 +++ .../FormField/components/TextInput/index.js | 44 +++ src/components/FormField/index.js | 32 +++ src/components/FormFieldRepeater/index.js | 108 +++++++ .../FormFieldRepeaterCriteria/index.js | 143 ++++++++++ src/components/FormFieldRepeaterFaq/index.js | 122 ++++++++ src/pages/Bando/index.js | 267 ++++++++++++------ 14 files changed, 952 insertions(+), 81 deletions(-) create mode 100644 src/components/FormField/components/Datepicker/index.js create mode 100644 src/components/FormField/components/DatepickerRange/index.js create mode 100644 src/components/FormField/components/Fileupload/index.js create mode 100644 src/components/FormField/components/NumberInput/index.js create mode 100644 src/components/FormField/components/TextArea/index.js create mode 100644 src/components/FormField/components/TextInput/index.js create mode 100644 src/components/FormField/index.js create mode 100644 src/components/FormFieldRepeater/index.js create mode 100644 src/components/FormFieldRepeaterCriteria/index.js create mode 100644 src/components/FormFieldRepeaterFaq/index.js diff --git a/src/assets/scss/components/appForm.scss b/src/assets/scss/components/appForm.scss index 63ffb5f..4a55012 100644 --- a/src/assets/scss/components/appForm.scss +++ b/src/assets/scss/components/appForm.scss @@ -7,4 +7,88 @@ display: flex; flex-direction: column; gap: 14px; + + label { + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: normal; + + span { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-left: 10px; + } + } + + small { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 21px; + } + + &.datepicker, &.formfieldrepeater, &.datepickerrange, &.fileupload { + .p-button:not(.p-button-danger) { + background: var(--button-secondary-background); + border: 1px solid var(--button-secondary-borderColor); + } + .p-button:not(:disabled, .p-button-danger):hover { + background: var(--button-secondary-borderColor); + } + } +} +.appForm__twoCols { + display: flex; + gap: 1rem; + justify-content: space-between; + flex-wrap: wrap; + + > div { + flex: 1 1 auto; + min-width: 300px; + max-width: 700px; + display: flex; + flex-direction: column; + gap: 14px; + + label { + font-weight: 400; + } + } +} +.appForm__repeaterItem { + padding: 0.5rem 0.5rem 0.5rem 1rem; + border-left: 3px solid #dadada; + + + &:hover { + border-color: var(--button-secondary-background); + background: #FCF7E7; + } +} + +.appForm__faqHeaderControls { + display: flex; + gap: 1rem; + + button { + flex: 0 0 auto; + } + + > div { + flex: 1 1 auto; + } +} + +.appForm__faqTab { + display: flex; + justify-content: space-between; + gap: 1rem; +} +.appForm__faqTabItem { + display: flex; + gap: 0.5rem; } \ No newline at end of file diff --git a/src/assets/scss/components/appPage.scss b/src/assets/scss/components/appPage.scss index 59a9994..e0844b0 100644 --- a/src/assets/scss/components/appPage.scss +++ b/src/assets/scss/components/appPage.scss @@ -35,6 +35,7 @@ flex-direction: column; gap: 14px; padding: 28px; + border-left: 4px solid var(--card-borderColor-color); h1, h2, h3 { margin: 0; @@ -64,4 +65,11 @@ display: flex; gap: 24px; margin-bottom: 24px; +} + +.mb-2 { + margin-bottom: 4px; +} +.mb-8 { + margin-bottom: 16px; } \ No newline at end of file diff --git a/src/assets/scss/theme.scss b/src/assets/scss/theme.scss index f8f29a6..03e524b 100644 --- a/src/assets/scss/theme.scss +++ b/src/assets/scss/theme.scss @@ -9,6 +9,9 @@ --menuitem-active-color: #FFF; --menuitem-active-background: #3B7C43; --Black: #000; + --card-borderColor-color: #EEC137; + --button-secondary-borderColor: #C79807; + --button-secondary-background: var(--menu-borderColor); --card-full-background-color-2: #EEC137; --card-full-background-color-3: #FA8E42; diff --git a/src/components/FormField/components/Datepicker/index.js b/src/components/FormField/components/Datepicker/index.js new file mode 100644 index 0000000..70c9981 --- /dev/null +++ b/src/components/FormField/components/Datepicker/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { isNil } from 'ramda'; +import { Calendar } from 'primereact/calendar'; + +const Datepicker = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null, + minDate = null, + maxDate = null + }) => { + return ( + <> + + ( + field.onChange(e.value)} + dateFormat="dd/mm/yy" + mask="99/99/9999" + showIcon + minDate={minDate} maxDate={maxDate} + className={classNames({ 'p-invalid': fieldState.invalid })}/> + )}/> + {infoText ? {infoText} : null} + ) +} + +export default Datepicker; \ No newline at end of file diff --git a/src/components/FormField/components/DatepickerRange/index.js b/src/components/FormField/components/DatepickerRange/index.js new file mode 100644 index 0000000..2868e2b --- /dev/null +++ b/src/components/FormField/components/DatepickerRange/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { isNil } from 'ramda'; +import { Calendar } from 'primereact/calendar'; + +const DatepickerRange = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null, + minDate = null, + maxDate = null + }) => { + return ( + <> + + ( + field.onChange(e.value)} + dateFormat="dd/mm/yy" mask="99/99/9999" + showIcon + minDate={minDate} maxDate={maxDate} + selectionMode="range" readOnlyInput hideOnRangeSelection + className={classNames({ 'p-invalid': fieldState.invalid })}/> + )}/> + {infoText ? {infoText} : null} + ) +} + +export default DatepickerRange; \ No newline at end of file diff --git a/src/components/FormField/components/Fileupload/index.js b/src/components/FormField/components/Fileupload/index.js new file mode 100644 index 0000000..e1d8564 --- /dev/null +++ b/src/components/FormField/components/Fileupload/index.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { __ } from '@wordpress/i18n'; +import { FileUpload } from 'primereact/fileupload'; + +const Fileupload = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null, + accept = 'image/*', + api = '/api/upload', + emptyText = __('Trascina qui il tuo file', 'gepafin'), + chooseLabel = __('Aggiungi immagine', 'gepafin') + }) => { + return ( + <> + + ( + {emptyText}

} + chooseLabel={chooseLabel} + cancelLabel={__('Cancella', 'gepafin')} + uploadLabel={__('Carica', 'gepafin')} + className={classNames({ 'p-invalid': fieldState.invalid })}/> + )}/> + {infoText ? {infoText} : null} + {defaultValue ?

Uploaded:

: null} + {defaultValue} + ) +} + +export default Fileupload; \ No newline at end of file diff --git a/src/components/FormField/components/NumberInput/index.js b/src/components/FormField/components/NumberInput/index.js new file mode 100644 index 0000000..709ab1c --- /dev/null +++ b/src/components/FormField/components/NumberInput/index.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { InputNumber } from 'primereact/inputnumber'; + + +const NumberInput = ({ + fieldName, + label, + control, + errors, + defaultValue = 0, + config = {}, + infoText = null, + inputgroup = false, + icon = null, + locale = 'it-IT', + minFractionDigits = 2, + step = 1, + min, + max + }) => { + const input = ( + field.onChange(e.value)} + min={min} + max={max} + locale={locale} minFractionDigits={minFractionDigits} step={step} + className={classNames({ 'p-invalid': fieldState.invalid })}/> + )}/> + return ( + <> + + {inputgroup + ?
+ + {icon} + + {input} +
+ : input} + {infoText ? {infoText} : null} + ) +} + +export default NumberInput; \ No newline at end of file diff --git a/src/components/FormField/components/TextArea/index.js b/src/components/FormField/components/TextArea/index.js new file mode 100644 index 0000000..1bf00c8 --- /dev/null +++ b/src/components/FormField/components/TextArea/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { InputTextarea } from 'primereact/inputtextarea'; + +const TextArea = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null + }) => { + return ( + <> + + ( + + )}/> + {infoText ? {infoText} : null} + ) +} + +export default TextArea; \ No newline at end of file diff --git a/src/components/FormField/components/TextInput/index.js b/src/components/FormField/components/TextInput/index.js new file mode 100644 index 0000000..1cb445d --- /dev/null +++ b/src/components/FormField/components/TextInput/index.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { classNames } from 'primereact/utils'; +import { Controller } from 'react-hook-form'; +import { InputText } from 'primereact/inputtext'; + +const TextInput = ({ + fieldName, + label, + control, + errors, + defaultValue, + config = {}, + infoText = null, + inputgroup = false, + icon = null + }) => { + const input = ( + + )}/> + return ( + <> + + {inputgroup + ?
+ + {icon} + + {input} +
+ : input} + {infoText ? {infoText} : null} + ) +} + +export default TextInput; \ No newline at end of file diff --git a/src/components/FormField/index.js b/src/components/FormField/index.js new file mode 100644 index 0000000..6cb07b6 --- /dev/null +++ b/src/components/FormField/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { isNil } from 'ramda'; +import { classNames } from 'primereact/utils'; + +// components +import TextInput from './components/TextInput'; +import TextArea from './components/TextArea'; +import Datepicker from './components/Datepicker'; +import DatepickerRange from './components/DatepickerRange'; +import Fileupload from './components/Fileupload'; +import NumberInput from './components/NumberInput'; + +const FormField = (props) => { + const fields = { + textinput: TextInput, + textarea: TextArea, + datepicker: Datepicker, + datepickerrange: DatepickerRange, + fileupload: Fileupload, + numberinput: NumberInput + } + const Comp = !isNil(fields[props.type]) ? fields[props.type] : null; + + return (!isNil(Comp) + ?
+ +
+ : null + ) +} + +export default FormField; \ No newline at end of file diff --git a/src/components/FormFieldRepeater/index.js b/src/components/FormFieldRepeater/index.js new file mode 100644 index 0000000..317f390 --- /dev/null +++ b/src/components/FormFieldRepeater/index.js @@ -0,0 +1,108 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { classNames } from 'primereact/utils'; +import { __ } from '@wordpress/i18n'; +import { InputText } from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import { Menu } from 'primereact/menu'; +import { Dropdown } from 'primereact/dropdown'; + +const FormFieldRepeater = ({ + data, + setDataFn, + fieldName, + options = [], + errors, + register, + label, + infoText + }) => { + const forMenu = useRef(null); + const [stateFieldData, setStateFieldData] = useState([]); + const menuItems = [ + { + type: 'existing', + label: __('Esistente', 'gepafin'), + command: (data) => { + setStateFieldData([...stateFieldData, {id: null, value: '', status: data.item.type}]); + } + }, + { + type: 'new', + label: __('Nuovo', 'gepafin'), + command: (data) => { + setStateFieldData([...stateFieldData, {id: null, value: '', status: data.item.type}]); + } + } + ] + + const removeItem = (index) => { + const newData = stateFieldData.toSpliced(index, 1); + setStateFieldData(newData); + } + + const selectItem = (e, index) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.value = e.value; + } + return o; + }) + setStateFieldData(newData); + } + + const onInputChange = (e, index) => { + const { value } = e.target; + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.value = value; + } + return o; + }) + setStateFieldData(newData); + } + + const properField = (item, i) => { + return item.status === 'new' + ? onInputChange(e, i)}/> + : selectItem(e, i)} + optionDisabled={(opt) => usedExistingValues.includes(opt.value)} + options={options} optionLabel="value"/> + } + + const usedExistingValues = stateFieldData + .filter(o => o.status === 'existing') + .map(o => o.value); + + useEffect(() => { + const storeFieldData = data[fieldName] ?? []; + const newData = storeFieldData.map(o => ({...o, status: o.id ? 'existing' : 'new'})) + setStateFieldData(newData); + register(fieldName) + }, []) + + useEffect(() => { + setDataFn(fieldName, [...stateFieldData]); + }, [stateFieldData]) + + return ( +
+ + {stateFieldData.map((o, i) =>
+
+ {properField(o, i)} +
+ {o.status === 'new' && infoText ? {infoText} : null} +
)} + +
+ ) +} + +export default FormFieldRepeater; \ No newline at end of file diff --git a/src/components/FormFieldRepeaterCriteria/index.js b/src/components/FormFieldRepeaterCriteria/index.js new file mode 100644 index 0000000..f23b533 --- /dev/null +++ b/src/components/FormFieldRepeaterCriteria/index.js @@ -0,0 +1,143 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { classNames } from 'primereact/utils'; +import { __ } from '@wordpress/i18n'; +import { InputText } from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import { Menu } from 'primereact/menu'; +import { Dropdown } from 'primereact/dropdown'; +import { InputNumber } from 'primereact/inputnumber'; + +const FormFieldRepeaterCriteria = ({ + data, + setDataFn, + fieldName, + options = [], + errors, + register, + label, + infoText + }) => { + const forMenu = useRef(null); + const [stateFieldData, setStateFieldData] = useState([]); + const menuItems = [ + { + type: 'existing', + label: __('Esistente', 'gepafin'), + command: (data) => { + setStateFieldData([...stateFieldData, { id: null, value: '', status: data.item.type }]); + } + }, + { + type: 'new', + label: __('Nuovo', 'gepafin'), + command: (data) => { + setStateFieldData([...stateFieldData, { id: null, value: '', status: data.item.type }]); + } + } + ] + + const removeItem = (index) => { + const newData = stateFieldData.toSpliced(index, 1); + setStateFieldData(newData); + } + + const selectItem = (e, index) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.value = e.value; + } + return o; + }) + setStateFieldData(newData); + } + + const onInputChange = (value, index, name) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o[name] = value; + } + return o; + }) + setStateFieldData(newData); + } + + const properField = (item, i) => { + return item.status === 'new' + ? onInputChange(e.target.value, i, 'value')}/> + : selectItem(e, i)} + optionDisabled={(opt) => usedExistingValues.includes(opt.value)} + options={options} optionLabel="value"/> + } + + const usedExistingValues = stateFieldData + .filter(o => o.status === 'existing') + .map(o => o.value); + + useEffect(() => { + const storeFieldData = data[fieldName] ?? []; + const newData = storeFieldData.map(o => ({ ...o, status: o.id ? 'existing' : 'new' })) + setStateFieldData(newData); + register(fieldName) + }, []) + + useEffect(() => { + setDataFn(fieldName, [...stateFieldData]); + }, [stateFieldData]) + + return ( +
+ + {stateFieldData.map((o, i) =>
+
+
+ + onInputChange(e.value, i, 'total')}/> +
+
+ + onInputChange(e.value, i, 'threshold')}/> +
+
+
+
+ +
+ {properField(o, i)} +
+ {o.status === 'new' && infoText ? {infoText} : null} +
+
+ + onInputChange(e.value, i, 'min')}/> +
+
+ + onInputChange(e.value, i, 'max')}/> +
+
+
)} + +
+ ) +} + +export default FormFieldRepeaterCriteria; \ No newline at end of file diff --git a/src/components/FormFieldRepeaterFaq/index.js b/src/components/FormFieldRepeaterFaq/index.js new file mode 100644 index 0000000..d20e308 --- /dev/null +++ b/src/components/FormFieldRepeaterFaq/index.js @@ -0,0 +1,122 @@ +import React, { useRef, useEffect, useState } from 'react'; +import { classNames } from 'primereact/utils'; +import { __ } from '@wordpress/i18n'; +import { InputText } from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import { Dropdown } from 'primereact/dropdown'; +import { Accordion, AccordionTab } from 'primereact/accordion'; +import { ToggleButton } from 'primereact/togglebutton'; + +const FormFieldRepeaterFaq = ({ + data, + setDataFn, + fieldName, + options = [], + errors, + register, + label, + infoText + }) => { + const [stateFieldData, setStateFieldData] = useState([]); + + const removeItem = (index) => { + const newData = stateFieldData.toSpliced(index, 1); + setStateFieldData(newData); + } + + const selectItem = () => { + setStateFieldData([...stateFieldData, { id: 0, status: 'new', question: '', answer: '', visible: true }]); + } + + const onInputChange = (e, index) => { + const { value } = e.target; + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.value = value; + } + return o; + }) + setStateFieldData(newData); + } + + const addNewItem = () => { + + } + + const setChecked = (e, index) => { + e.preventDefault(); + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.visible = e.value; + } + return o; + }); + setStateFieldData(newData); + } + + const editItem = (e, index) => { + e.stopPropagation(); + console.log('editItem') + } + + const usedExistingValues = stateFieldData + .filter(o => o.status === 'existing') + .map(o => o.question); + + useEffect(() => { + const storeFieldData = data[fieldName] ?? []; + const newData = storeFieldData.map(o => ({ ...o, status: o.id ? 'existing' : 'new' })) + setStateFieldData(newData); + register(fieldName) + }, []) + + useEffect(() => { + setDataFn(fieldName, [...stateFieldData]); + }, [stateFieldData]) + + return ( +
+ +
+
+ + {stateFieldData.map((o, i) => +
+ setChecked(e, i)}/> + {o.question} +
+
+
+
+ } + > +

+ {o.answer} +

+ )} + + + ) +} + +export default FormFieldRepeaterFaq; \ No newline at end of file diff --git a/src/pages/Bando/index.js b/src/pages/Bando/index.js index c9b0086..cbd9ac6 100644 --- a/src/pages/Bando/index.js +++ b/src/pages/Bando/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { __ } from '@wordpress/i18n'; import { useParams } from 'react-router-dom'; import { useForm, Controller } from 'react-hook-form'; @@ -10,10 +10,17 @@ import { InputTextarea } from 'primereact/inputtextarea'; import getBandoLabel from '../../helpers/getBandoLabel'; import { Button } from 'primereact/button'; import { Dropdown } from 'primereact/dropdown'; +import { Menu } from 'primereact/menu'; +import FormField from '../../components/FormField'; +import FormFieldRepeater from '../../components/FormFieldRepeater'; +import { Skeleton } from 'primereact/skeleton'; +import FormFieldRepeaterCriteria from '../../components/FormFieldRepeaterCriteria'; +import FormFieldRepeaterFaq from '../../components/FormFieldRepeaterFaq'; const Bando = () => { const { id } = useParams(); const [data, setData] = useState({}); + const [isFormLoading, setIsFormLoading] = useState(true); const [selectedTemplate, setSelectedTemplate] = useState(null); const [templates, setTemplate] = useState(null); const { @@ -21,10 +28,24 @@ const Bando = () => { reset, handleSubmit, formState: { errors }, - getValues + getValues, + setValue, + register } = useForm(data); - const onSubmit = data => console.log(data); - console.log('data', data); + let minDateStart = new Date(); + + const onSubmit = formData => console.log(formData); + + // temp + const exampleOfAimedToOptions = [{ id: 11, value: 'PMI con sede in Umbria' }]; + const exampleOfCriteriaOptions = [{ id: 15, value: 'Innovatività del progetto' }]; + const exampleOfFaqOptions = [ + { id: 2, question: 'Question 1?', answer: 'Lorem ipsum dolor' } + ]; + const exampleOfChecklistOptions = [ + { id: 9, value: 'Requisiti di ammissibilità soddisfatti' }, + { id: 9, value: 'Documentazione completa' } + ]; const onPublish = () => { console.log('click onPublish'); @@ -34,29 +55,32 @@ const Bando = () => { const parsed = parseInt(id) const bandoId = !isNaN(parsed) ? parsed : 0; - const data = 0 === bandoId - ? { - status: 'draft', - name: '', - description: '' - } - : { - name: 'Bando Innovazione 2024', - description: '', - start_date: '2024-08-08T00:00:00+00:00', - end_date: '2024-08-30T00:00:00+00:00', - submissions: 24, - status: 'publish', - id: 11 - } - setData(data); - reset(); + setTimeout(() => { + const data = 0 === bandoId + ? { + status: 'draft', + name: '', + description: '' + } + : { + name: 'Bando Innovazione 2024', + description: '', + start_date: '2024-08-08T00:00:00+00:00', + end_date: '2024-08-30T00:00:00+00:00', + submissions: 24, + status: 'publish', + id: 11 + } + setData(data); + reset(); - const templates = [ - { name: 'Il mio template', value: 22 }, - { name: 'Template #11', value: 11 }, - ]; - setTemplate(templates) + const templates = [ + { name: 'Il mio template', value: 22 }, + { name: 'Template #11', value: 11 }, + ]; + setTemplate(templates); + setIsFormLoading(false); + }, 3000); }, [id]); return ( @@ -71,65 +95,139 @@ const Bando = () => {
-
-
- - setSelectedTemplate(e.value)} - options={templates} - optionLabel="name" - placeholder={__('Seleziona template', 'gepafin')} /> -
-
+ {!isFormLoading + ?
+
+ + setSelectedTemplate(e.value)} + options={templates} + optionLabel="name" + placeholder={__('Seleziona template', 'gepafin')}/> +
+
: null}
- {data + {!isFormLoading ?
-
- - ( - - )}/> -
+ -
- - ( - - )}/> -
+ + + {__('A chi si rivolge', 'gepafin')}* + {__('(almeno 1 tipo di destinatari)', 'gepafin')}} + /> + + + + + + {__('Criteri di valutazione', 'gepafin')}* + {__('(almeno 1 criterio di valutazione)', 'gepafin')}}/> + + + + + + + + {__('Checklist valutazione Pre-Istruttoria', 'gepafin')}* + {__('(almeno 1 elemento)', 'gepafin')}} + />
@@ -137,11 +235,18 @@ const Bando = () => { type="submit" label={__('Salva Bozza', 'gepafin')} icon="pi pi-save" iconPos="right"/>
- : null} + + : <> + + + + + } ) }