source: frontend/src/pages/Recipes.jsx@ 2e0035f

Last change on this file since 2e0035f was 2e0035f, checked in by Acko <lavurovski.a65@…>, 8 days ago

Initial Commit

  • Property mode set to 100644
File size: 5.6 KB
Line 
1import { useEffect, useState } from 'react'
2import { api } from '../lib/api.js'
3
4/** UseCase1001 (create a recipe) and UseCase1004 (share it as a post). */
5export default function Recipes() {
6 const [mine, setMine] = useState([])
7 const [catalog, setCatalog] = useState([])
8 const [restrictions, setRestrictions] = useState([])
9 const [detail, setDetail] = useState(null)
10 const [error, setError] = useState(null)
11 const [form, setForm] = useState({ name: '', guide: '', servings: 4, restrictionIds: [] })
12 const [lines, setLines] = useState([{ ingredientName: '', quantity: '' }])
13
14 const load = () => api.get('/recipes/mine').then(setMine).catch(e => setError(e.message))
15
16 useEffect(() => {
17 load()
18 api.get('/catalog/ingredients').then(setCatalog).catch(() => {})
19 api.get('/catalog/restrictions').then(setRestrictions).catch(() => {})
20 }, [])
21
22 const setLine = (i, key) => (e) => {
23 const copy = [...lines]
24 copy[i] = { ...copy[i], [key]: e.target.value }
25 setLines(copy)
26 }
27
28 const create = async () => {
29 setError(null)
30 try {
31 const body = {
32 name: form.name,
33 guide: form.guide,
34 servings: Number(form.servings),
35 ingredients: lines
36 .filter(l => l.ingredientName && l.quantity)
37 .map(l => ({ ingredientName: l.ingredientName, quantity: Number(l.quantity) })),
38 restrictionIds: form.restrictionIds.map(Number)
39 }
40 const created = await api.post('/recipes', body)
41 setDetail(created)
42 setForm({ name: '', guide: '', servings: 4, restrictionIds: [] })
43 setLines([{ ingredientName: '', quantity: '' }])
44 load()
45 } catch (e) { setError(e.message) }
46 }
47
48 const share = async (id) => {
49 setError(null)
50 try {
51 await api.post('/posts', { recipeId: id, isPrivate: false, isFavourite: false, status: 'published' })
52 alert('Shared to the feed')
53 } catch (e) { setError(e.message) }
54 }
55
56 return (
57 <div>
58 {error && <div className="error">{error}</div>}
59
60 <div className="card">
61 <h3>New recipe</h3>
62 <p className="muted">
63 Energy is not entered: the server sums it from the ingredients and divides by servings.
64 </p>
65 <div className="stack" style={{ maxWidth: 560 }}>
66 <input placeholder="name" value={form.name}
67 onChange={e => setForm({ ...form, name: e.target.value })} />
68 <textarea placeholder="preparation steps" rows={3} value={form.guide}
69 onChange={e => setForm({ ...form, guide: e.target.value })} />
70 <input type="number" min="1" placeholder="servings" value={form.servings}
71 onChange={e => setForm({ ...form, servings: e.target.value })} />
72
73 {lines.map((l, i) => (
74 <div className="row" key={i}>
75 <select value={l.ingredientName} onChange={setLine(i, 'ingredientName')} style={{ flex: 1 }}>
76 <option value="">— ingredient —</option>
77 {catalog.map(c => <option key={c.name} value={c.name}>{c.name} ({c.kcal} kcal)</option>)}
78 </select>
79 <input type="number" placeholder="grams" style={{ width: 110 }}
80 value={l.quantity} onChange={setLine(i, 'quantity')} />
81 </div>
82 ))}
83 <button className="small" onClick={() => setLines([...lines, { ingredientName: '', quantity: '' }])}>
84 + another ingredient
85 </button>
86
87 <select multiple size={4} value={form.restrictionIds}
88 onChange={e => setForm({
89 ...form,
90 restrictionIds: Array.from(e.target.selectedOptions, o => o.value)
91 })}>
92 {restrictions.map(r => <option key={r.id} value={r.id}>{r.type} — {r.description}</option>)}
93 </select>
94
95 <button className="primary" onClick={create}>Create recipe</button>
96 </div>
97 </div>
98
99 {detail && (
100 <div className="card">
101 <h3>{detail.name}</h3>
102 <div className="muted">
103 total <span className="kcal">{detail.kcalSum} kcal</span> ·
104 {' '}{String(detail.servings)} servings ·
105 {' '}<span className="kcal">{detail.kcalPerServing} kcal / portion</span>
106 </div>
107 <table>
108 <thead><tr><th>ingredient</th><th>grams</th><th>kcal/100g</th><th>contributes</th></tr></thead>
109 <tbody>
110 {detail.ingredients.map(i => (
111 <tr key={i.ingredientName}>
112 <td>{i.ingredientName}</td><td>{i.quantity}</td>
113 <td>{i.kcalPer100g}</td><td>{i.kcalContributed}</td>
114 </tr>
115 ))}
116 </tbody>
117 </table>
118 {detail.restrictions.length > 0 &&
119 <p className="muted">tags: {detail.restrictions.join(', ')}</p>}
120 </div>
121 )}
122
123 <div className="card">
124 <h3>My recipes</h3>
125 <table>
126 <tbody>
127 {mine.map(r => (
128 <tr key={r.id}>
129 <td>{r.name}</td>
130 <td className="kcal">{r.kcalPerServing} kcal / portion</td>
131 <td style={{ width: 1 }}>
132 <button className="small" onClick={() => api.get(`/recipes/${r.id}`).then(setDetail)}>
133 view
134 </button>
135 </td>
136 <td style={{ width: 1 }}>
137 <button className="small" onClick={() => share(r.id)}>share</button>
138 </td>
139 </tr>
140 ))}
141 </tbody>
142 </table>
143 {mine.length === 0 && <p className="muted">No recipes yet.</p>}
144 </div>
145 </div>
146 )
147}
Note: See TracBrowser for help on using the repository browser.