116 lines
3.4 KiB
JavaScript
116 lines
3.4 KiB
JavaScript
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
import { api } from '../api.js'
|
|
|
|
function newItem() {
|
|
const id = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `s-${Date.now()}-${Math.random()}`
|
|
return { id, text: '', done: false }
|
|
}
|
|
|
|
function rowsFrom(items) {
|
|
if (items?.length) {
|
|
return items.map((item) => ({
|
|
id: item.id || newItem().id,
|
|
text: item.text || '',
|
|
done: !!item.done
|
|
}))
|
|
}
|
|
return [newItem()]
|
|
}
|
|
|
|
export default function DayScratch({ dayId, token, items, onError }) {
|
|
const [local, setLocal] = useState(() => rowsFrom(items))
|
|
const timer = useRef(null)
|
|
const seq = useRef(0)
|
|
const pendingFocus = useRef(null)
|
|
const inputRefs = useRef({})
|
|
|
|
useEffect(() => {
|
|
window.clearTimeout(timer.current)
|
|
seq.current += 1
|
|
setLocal(rowsFrom(items))
|
|
}, [dayId])
|
|
|
|
useLayoutEffect(() => {
|
|
const id = pendingFocus.current
|
|
if (!id) return
|
|
const el = inputRefs.current[id]
|
|
if (!el) return
|
|
el.focus()
|
|
pendingFocus.current = null
|
|
}, [local])
|
|
|
|
const persist = (next, focusId) => {
|
|
if (focusId) pendingFocus.current = focusId
|
|
const mine = ++seq.current
|
|
setLocal(next)
|
|
window.clearTimeout(timer.current)
|
|
timer.current = window.setTimeout(async () => {
|
|
try {
|
|
await api(`/api/journal/days/${dayId}/scratch`, {
|
|
token,
|
|
method: 'PATCH',
|
|
body: { items: next.filter((item) => item.text.trim() || item.done) }
|
|
})
|
|
} catch (err) {
|
|
if (mine === seq.current) onError?.(err.message)
|
|
}
|
|
}, 400)
|
|
}
|
|
|
|
useEffect(() => () => window.clearTimeout(timer.current), [])
|
|
|
|
const patch = (id, fields) => persist(local.map((row) => (row.id === id ? { ...row, ...fields } : row)))
|
|
|
|
return (
|
|
<aside className="day-scratch">
|
|
<h2>Stichpunkte</h2>
|
|
<p className="muted">
|
|
Nur für dich, parallel zum Gespräch. Kanshō sieht diese Liste nicht — noch unklar, ob das später sinnvoll wäre.
|
|
</p>
|
|
<ul className="day-scratch-list">
|
|
{local.map((item, index) => (
|
|
<li key={item.id}>
|
|
<label className="check">
|
|
<input
|
|
type="checkbox"
|
|
checked={!!item.done}
|
|
onChange={(e) => patch(item.id, { done: e.target.checked })}
|
|
/>
|
|
<input
|
|
ref={(el) => {
|
|
if (el) inputRefs.current[item.id] = el
|
|
else delete inputRefs.current[item.id]
|
|
}}
|
|
type="text"
|
|
autoComplete="off"
|
|
value={item.text}
|
|
placeholder="Nicht vergessen …"
|
|
onChange={(e) => patch(item.id, { text: e.target.value })}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
const created = newItem()
|
|
const next = [...local]
|
|
next.splice(index + 1, 0, created)
|
|
persist(next, created.id)
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<button
|
|
type="button"
|
|
className="ghost"
|
|
onClick={() => {
|
|
const created = newItem()
|
|
persist([...local, created], created.id)
|
|
}}
|
|
>
|
|
Punkt hinzufügen
|
|
</button>
|
|
</aside>
|
|
)
|
|
}
|