Build and Push Docker Image / build-and-push (push) Failing after 20s
App bar: brand, Studies / Read Bible / Reading Plans, and one account menu (theme, Settings, Admin, Log out). Study stage: notes get their own box above the resources in the right column. Settings, Admin and the login page use the reader themes; add a theme-aware danger colour. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
104 lines
6.4 KiB
React
104 lines
6.4 KiB
React
// Observe / Interpret / Apply as an accordion (one open at a time), plus a "More"
|
||
// area for background notes and tags so the page stays quiet.
|
||
import { useEffect, useState } from 'react';
|
||
|
||
const SECTIONS = [
|
||
{ field: 'observation', label: 'Observe', hint: 'What does the text say?',
|
||
placeholder: "What does the text actually say?\n• Who is speaking, and to whom?\n• What key words or phrases repeat?\n• What's the tone, structure, or literary style?" },
|
||
{ field: 'interpretation', label: 'Interpret', hint: 'What does it mean?',
|
||
placeholder: "What did this mean to its original audience?\n• What's the historical/cultural context?\n• How does it fit the surrounding argument?\n• What does it reveal about God's character?" },
|
||
{ field: 'application', label: 'Apply', hint: 'What do I do with it?',
|
||
placeholder: 'How should this shape your life today?\n• What attitude or action does this call for?\n• Is there a promise to trust or a warning to heed?\n• Who could you share this with?' },
|
||
];
|
||
|
||
const areaCls = 'w-full resize-y rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3.5 py-3 font-serif text-[15px] leading-7 text-[var(--r-text)] placeholder:font-sans placeholder:text-[13px] placeholder:leading-5 placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||
const words = (s) => (s ?? '').trim().split(/\s+/).filter(Boolean).length;
|
||
|
||
export default function NotesPane({ chunk, updateChunk, allTags }) {
|
||
const [open, setOpen] = useState(() => {
|
||
try { return localStorage.getItem('studyNotesOpen') || 'observation'; } catch { return 'observation'; }
|
||
});
|
||
const [more, setMore] = useState(false);
|
||
const [tagInput, setTagInput] = useState('');
|
||
useEffect(() => { setTagInput(''); }, [chunk.id]);
|
||
useEffect(() => { try { localStorage.setItem('studyNotesOpen', open || ''); } catch { /* storage unavailable */ } }, [open]);
|
||
|
||
const tags = chunk.tags ?? [];
|
||
const addTag = (tag) => {
|
||
const t = tag.trim();
|
||
if (!t || tags.some((x) => x.toLowerCase() === t.toLowerCase())) { setTagInput(''); return; }
|
||
updateChunk(chunk.id, { tags: [...tags, t] });
|
||
setTagInput('');
|
||
};
|
||
const suggestions = allTags.filter((t) => !tags.some((x) => x.toLowerCase() === t.toLowerCase()));
|
||
|
||
return (
|
||
<section aria-label="Study notes" className="space-y-2">
|
||
{SECTIONS.map(({ field, label, hint, placeholder }) => {
|
||
const isOpen = open === field;
|
||
const n = words(chunk[field]);
|
||
return (
|
||
<div key={field} className={`overflow-hidden rounded-2xl border ${isOpen ? 'border-[var(--r-accent)]' : 'border-[var(--r-line)]'}`}>
|
||
<button type="button" onClick={() => setOpen(isOpen ? null : field)} aria-expanded={isOpen}
|
||
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left">
|
||
<span className="flex items-center gap-2.5">
|
||
<span aria-hidden="true" className={`h-2.5 w-2.5 rounded-full border ${n ? 'border-[var(--r-accent)] bg-[var(--r-accent)]' : 'border-[var(--r-sub)]'}`} />
|
||
<span className="text-sm font-medium">{label}</span>
|
||
<span className="text-xs text-[var(--r-sub)]">{hint}</span>
|
||
</span>
|
||
<span className="text-xs text-[var(--r-sub)]">{n ? `${n} word${n === 1 ? '' : 's'}` : ''}</span>
|
||
</button>
|
||
{isOpen && (
|
||
<div className="px-4 pb-4">
|
||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={placeholder}
|
||
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
<div className="rounded-2xl border border-[var(--r-line)]">
|
||
<button type="button" onClick={() => setMore((m) => !m)} aria-expanded={more}
|
||
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm">
|
||
<span><span className="font-medium">More</span> <span className="text-xs text-[var(--r-sub)]">background{tags.length ? ` · ${tags.length} tag${tags.length === 1 ? '' : 's'}` : ' and tags'}</span></span>
|
||
<span className="text-xs text-[var(--r-sub)]">{more ? '▾' : '▸'}</span>
|
||
</button>
|
||
{more && (
|
||
<div className="space-y-4 px-4 pb-4">
|
||
<label className="block text-xs text-[var(--r-sub)]">
|
||
Background / general notes
|
||
<textarea value={chunk.generalNotes ?? ''} rows={5} onChange={(e) => updateChunk(chunk.id, { generalNotes: e.target.value })}
|
||
placeholder="e.g. author, date, audience, historical context, key themes…" className={`mt-1 ${areaCls}`} />
|
||
</label>
|
||
<div>
|
||
<p className="mb-1.5 text-xs text-[var(--r-sub)]">Tags</p>
|
||
<div className="flex flex-wrap items-center gap-1.5">
|
||
{tags.map((tag) => (
|
||
<span key={tag} className="inline-flex items-center gap-1 rounded-full bg-[var(--r-bar)] px-2.5 py-1 text-xs">
|
||
{tag}
|
||
<button type="button" aria-label={`Remove tag ${tag}`} onClick={() => updateChunk(chunk.id, { tags: tags.filter((t) => t !== tag) })} className="text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]">×</button>
|
||
</span>
|
||
))}
|
||
<input value={tagInput} onChange={(e) => setTagInput(e.target.value)} list="study-tag-suggestions"
|
||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTag(tagInput); } }}
|
||
placeholder="Add a tag and press Enter"
|
||
className="min-w-[10rem] flex-1 rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1 text-xs text-[var(--r-text)] focus:outline-none" />
|
||
</div>
|
||
{suggestions.length > 0 && (
|
||
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-xs">
|
||
<span className="text-[var(--r-sub)]">Suggestions:</span>
|
||
{suggestions.slice(0, 12).map((t) => (
|
||
<button key={t} type="button" onClick={() => addTag(t)} className="rounded-full border border-[var(--r-line)] px-2.5 py-0.5 text-[var(--r-sub)] hover:bg-black/5">+ {t}</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
<datalist id="study-tag-suggestions">{allTags.map((t) => <option key={t} value={t} />)}</datalist>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|