| 1 | import { useEffect, useState } from 'react'
|
|---|
| 2 | import { api } from '../lib/api.js'
|
|---|
| 3 |
|
|---|
| 4 | /** UseCase1001 (create a recipe) and UseCase1004 (share it as a post). */
|
|---|
| 5 | export 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 | }
|
|---|