Allow undated reconstructed events
This commit is contained in:
+26
-8
@@ -40,6 +40,7 @@ export function App() {
|
||||
const [editingFolderId, setEditingFolderId] = useState<string | null>(null)
|
||||
const [editingFileId, setEditingFileId] = useState<string | null>(null)
|
||||
const [editingEventId, setEditingEventId] = useState<string | null>(null)
|
||||
const [newEventDraft, setNewEventDraft] = useState<Evidence | null>(null)
|
||||
const [editingPartyId, setEditingPartyId] = useState<string | null>(null)
|
||||
const [newPartyDraft, setNewPartyDraft] = useState<Evidence | null>(null)
|
||||
const [briefOpen, setBriefOpen] = useState(false)
|
||||
@@ -143,9 +144,8 @@ export function App() {
|
||||
const { viewport } = caseState
|
||||
const position = nextOpenBoardPosition(caseState.evidence, { x: Math.max(100, (620 - viewport.x) / viewport.zoom), y: Math.max(100, (290 - viewport.y) / viewport.zoom) }, { width: 270 })
|
||||
const event: Evidence = { id: uid('event'), type: 'event', title: 'UNTITLED EVENT', content: 'Describe what happened.',
|
||||
eventDate: new Date().toISOString(), supportingEvidenceIds: [], ...position, width: 270 }
|
||||
update(state => ({ ...state, evidence: [...state.evidence, event] }))
|
||||
setSelected(event.id); setRecentlyCreatedExhibitId(event.id); setEditingEventId(event.id)
|
||||
supportingEvidenceIds: [], ...position, width: 270 }
|
||||
setNewEventDraft(event)
|
||||
}
|
||||
|
||||
const addParty = () => {
|
||||
@@ -315,7 +315,11 @@ export function App() {
|
||||
...caseState.documents.filter(document => !containedDocumentIds.has(document.id) && (document.publishedAt || document.date)).map(document => ({ id: `document:${document.id}`, sourceTemporalId: `document:${document.id}`, date: document.publishedAt || document.date, label: document.title, kind: 'document' as const, documentId: document.id })),
|
||||
...caseState.evidence.filter(widget => widget.type === 'event' && widget.eventDate).map(widget => ({ id: `widget:${widget.id}`, sourceTemporalId: `widget:${widget.id}`, date: widget.eventDate!, label: widget.content, kind: 'widget' as const, evidenceId: widget.id })),
|
||||
].sort((a, b) => dateValue(a.date) - dateValue(b.date))
|
||||
const storyEvents = caseState.evidence.filter(item => item.type === 'event' && item.eventDate).sort((a, b) => dateValue(a.eventDate!) - dateValue(b.eventDate!))
|
||||
const storyEvents = caseState.evidence.filter(item => item.type === 'event').sort((a, b) => {
|
||||
if (!a.eventDate) return b.eventDate ? 1 : 0
|
||||
if (!b.eventDate) return -1
|
||||
return dateValue(a.eventDate) - dateValue(b.eventDate)
|
||||
})
|
||||
return <main className="desktop">
|
||||
<header className="menubar">
|
||||
<div className="brand"><span className="brand-mark">GU</span><span>OSINT BOARD <em>/ {requestedEditMode && caseState.editingAllowed ? 'LEVEL EDITOR' : 'CASE TERMINAL'}</em></span></div>
|
||||
@@ -355,7 +359,7 @@ export function App() {
|
||||
onLocate={focusEvidence}
|
||||
onEditParty={setEditingPartyId}
|
||||
/>}
|
||||
{storyEvents.length > 0 && <aside className="story-strip"><small>RECONSTRUCTED STORY</small>{storyEvents.map((event, index) => <button key={event.id} className={selected === event.id ? 'selected' : ''} onClick={() => focusEvidence(event.id)}><time>{event.eventDate!.slice(0, 10)}</time><b>{index + 1}. {event.title}</b><span>{event.content}</span></button>)}</aside>}
|
||||
{storyEvents.length > 0 && <aside className="story-strip"><small>RECONSTRUCTED STORY</small>{storyEvents.map((event, index) => <button key={event.id} className={selected === event.id ? 'selected' : ''} onClick={() => focusEvidence(event.id)}><time>{event.eventDate?.slice(0, 10) || 'UNDATED'}</time><b>{index + 1}. {event.title}</b><span>{event.content}</span></button>)}</aside>}
|
||||
{!docsOpen && <button className="open-files" onClick={() => setDocsOpen(true)}><FolderOpen size={18}/> CASE MATERIALS <b>{caseState.documents.length}</b></button>}
|
||||
<div className="board-actions">
|
||||
<button className={boardTool === 'move' ? 'active' : ''} title="Move widgets" onClick={() => setBoardTool('move')}><MousePointer2 size={17}/> MOVE</button>
|
||||
@@ -388,6 +392,20 @@ export function App() {
|
||||
onClose={() => setEditingEventId(null)}
|
||||
onSave={event => { update(state => ({ ...state, evidence: state.evidence.map(item => item.id === event.id ? event : item) })); setEditingEventId(null); setStatus('EVENT NARRATIVE UPDATED') }}
|
||||
/>}
|
||||
{newEventDraft && <EventEditor
|
||||
key={newEventDraft.id}
|
||||
event={newEventDraft}
|
||||
evidence={caseState.evidence}
|
||||
documents={caseState.documents}
|
||||
onClose={() => setNewEventDraft(null)}
|
||||
onSave={event => {
|
||||
update(state => ({ ...state, evidence: [...state.evidence, event] }))
|
||||
setNewEventDraft(null)
|
||||
setSelected(event.id)
|
||||
setRecentlyCreatedExhibitId(event.id)
|
||||
setStatus(event.eventDate ? 'DATED EVENT ADDED' : 'UNDATED EVENT ADDED')
|
||||
}}
|
||||
/>}
|
||||
{editingPartyId && <PartyEditor
|
||||
key={editingPartyId}
|
||||
party={caseState.evidence.find(item => item.id === editingPartyId)!}
|
||||
@@ -840,7 +858,7 @@ function EventEditor({ event, evidence, documents, onClose, onSave }: { event: E
|
||||
const toggle = (id: string) => setSupports(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id])
|
||||
const submit = (submitEvent: React.FormEvent) => {
|
||||
submitEvent.preventDefault()
|
||||
const eventDate = occurredAt ? new Date(occurredAt).toISOString() : new Date().toISOString()
|
||||
const eventDate = occurredAt ? new Date(occurredAt).toISOString() : undefined
|
||||
onSave({ ...event, title: title.trim() || 'UNTITLED EVENT', content: narrative.trim() || 'Something happened.', eventDate, supportingEvidenceIds: supports })
|
||||
}
|
||||
return <div className="modal-shade"><form className="window folder-editor event-editor" onSubmit={submit}>
|
||||
@@ -849,13 +867,13 @@ function EventEditor({ event, evidence, documents, onClose, onSave }: { event: E
|
||||
<small>EVENT EXHIBIT · THIS HAPPENED</small>
|
||||
<label className="field"><span>TITLE</span><input aria-label="Event title" value={title} onChange={change => setTitle(change.target.value)}/></label>
|
||||
<label className="field"><span>NARRATIVE</span><textarea aria-label="Event narrative" rows={4} value={narrative} onChange={change => setNarrative(change.target.value)}/></label>
|
||||
<label className="field"><span><CalendarClock size={13}/> OCCURRED AT · LOCAL</span><input aria-label="Occurred at" type="datetime-local" required value={occurredAt} onChange={change => setOccurredAt(change.target.value)}/></label>
|
||||
<label className="field event-date-field"><span><CalendarClock size={13}/> OCCURRED AT · LOCAL · OPTIONAL</span><span><input aria-label="Occurred at" type="datetime-local" value={occurredAt} onChange={change => setOccurredAt(change.target.value)}/><button type="button" disabled={!occurredAt} onClick={() => setOccurredAt('')}>CLEAR DATE</button></span></label>
|
||||
<div className="folder-members-heading"><div><b>SUPPORTING EXHIBITS</b><small>{supports.length} CITED</small></div><span>NORMALIZED EVENT EVIDENCE</span></div>
|
||||
<div className="folder-members event-support-list">
|
||||
{candidates.length === 0 && <p>ADD SOURCE MATERIAL OR NOTES BEFORE CITING EVIDENCE.</p>}
|
||||
{candidates.map(candidate => { const included = supports.includes(candidate.id); return <div className={`folder-member ${included ? 'included' : ''}`} key={candidate.id}><label><input type="checkbox" checked={included} onChange={() => toggle(candidate.id)}/><Network size={16}/><span><b>{candidate.title}</b><small>{candidate.kind}</small></span></label><span className="folder-member-date">{included ? 'SUPPORTS EVENT' : 'NOT CITED'}</span></div> })}
|
||||
</div>
|
||||
<p className="folder-editor-note">An event is an investigator assertion. Its occurrence time is separate from the publication time of the documents supporting it.</p>
|
||||
<p className="folder-editor-note">An event is an investigator assertion. Leave occurrence time blank when it is unknown; creation time is never used for timeline placement.</p>
|
||||
<div className="folder-editor-actions"><button type="button" onClick={onClose}>CANCEL</button><button className="primary" type="submit">SAVE EVENT</button></div>
|
||||
</div>
|
||||
</form></div>
|
||||
|
||||
Reference in New Issue
Block a user