index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import React, { useState, forwardRef } from 'react';
  2. import Recipe, { Ingredient, Suggestion, _ingredient, _subRecipe, DBRecipe } from 'types/recipes';
  3. import { Grid, Row, Cell } from '@material/react-layout-grid';
  4. import { Field, FieldArray, Formik, useField, Form, FieldArrayRenderProps } from 'formik';
  5. import { MeasuresTypes } from 'services/measurements';
  6. import { Input, Textarea, ControlledInput } from 'components/Input';
  7. import Button from 'components/Button';
  8. import Dialog from 'components/DialogConverter';
  9. import TagInput from 'components/TagInput';
  10. import DurationPicker from 'components/DurationPicker';
  11. import sample_image from 'sample.png';
  12. import './styles.scss';
  13. type RecipeFormProps<T> = {
  14. initialValues: T,
  15. onSubmit: (data: T) => void
  16. }
  17. const convertIngredient = (ingredient: Ingredient, suggestion: Suggestion): Ingredient => {
  18. if (ingredient.unit === suggestion.referenceUnit) {
  19. return {
  20. ...ingredient,
  21. unit: suggestion.prefferedUnit,
  22. quantity: suggestion.equivalence * ingredient.quantity
  23. }
  24. } else {
  25. if (ingredient.unit === suggestion.prefferedUnit) {
  26. alert('Reset values to original plox')
  27. }
  28. }
  29. return ingredient;
  30. }
  31. type FormikInputProps = {
  32. [x: string]: any,
  33. name: string,
  34. label?: string,
  35. };
  36. const FormikInput = ({ label, ...props }:FormikInputProps) => {
  37. const [field, meta] = useField(props);
  38. return (
  39. <>
  40. <Input
  41. label={label}
  42. { ...props }
  43. { ...field }
  44. />
  45. {/* TODO: Add validation errors when onSubmit */}
  46. {meta.touched && meta.error ? (
  47. <div className="error">{meta.error}</div>
  48. ) : null}
  49. </>
  50. );
  51. };
  52. const FormikFocusInput = forwardRef<HTMLInputElement, FormikInputProps>(({ label, ...props }, ref) => {
  53. const [field, meta] = useField(props);
  54. return (
  55. <>
  56. <ControlledInput
  57. ref={ref}
  58. label={label}
  59. { ...props }
  60. { ...field }
  61. />
  62. {/* TODO: Add validation errors when onSubmit */}
  63. {meta.touched && meta.error ? (
  64. <div className="error">{meta.error}</div>
  65. ) : null}
  66. </>
  67. );
  68. });
  69. const FormikTextarea = ({ label, ...props }: FormikInputProps) => {
  70. const [field, meta] = useField(props);
  71. return (
  72. <>
  73. <Textarea
  74. label={label}
  75. { ...props }
  76. { ...field }
  77. />
  78. {/* TODO: Add validation errors when onSubmit */}
  79. {meta.touched && meta.error ? (
  80. <div className="error">{meta.error}</div>
  81. ) : null}
  82. </>
  83. );
  84. };
  85. const RecipeForm = <T extends Recipe|DBRecipe>({ initialValues, onSubmit }: RecipeFormProps<T>) => {
  86. const [selectedTab, setSelectedTab] = useState(0);
  87. const [focusLast, setFocusLast] = useState(false);
  88. return (
  89. <div>
  90. <h4>New form</h4>
  91. <Formik
  92. enableReinitialize
  93. initialValues={initialValues}
  94. onSubmit={(values) => {
  95. console.log('submit', values)
  96. }}
  97. >
  98. {
  99. ({setFieldValue, submitForm, values}) => {
  100. return (
  101. <Grid>
  102. <Form className='cbk-recipe-form'>
  103. <Row>
  104. <Cell columns={2}>
  105. <img src={sample_image} style={{ width: '100%' }}/>
  106. </Cell>
  107. <Cell columns={10}>
  108. <FormikInput name='name' label='name' />
  109. <FormikTextarea name='summary' label='summary' />
  110. </Cell>
  111. </Row>
  112. <h5>Author Information</h5>
  113. <section>
  114. <Row>
  115. <Cell columns={3}>
  116. <FormikInput name='author.name' label='name' />
  117. </Cell>
  118. <Cell columns={3}>
  119. <FormikInput name='author.website' label='website' />
  120. </Cell>
  121. </Row>
  122. </section>
  123. <h5>Recipe Information</h5>
  124. <section>
  125. <Row>
  126. <Cell columns={3}>
  127. {/* <FormikInput name='details.preparationTime' label='preparation time' /> */}
  128. <DurationPicker
  129. initialValue={values.details.preparationTime}
  130. onChange={(duration)=>{ setFieldValue('details.preparationTime', duration)}}
  131. label='preparation time'
  132. />
  133. </Cell>
  134. <Cell columns={3}>
  135. <DurationPicker
  136. initialValue={values.details.cookingTime}
  137. onChange={(duration)=>{ setFieldValue('details.cookingTime', duration)}}
  138. label='cooking time'
  139. />
  140. </Cell>
  141. <Cell columns={3}>
  142. <FormikInput name='details.servings' label='servings' type='number' />
  143. </Cell>
  144. <Cell columns={3}>
  145. <FormikInput name='details.url' label='recipe url' />
  146. </Cell>
  147. </Row>
  148. <Row>
  149. <Cell columns={12}>
  150. <TagInput
  151. tags={values.tags}
  152. label='tags'
  153. onNewTag={(tags) => setFieldValue('tags', tags)}
  154. />
  155. </Cell>
  156. </Row>
  157. <Row>
  158. <Cell columns={12}>
  159. <TagInput
  160. tags={values.course}
  161. label='dish course'
  162. onNewTag={(dishCourse) => {console.log("dish", dishCourse, values); setFieldValue('course', dishCourse)}}
  163. />
  164. </Cell>
  165. </Row>
  166. </section>
  167. <h5>Ingredients</h5>
  168. <section>
  169. <FieldArray name='ingredients'>
  170. {({remove, push}) => {
  171. return (
  172. <div className='subrecipe-tabs'>
  173. <ul className='tab-header'>
  174. {
  175. values.ingredients.map((subrecipe, subrecipeIdx) => (
  176. <li
  177. className={`tab${selectedTab === subrecipeIdx ? ' tab--selected' : ''}`}
  178. key={subrecipeIdx}
  179. >
  180. <div className='tab__content' onClick={()=> setSelectedTab(subrecipeIdx)}>
  181. <Field name={`ingredients[${subrecipeIdx}].name`} placeholder='subrecipe name'/>
  182. </div>
  183. <Button
  184. type='button'
  185. icon='clear'
  186. onClick={() => remove(subrecipeIdx)}
  187. disabled={values.ingredients.length === 1}
  188. small
  189. ></Button>
  190. <span></span>
  191. </li>
  192. ))
  193. }
  194. <li className='tab tab--add'>
  195. <Button type='button' icon='add' onClick={() => push({..._subRecipe})}></Button>
  196. </li>
  197. </ul>
  198. <div className='ingredients-form'>
  199. <Row className='ingredients-form-header'>
  200. <Cell columns={3}>
  201. Ingredient Name
  202. </Cell>
  203. <Cell columns={2}>
  204. Quantity
  205. </Cell>
  206. <Cell columns={1}>
  207. Unit
  208. </Cell>
  209. <Cell columns={2}>
  210. Notes
  211. </Cell>
  212. <Cell columns={3}>
  213. Original
  214. </Cell>
  215. </Row>
  216. <FieldArray name={`ingredients[${selectedTab}].ingredients`}>
  217. { (ingredientHelpers: any) => {
  218. return (
  219. <div>
  220. {
  221. values.ingredients[selectedTab] && values.ingredients[selectedTab].ingredients.map((ingredient, index, array) => (
  222. <div key={index} className='ingredients-form-item'>
  223. <Row className='ingredient-detail'>
  224. <Cell columns={3}>
  225. <FormikFocusInput name={`ingredients[${selectedTab}].ingredients[${index}].name`}
  226. ref={(ref: any) => {
  227. if(index === array.length-1 && ref && focusLast) {
  228. ref.focus();
  229. setFocusLast(false);
  230. }
  231. }}
  232. />
  233. </Cell>
  234. <Cell columns={2}>
  235. <FormikInput name={`ingredients[${selectedTab}].ingredients[${index}].quantity`} type='number'/>
  236. {ingredient.unit && ingredient.quantity ? (
  237. <Dialog
  238. measure={{unit: ingredient.unit, quantity: ingredient.quantity}}
  239. onConvert={result => ingredientHelpers.replace(index, { ...ingredient, ...result })}
  240. />
  241. ): null}
  242. </Cell>
  243. <Cell columns={1}>
  244. <Field component='select' name={`ingredients[${selectedTab}].ingredients[${index}].unit`}>
  245. <option value=''></option>
  246. {
  247. MeasuresTypes.map((measure, measureIdx) => (
  248. <option key={measureIdx} value={measure}>{measure}</option>
  249. ))
  250. }
  251. </Field>
  252. </Cell>
  253. <Cell columns={2}>
  254. <FormikInput name={`ingredients[${selectedTab}].ingredients[${index}].notes`}/>
  255. </Cell>
  256. <Cell columns={3}>
  257. <FormikInput name={`ingredients[${selectedTab}].ingredients[${index}]._original`} disabled/>
  258. </Cell>
  259. <Cell columns={1}>
  260. <Button type='button' icon='clear' onClick={() => {
  261. ingredientHelpers.remove(index)
  262. }}
  263. disabled={values.ingredients[selectedTab].ingredients.length === 1}
  264. small></Button>
  265. </Cell>
  266. </Row>
  267. <div className='ingredient-suggestions'>
  268. {
  269. ingredient.suggestions && ingredient.suggestions.map((suggestion, sugIdx) => (
  270. <button
  271. className='suggestion'
  272. type='button'
  273. key={sugIdx}
  274. onClick={() => {
  275. setFieldValue(
  276. `ingredients[${selectedTab}].ingredients[${index}]`,
  277. convertIngredient(ingredient, suggestion)
  278. )
  279. }}
  280. >{suggestion.name}</button>
  281. ))
  282. }
  283. </div>
  284. </div>
  285. ))
  286. }
  287. <Button type='button' onClick={() => { ingredientHelpers.push({ ..._ingredient }); setFocusLast(true);}}>
  288. add ingredient
  289. </Button>
  290. </div>
  291. )
  292. }}
  293. </FieldArray>
  294. </div>
  295. </div>
  296. );
  297. }}
  298. </FieldArray>
  299. </section>
  300. <h5>Instructions</h5>
  301. <section>
  302. <FieldArray name='instructions'>
  303. {({ remove, push }) => (
  304. <div>
  305. {
  306. values.instructions.map((instruction, instructionIdx) => (
  307. <Row key={instructionIdx}>
  308. <Cell columns={11}>
  309. <Field name={`instructions[${instructionIdx}]`}/>
  310. </Cell>
  311. <Cell columns={1}>
  312. <Button icon='clear' onClick={() => remove(instructionIdx)} small></Button>
  313. </Cell>
  314. </Row>
  315. ))
  316. }
  317. <Button type="button" onClick={() => push('')}>Add instruction</Button>
  318. </div>
  319. )}
  320. </FieldArray>
  321. </section>
  322. <Button type='button' raised unelevated onClick={() => submitForm()} >Submit</Button>
  323. </Form>
  324. </Grid>
  325. )}
  326. }
  327. </Formik>
  328. </div>
  329. )};
  330. export default RecipeForm;