// BVI Timeline — Form and Sidebar Components
// Exported to window for use in Timeline.html
const { useState, useEffect, useRef, useMemo } = React;
// ── EVENT FORM MODAL ──────────────────────────────────────────────────────────
function EventForm({ event, onSave, onClose }) {
const isNew = !event || !event.id;
const [cal, setCal] = useState(event?.cal || 'greg');
const [year, setYear] = useState(event?.year || '');
const [month, setMonth] = useState(event?.month || '');
const [day, setDay] = useState(event?.day || '');
const [precision,setPrecision]= useState(event?.precision|| 'day');
const [title, setTitle] = useState(event?.title || '');
const [desc, setDesc] = useState(event?.desc || '');
const [category, setCategory] = useState(event?.category || 'LEGAL');
const [power, setPower] = useState(event?.power || 'GBR');
const [source, setSource] = useState(event?.source || '');
const [citation, setCitation] = useState(event?.citation || '');
const [verified, setVerified] = useState(event?.verified || false);
const [notes, setNotes] = useState(event?.notes || '');
const computedJDN = useMemo(() => {
const y = parseInt(year), m = parseInt(month)||1, d = parseInt(day)||1;
if (!y || isNaN(y)) return null;
return cal === 'julian' ? TU.julianToJDN(y,m,d) : TU.gregorianToJDN(y,m,d);
}, [cal, year, month, day]);
const gregEquiv = computedJDN ? TU.jdnToGregorian(computedJDN) : null;
const julEquiv = computedJDN ? TU.jdnToJulian(computedJDN) : null;
function handleSave() {
if (!title.trim()) { alert('Title required'); return; }
if (!year || isNaN(parseInt(year))) { alert('Year required'); return; }
if (!computedJDN) { alert('Invalid date'); return; }
const saved = {
id: event?.id || ('u_' + Date.now()),
jdn: computedJDN, cal, year: parseInt(year),
month: parseInt(month)||1, day: parseInt(day)||1,
precision, title: title.trim(), desc, category, power,
source, citation, verified, notes,
seedData: false, addedAt: event?.addedAt || Date.now(),
};
onSave(saved);
}
const labelStyle = { display:'block', fontSize:11, fontWeight:600, letterSpacing:'0.06em', textTransform:'uppercase', color:'#7a6a55', marginBottom:4 };
const inputStyle = { width:'100%', padding:'6px 8px', border:'1px solid #d4c8b8', borderRadius:4, fontSize:13, background:'#fdf9f4', boxSizing:'border-box' };
const rowStyle = { display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 };
return (
e.target===e.currentTarget && onClose()}>
{isNew ? 'Add Event' : 'Edit Event'}
{/* DATE */}
Calendar:
{['greg','julian'].map(c => (
))}
{computedJDN && (
JDN: {computedJDN} |
Gregorian: {TU.formatDate(gregEquiv)}
{cal==='julian' && <> | Julian (O.S.): {TU.formatDate(julEquiv)}>}
{cal==='greg' && julEquiv && parseInt(year) <= 1752 && <> | Julian (O.S.): {TU.formatDate(julEquiv)}>}
)}
{/* TITLE & DESCRIPTION */}
setTitle(e.target.value)} placeholder="e.g. Carlisle Letters Patent"/>
{/* CATEGORY & POWER */}
{/* SOURCE */}
{/* NOTES */}
{/* VERIFIED */}
);
}
// ── EVENT DETAIL PANEL ────────────────────────────────────────────────────────
function EventDetail({ event, onEdit, onDelete, onClose }) {
if (!event) return null;
const g = TU.jdnToGregorian(event.jdn);
const j = TU.jdnToJulian(event.jdn);
const cat = EVENT_CATEGORIES[event.category] || {};
const pow = POWERS[event.power] || {};
const badge = (label, color) => (
{label}
);
return (
{badge(cat.label || event.category, cat.color || '#888')}
{badge(pow.label || event.power, pow.color || '#888')}
{event.seedData && !event.verified && badge('⚠ Needs verification','#b45309')}
{event.verified && badge('✓ Verified','#2e7d32')}
{event.title}
JDN {event.jdn} · {TU.formatDate(g, 'long')} (Gregorian)
{event.year <= 1752 && ` · O.S. ${TU.formatDate(j, 'long')}`}
{event.desc &&
{event.desc}
}
{event.source && (
Source: {event.source}
{event.citation && <>
Citation: {event.citation}>}
)}
{event.notes && (
Notes: {event.notes}
)}
);
}
// ── FILTER SIDEBAR ────────────────────────────────────────────────────────────
function FilterSidebar({ filters, onChange, eventCount, totalCount }) {
const tog = (set, key) => {
const next = new Set(set);
next.has(key) ? next.delete(key) : next.add(key);
return next;
};
const sectionTitle = txt => (
{txt}
);
const chip = (key, label, color, active, onClick) => (
{label}
);
return (
Showing {eventCount} of {totalCount} events
{/* Search */}
onChange({...filters, search: e.target.value})}
style={{width:'100%',boxSizing:'border-box',padding:'6px 8px',borderRadius:4,border:'1px solid #3a2e1e',background:'#140f08',color:'#e8dcc8',fontSize:12,outline:'none'}}
/>
{sectionTitle('Colonial Power')}
{Object.entries(POWERS).map(([k,v]) =>
chip(k, v.label, v.color, filters.powers.has(k), () => onChange({...filters, powers: tog(filters.powers, k)}))
)}
{sectionTitle('Category')}
{Object.entries(EVENT_CATEGORIES).map(([k,v]) =>
chip(k, v.label, v.color, filters.categories.has(k), () => onChange({...filters, categories: tog(filters.categories, k)}))
)}
{sectionTitle('Status')}
onChange({...filters, showSeed: !filters.showSeed})}
style={{display:'flex',alignItems:'center',gap:8,cursor:'pointer',fontSize:12,color: filters.showSeed ? '#f0e8d8':'#a09080',padding:'4px 0'}}>
Show seed events
onChange({...filters, showVerified: !filters.showVerified})}
style={{display:'flex',alignItems:'center',gap:8,cursor:'pointer',fontSize:12,color: filters.showVerified ? '#f0e8d8':'#a09080',padding:'4px 0'}}>
Verified only
⚠ = Seed / unverified
✓ = Source verified
O.S. = Old Style (Julian)
N.S. = New Style (Gregorian)
);
}
// ── PERSON DETAIL CARD ───────────────────────────────────────────────────
function PersonDetailCard({ person, roleTags, monarchs, viGovs, liGovs, events, onSelectEvent, onClose }) {
if (!person) return null;
const dispDate = (y,m,d,cal) => {
if (!y) return null;
const s = `${d?d+' ':''}${m?TU.MONTHS_SHORT[m-1]+' ':''}${y}`;
return cal==='julian' ? s+' (O.S.)' : s;
};
const born = dispDate(person.birthYear,person.birthMonth,person.birthDay,person.birthCal);
const died = dispDate(person.deathYear,person.deathMonth,person.deathDay,person.deathCal);
const linkedEvents = (events||[]).filter(e => (e.personIds||[]).includes(person.id)).sort((a,b)=>a.jdn-b.jdn);
const monarchRoles = (monarchs||[]).filter(m => m.personId===person.id);
const liRoles = (liGovs||[]).filter(g => g.personId===person.id);
const viRoles = (viGovs||[]).filter(g => g.personId===person.id);
return (
{onClose &&
}
{person.name}
{person.aliases &&
{person.aliases}
}
{born ? `b. ${born}` : 'b. ?'} — {died ? `d. ${died}` : 'd. ?'}
{person.seedData && !person.verified && ⚠ unverified}
{person.verified && ✓ verified}
{(person.roleTags||[]).map(tid => {
const t = roleTags.find(r=>r.id===tid);
if (!t) return null;
return {t.label};
})}
{person.biography &&
{person.biography}
}
{(monarchRoles.length+liRoles.length+viRoles.length) > 0 && (
Offices held
{monarchRoles.map((m,i)=>
· Monarch — {m.sYear||'?'} to {m.eYear||'?'}
)}
{liRoles.map((g,i)=>
· Leeward Islands {g.title||'Governor'} — {g.startYear||'?'} to {g.endYear||'?'}
)}
{viRoles.map((g,i)=>
· Virgin Islands {g.title||'Governor'} — {g.startYear||'?'} to {g.endYear||'?'}
)}
)}
{linkedEvents.length > 0 && (
Related events
{linkedEvents.map(e => (
onSelectEvent && onSelectEvent(e)} style={{fontSize:12,color:'#1a3a8b',padding:'3px 0',cursor:onSelectEvent?'pointer':'default',textDecoration:onSelectEvent?'underline':'none'}}>
→ {e.title} ({TU.jdnToDisplay(e.jdn,'year')})
))}
)}
{person.source && (
Source: {person.source}
{person.citation && <>
Citation: {person.citation}>}
)}
{person.notes &&
Notes: {person.notes}
}
);
}
// ── PEOPLE LIST ────────────────────────────────────────────────────────────
function PeopleList({ people, roleTags, onSelect, selected }) {
const [search, setSearch] = useState('');
const [tagFilter, setTagFilter] = useState(null);
const filtered = useMemo(()=>{
return people.filter(p => {
if (tagFilter && !(p.roleTags||[]).includes(tagFilter)) return false;
if (search) {
const q = search.toLowerCase();
if (!(p.name+(p.aliases||'')+(p.biography||'')+(p.notes||'')).toLowerCase().includes(q)) return false;
}
return true;
}).sort((a,b)=>(a.birthYear||9999)-(b.birthYear||9999));
},[people,search,tagFilter]);
return (
setSearch(e.target.value)} style={{padding:'5px 8px',fontSize:12,border:'1px solid #c4b49a',borderRadius:4,background:'#fdf9f4',width:200}}/>
{filtered.length} of {people.length}
{filtered.map(p => {
const isSel = selected?.id===p.id;
return (
onSelect(p)} style={{padding:'10px 14px',background:isSel?'#fff':'#fdf9f4',border:`1px solid ${isSel?'#8b1a1a':'#d4c8b8'}`,borderRadius:6,cursor:'pointer',transition:'all 0.1s'}}>
{p.name}
{p.birthYear||'?'} – {p.deathYear||'?'}
{(p.roleTags||[]).map(tid => {
const t = roleTags.find(r=>r.id===tid);
if (!t) return null;
return {t.label};
})}
);
})}
{filtered.length===0 &&
No people match.
}
);
}
Object.assign(window, { EventForm, EventDetail, FilterSidebar, PersonDetailCard, PeopleList });