Create.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import React from 'react';
  2. import { assocPath, remove } from 'ramda';
  3. import Navbar from 'components/Navbar';
  4. import { Grid, Row, Cell } from '@material/react-layout-grid';
  5. import Btn from 'components/Button';
  6. import Input from 'components/Input';
  7. import TagInput from 'components/TagInput';
  8. import Select from 'components/Select';
  9. import { Form as IngredientForm } from 'components/Ingredient';
  10. import './styles.scss';
  11. import { scrapeRecipe } from './services';
  12. import sample_img from "../../sample.png";
  13. import Recipe, { SubRecipe, Author, Details, _recipe, _subRecipe, _ingredient, Ingredient, Suggestion } from 'types/recipes';
  14. type CreateRecipeProps = {
  15. };
  16. type CreateRecipeState = {
  17. form: Recipe,
  18. scrapeUrl: string,
  19. };
  20. type FormKeys = keyof Recipe | keyof SubRecipe | keyof Ingredient | keyof Author | keyof Details | number;
  21. const Suggestions = (suggestions: Suggestion[] = []) => (
  22. <div>
  23. {
  24. suggestions.map(suggestion => (
  25. <Btn>{suggestion.name}</Btn>
  26. ))
  27. }
  28. </div>
  29. )
  30. class CreateRecipe extends React.Component<CreateRecipeProps, CreateRecipeState> {
  31. constructor(props: any) {
  32. super(props);
  33. this.state = {
  34. form: {
  35. ..._recipe,
  36. ingredients: [
  37. {
  38. "name": "For the Chicken:",
  39. "ingredients": [
  40. {
  41. "name": "thin boneless skinless chicken breast",
  42. "quantity": 1.5,
  43. "unit": "pound",
  44. "_original": "1-1/2 lb of Thin Boneless Skinless Chicken Breast",
  45. "suggestions": []
  46. },
  47. {
  48. "name": "olive oil",
  49. "quantity": 2,
  50. "unit": "tablespoon",
  51. "_original": "2 Tbsp of Olive Oil",
  52. "suggestions": []
  53. },
  54. {
  55. "name": "juice of lime",
  56. "quantity": 1,
  57. "unit": "",
  58. "_original": "Juice of 1 Lime",
  59. "suggestions": []
  60. },
  61. {
  62. "name": "chili powder",
  63. "quantity": 0.5,
  64. "unit": "teaspoon",
  65. "_original": "1/2 tsp of Chili Powder",
  66. "suggestions": [
  67. {
  68. "_id": "5cf1397d72cd194524435041",
  69. "name": "cocoa powder",
  70. "variants": [
  71. "cocoa"
  72. ],
  73. "equivalence": 118,
  74. "referenceUnit": "cup",
  75. "prefferedUnit": "gr",
  76. "measure": 2
  77. },
  78. {
  79. "_id": "5cf1397d72cd19452443503c",
  80. "name": "icing sugar",
  81. "variants": [
  82. "confectioner sugar",
  83. "powdered sugar"
  84. ],
  85. "equivalence": 125,
  86. "referenceUnit": "cup",
  87. "prefferedUnit": "gr",
  88. "measure": 2
  89. }
  90. ]
  91. },
  92. {
  93. "name": "oregano",
  94. "quantity": 0.5,
  95. "unit": "teaspoon",
  96. "_original": "1/2 tsp of Oregano",
  97. "suggestions": []
  98. },
  99. {
  100. "name": "cumin",
  101. "quantity": 0.5,
  102. "unit": "teaspoon",
  103. "_original": "1/2 tsp of Cumin",
  104. "suggestions": []
  105. },
  106. {
  107. "name": "paprika",
  108. "quantity": 0.5,
  109. "unit": "teaspoon",
  110. "_original": "1/2 tsp of Paprika",
  111. "suggestions": []
  112. },
  113. {
  114. "name": "granulated garlic",
  115. "quantity": 0.5,
  116. "unit": "teaspoon",
  117. "_original": "1/2 tsp of Granulated Garlic",
  118. "suggestions": [
  119. {
  120. "_id": "5cf1397d72cd19452443503b",
  121. "name": "granulated sugar",
  122. "variants": [
  123. "sugar",
  124. "plain sugar"
  125. ],
  126. "equivalence": 200,
  127. "referenceUnit": "cup",
  128. "prefferedUnit": "gr",
  129. "measure": 2
  130. }
  131. ]
  132. },
  133. {
  134. "name": "salt, to taste",
  135. "quantity": 0,
  136. "unit": "",
  137. "_original": "Salt, to taste",
  138. "suggestions": []
  139. }
  140. ]
  141. },
  142. {
  143. "name": "For the Dressing:",
  144. "ingredients": [
  145. {
  146. "name": "plain greek yogurt",
  147. "quantity": 0.5,
  148. "unit": "cup",
  149. "_original": "1/2 cup of Plain Greek Yogurt",
  150. "suggestions": [
  151. {
  152. "_id": "5cf1397d72cd194524435040",
  153. "name": "yogurt",
  154. "variants": [
  155. "plain yogurt"
  156. ],
  157. "equivalence": 245,
  158. "referenceUnit": "cup",
  159. "prefferedUnit": "gr",
  160. "measure": 2
  161. },
  162. {
  163. "_id": "5cf1397d72cd19452443503b",
  164. "name": "granulated sugar",
  165. "variants": [
  166. "sugar",
  167. "plain sugar"
  168. ],
  169. "equivalence": 200,
  170. "referenceUnit": "cup",
  171. "prefferedUnit": "gr",
  172. "measure": 2
  173. },
  174. {
  175. "_id": "5cf1397d72cd194524435030",
  176. "name": "all purpose flour",
  177. "variants": [
  178. "plain flour",
  179. "all purpose flour",
  180. "regular flour",
  181. "flour"
  182. ],
  183. "equivalence": 125,
  184. "referenceUnit": "cup",
  185. "prefferedUnit": "gr",
  186. "translation": {
  187. "dutch": "patentbloem"
  188. },
  189. "measure": 2
  190. }
  191. ]
  192. },
  193. {
  194. "name": "fresh cilantro",
  195. "quantity": 1,
  196. "unit": "cup",
  197. "_original": "1 cup of Fresh Cilantro",
  198. "suggestions": []
  199. },
  200. {
  201. "name": "scallions, roughly chopped",
  202. "quantity": 2,
  203. "unit": "",
  204. "_original": "2 Scallions, roughly chopped",
  205. "suggestions": []
  206. },
  207. {
  208. "name": "juice of lime or more, according to taste",
  209. "quantity": 1,
  210. "unit": "",
  211. "_original": "Juice of 1 Lime or more, according to taste",
  212. "suggestions": []
  213. },
  214. {
  215. "name": "olive oil",
  216. "quantity": 1,
  217. "unit": "tablespoon",
  218. "_original": "1 Tbsp of Olive Oil",
  219. "suggestions": []
  220. },
  221. {
  222. "name": "salt, to taste",
  223. "quantity": 0,
  224. "unit": "",
  225. "_original": "Salt, to taste",
  226. "suggestions": []
  227. }
  228. ]
  229. },
  230. {
  231. "name": "For the rest of the salad:",
  232. "ingredients": [
  233. {
  234. "name": "fresh lettuce of your choice",
  235. "quantity": 0,
  236. "unit": "",
  237. "_original": "Fresh Lettuce of your choice",
  238. "suggestions": []
  239. },
  240. {
  241. "name": "bell peppers, halved and seeded",
  242. "quantity": 2,
  243. "unit": "",
  244. "_original": "2 Bell Peppers, halved and seeded",
  245. "suggestions": []
  246. },
  247. {
  248. "name": "scallions or red onion, sliced",
  249. "quantity": 0,
  250. "unit": "",
  251. "_original": "Scallions or REd Onion, sliced",
  252. "suggestions": []
  253. },
  254. {
  255. "name": "pico de gallo salsa",
  256. "quantity": 0.5,
  257. "unit": "cup",
  258. "_original": "1/2 cup of Pico De Gallo Salsa",
  259. "suggestions": []
  260. },
  261. {
  262. "name": "avocado, sliced",
  263. "quantity": 1,
  264. "unit": "",
  265. "_original": "1 Avocado, sliced",
  266. "suggestions": []
  267. }
  268. ]
  269. }
  270. ],
  271. },
  272. scrapeUrl: '',
  273. }
  274. }
  275. scrapeRecipe = () => {
  276. scrapeRecipe(this.state.scrapeUrl).then(recipe => {
  277. this.setState({
  278. form: {
  279. ...recipe,
  280. details: {
  281. url: this.state.scrapeUrl,
  282. ...recipe.details,
  283. }
  284. }
  285. })
  286. })
  287. }
  288. updateField = (key: FormKeys[]) => {
  289. return (e: any) => {
  290. const newValue = e.currentTarget.value;
  291. this.setState({
  292. form: assocPath(key, newValue, this.state.form)
  293. } as Pick<CreateRecipeState, keyof CreateRecipeState>)
  294. }
  295. }
  296. updateSubrecipeName = (subrecipe: number, newValue: string) => {
  297. this.setState({
  298. form: assocPath(['ingredients', subrecipe, 'name'], newValue, this.state.form),
  299. } as Pick<CreateRecipeState, keyof CreateRecipeState>)
  300. }
  301. updateIngredientUnit = (key: Array<string | number | symbol>, newValue: string) => {
  302. console.log('change', key, newValue)
  303. this.setState({
  304. form: assocPath(key, newValue, this.state.form)
  305. })
  306. }
  307. addIngredient = (subrecipe: number, last: number) => {
  308. return () => {
  309. const { ingredients } = this.state.form;
  310. if (ingredients[subrecipe].ingredients[last].name) {
  311. const newIngredients = ingredients[subrecipe].ingredients.concat([_ingredient]);
  312. this.setState({
  313. form: {
  314. ...this.state.form,
  315. ingredients: assocPath([subrecipe, 'ingredients'], newIngredients, this.state.form.ingredients)
  316. }
  317. })
  318. }
  319. }
  320. }
  321. removeIngredient = (subrecipe: number, index: number) => {
  322. return () => {
  323. const { ingredients } = this.state.form;
  324. this.setState({
  325. form: {
  326. ...this.state.form,
  327. ingredients: assocPath(
  328. [subrecipe, 'ingredients'],
  329. remove(index, 1, ingredients[subrecipe].ingredients),
  330. this.state.form.ingredients
  331. )
  332. }
  333. })
  334. }
  335. }
  336. addInstruction = () => {
  337. this.setState({
  338. form: {
  339. ...this.state.form,
  340. instructions: this.state.form.instructions.concat([''])
  341. }
  342. })
  343. }
  344. removeInstruction = (index: number) => {
  345. return () => {
  346. this.setState({
  347. form: {
  348. ...this.state.form,
  349. instructions: remove(index, 1, this.state.form.instructions)
  350. }
  351. })
  352. }
  353. }
  354. actionButton = (path: any[], index: number, AddFc: () => void, RemoveFc: () => void) => {
  355. const isLast = (xs: any[], i: number) => xs.length === i + 1;
  356. return isLast(path, index) ?
  357. {
  358. icon: 'add_circle_outline',
  359. onClick: AddFc
  360. } : {
  361. icon: 'remove_circle_outline',
  362. onClick: RemoveFc
  363. }
  364. }
  365. addSubrecipe = () => {
  366. this.setState({
  367. form: {
  368. ...this.state.form,
  369. ingredients: this.state.form.ingredients.concat([_subRecipe]),
  370. }
  371. })
  372. }
  373. removeSubrecipe = (index: number) => {
  374. this.setState({
  375. form: {
  376. ...this.state.form,
  377. ingredients: remove(index, 1, this.state.form.ingredients)
  378. }
  379. })
  380. }
  381. render() {
  382. const { form, scrapeUrl } = this.state;
  383. return (
  384. <div>
  385. <Navbar
  386. title="Create a recipe"
  387. >
  388. <Input
  389. label='scrape recipe'
  390. value={scrapeUrl}
  391. onChange={(e: any)=>{
  392. console.log('data', e.currentTarget.value);
  393. this.setState({scrapeUrl: e.currentTarget.value})}}
  394. button={{
  395. icon: 'format_shapes',
  396. onClick: this.scrapeRecipe
  397. }}
  398. />
  399. </Navbar>
  400. <div className="cbk-create-recipe">
  401. <Grid>
  402. <Row>
  403. <Cell columns={2}>
  404. <img src={sample_img} style={{ width: '100%' }}/>
  405. </Cell>
  406. <Cell columns={10}>
  407. <Input
  408. label='name'
  409. value={form.name}
  410. onChange={this.updateField(['name'])}
  411. style='display'
  412. />
  413. <Input
  414. label='summary'
  415. value={form.summary}
  416. onChange={this.updateField(['summary'])}
  417. textarea
  418. />
  419. </Cell>
  420. </Row>
  421. <h5>Author Information</h5>
  422. <section>
  423. <Row>
  424. <Cell columns={3}>
  425. <Input
  426. label='name'
  427. value={form.author.name}
  428. onChange={this.updateField(['author', 'name'])}
  429. />
  430. </Cell>
  431. <Cell columns={3}>
  432. <Input
  433. label='website'
  434. value={form.author.website}
  435. onChange={this.updateField(['author', 'website'])}
  436. />
  437. </Cell>
  438. </Row>
  439. </section>
  440. <h5>Recipe Information</h5>
  441. <section>
  442. <Row>
  443. <Cell columns={3}>
  444. <Input
  445. label='Preparation time'
  446. value={form.details.preparationTime}
  447. onChange={this.updateField(['details', 'preparationTime'])}
  448. icon='preparation'
  449. />
  450. </Cell>
  451. <Cell columns={3}>
  452. <Input
  453. label='Cooking time'
  454. value={form.details.cookingTime}
  455. onChange={this.updateField(['details', 'cookingTime'])}
  456. icon='cooking'
  457. />
  458. </Cell>
  459. <Cell columns={3}>
  460. <Input
  461. label='servings'
  462. value={form.details.servings}
  463. onChange={this.updateField(['details', 'servings'])}
  464. type='number'
  465. icon='servings'
  466. />
  467. </Cell>
  468. <Cell columns={3}>
  469. <Input
  470. label='recipe url'
  471. value={form.details.url || ''}
  472. onChange={this.updateField(['details', 'url'])}
  473. icon='recipe'
  474. />
  475. </Cell>
  476. <Cell columns={12}>
  477. <TagInput
  478. initialValues={form.tags}
  479. onNewTag={(tags) => { this.setState({
  480. form: {
  481. ...this.state.form,
  482. tags,
  483. }
  484. })}}
  485. />
  486. </Cell>
  487. </Row>
  488. </section>
  489. <h5>Ingredients</h5>
  490. <section>
  491. <IngredientForm
  492. components={form.ingredients}
  493. updateField={this.updateField}
  494. addSubrecipe={this.addSubrecipe}
  495. updateSubrecipeName={this.updateSubrecipeName}
  496. removeSubrecipe={this.removeSubrecipe}
  497. updateIngredientUnit={this.updateIngredientUnit}
  498. />
  499. {/* {
  500. form.ingredients.map((group, i) => (
  501. <div key={i}>
  502. <Row>
  503. <Cell columns={4}>
  504. <Input
  505. label='group name'
  506. value={group.name}
  507. onChange={this.updateField(['ingredients', i, 'name'])}
  508. button={this.actionButton(form.ingredients, i, this.addSubrecipe, this.removeSubrecipe(i))}
  509. />
  510. </Cell>
  511. </Row>
  512. <div>
  513. {
  514. group.ingredients.map((ingredient, j) => (
  515. <Row key={j}>
  516. <Cell columns={4}>
  517. <Input
  518. label="ingredient"
  519. value={ingredient.name}
  520. onChange={this.updateField(['ingredients', i, 'ingredients', j, 'name'])}
  521. />
  522. </Cell>
  523. <Cell columns={2}>
  524. <Input
  525. label="quantity"
  526. value={ingredient.quantity}
  527. onChange={this.updateField(['ingredients', i, 'ingredients', j, 'quantity'])}
  528. />
  529. </Cell>
  530. <Cell columns={2}>
  531. <Select
  532. label='unit'
  533. options={[
  534. {label: 'grams', value: 'gr'},
  535. {label: 'cups', value: 'cup'},
  536. {label: 'mililiters', value: 'ml'},
  537. ]}
  538. onChange={(x) => {console.log('selected', x)}}
  539. />
  540. </Cell>
  541. <Cell columns={4}>
  542. <Input
  543. label="original/notes"
  544. value={ingredient._original}
  545. onChange={this.updateField(['ingredients', i, 'ingredients', j, '_original'])}
  546. button={this.actionButton(form.ingredients[i].ingredients, j, this.addIngredient(i, j), this.removeIngredient(i, j))}
  547. />
  548. </Cell>
  549. <Cell columns={12}>
  550. { Suggestions(ingredient.suggestions) }
  551. </Cell>
  552. </Row>
  553. ))
  554. }
  555. </div>
  556. </div>
  557. ))
  558. } */}
  559. </section>
  560. <h5>Instructions</h5>
  561. <section>
  562. {
  563. form.instructions.map((instruction, i) => (
  564. <Row key={i}>
  565. <Cell columns={12}>
  566. <Input
  567. label={`Step ${i+1}`}
  568. value={instruction}
  569. onChange={this.updateField(['instructions', i])}
  570. button={this.actionButton(form.instructions, i, this.addInstruction, this.removeInstruction(i))}
  571. />
  572. </Cell>
  573. </Row>
  574. ))
  575. }
  576. </section>
  577. <Row>
  578. <Cell columns={12} className='cbk-create-recipe-actions'>
  579. <Btn raised unelevated>Create</Btn>
  580. </Cell>
  581. </Row>
  582. </Grid>
  583. </div>
  584. </div>
  585. )
  586. }
  587. }
  588. export default CreateRecipe;