import { useEffect, useRef, useState } from 'react' import api from '../api/client' import './ArticleSaveButton.css' /** * Keep this article. * * A library holds both articles and questions, because putting one of each * aside is the same act to whoever is doing it. The article goes in as itself * rather than standing in for the questions written against it — a topic with * no questions is still worth keeping, and a reader who saved the reading did * not ask for a session. * * Which libraries already hold it is asked of the server, as one question. It * was kept on the device for a while, because the API could not answer; that * was wrong on the second machine and silently so. */ //: What the panel is wide enough to want. Matches `width` in the stylesheet. const PANEL = 280 export default function ArticleSaveButton({ articleId }) { const [open, setOpen] = useState(false) // Which edge the panel hangs from. It hung from the right always, which is // right for a star at the right of a toolbar and wrong for this one, which // is the first thing in the article's bar: on a phone 200px of the panel sat // off the left of the screen, over the title, unreadable. const [side, setSide] = useState('right') const [libraries, setLibraries] = useState([]) const [query, setQuery] = useState('') const [busy, setBusy] = useState(false) const [saved, setSaved] = useState([]) const wrap = useRef(null) const anchor = useRef(null) // Asked once per article rather than per library: fetching each library's // contents to find out would also stamp every one of them as used and // reorder the reader's own list. useEffect(() => { if (!articleId) return undefined let live = true api.get(`/collections/for-article/${articleId}`) .then(res => { if (live) setSaved(res.data?.collection_ids || []) }) .catch(() => { if (live) setSaved([]) }) return () => { live = false } }, [articleId]) useEffect(() => { if (!open) return api.get('/collections/').then(res => setLibraries(Array.isArray(res.data) ? res.data : [])) .catch(() => setLibraries([])) }, [open]) // A popover that outlives the click that dismissed it is a popover you have // to close twice. useEffect(() => { if (!open) return undefined const away = (event) => { if (!wrap.current?.contains(event.target)) setOpen(false) } document.addEventListener('mousedown', away) return () => document.removeEventListener('mousedown', away) }, [open]) const saveInto = async (collectionId) => { setBusy(true) try { await api.put(`/collections/${collectionId}/articles/${articleId}`) setSaved(prev => (prev.includes(collectionId) ? prev : [...prev, collectionId])) setQuery('') } catch { /* nothing is marked, which is the honest signal */ } finally { setBusy(false) } } // Putting it back is the same control, so the star is a toggle per library // rather than a one-way door with the undo somewhere else. const removeFrom = async (collectionId) => { setBusy(true) try { await api.delete(`/collections/${collectionId}/articles/${articleId}`) setSaved(prev => prev.filter(id => id !== collectionId)) } catch { /* it stays marked, which is what the server still believes */ } finally { setBusy(false) } } const createAndSave = async (title) => { const name = title.trim() if (!name) return setBusy(true) try { const made = await api.post('/collections/', { title: name }) setLibraries(list => [...list, made.data]) setBusy(false) await saveInto(made.data.id) } catch { setBusy(false) } } const needle = query.trim().toLowerCase() const found = libraries.filter(row => row.title.toLowerCase().includes(needle)) // Offered only when nothing you already have is called this — two libraries // of the same name is a filing system nobody can use. const canCreate = !!needle && !libraries.some(row => row.title.trim().toLowerCase() === needle) const isSaved = saved.length > 0 return ( {open && (

Save this article

{/* One box for both. Searching what you have and naming what you do not are the same act — you type the name of the library you want and it either exists or it doesn't. */} setQuery(event.target.value)} onKeyDown={event => { if (event.key === 'Enter' && canCreate) { event.preventDefault(); createAndSave(query) } }} /> {canCreate && ( )} {found.length > 0 && ( )} {!found.length && !canCreate && (

{libraries.length ? 'No library by that name.' : 'Type a name to make your first library.'}

)}
)}
) }