build: production build with Phase 1 2025 Bible Reader implementation complete
Includes all Phase 1 features: - Search-first navigation with auto-complete - Responsive reading interface (desktop/tablet/mobile) - 4 customization presets + full fine-tuning controls - Layered details panel with notes, bookmarks, highlights - Smart offline caching with IndexedDB and auto-sync - Full accessibility (WCAG 2.1 AA) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
910
CROSS_REFERENCES_PANEL_PLAN.md
Normal file
910
CROSS_REFERENCES_PANEL_PLAN.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# Cross-References Panel - Implementation Plan
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Implement a comprehensive cross-reference system that helps users discover related Scripture passages, understand context, trace themes, and build a deeper knowledge of interconnected Bible teachings.
|
||||
|
||||
**Status:** Planning Phase
|
||||
**Priority:** 🔴 High
|
||||
**Estimated Time:** 2 weeks (80 hours)
|
||||
**Target Completion:** TBD
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals & Objectives
|
||||
|
||||
### Primary Goals
|
||||
1. Display relevant cross-references for any verse
|
||||
2. Provide context and categorization for references
|
||||
3. Enable quick navigation between related passages
|
||||
4. Support custom user-added cross-references
|
||||
5. Visualize reference networks and themes
|
||||
|
||||
### User Value Proposition
|
||||
- **For Bible students**: Understand context and connections
|
||||
- **For teachers**: Prepare comprehensive lessons
|
||||
- **For scholars**: Research thematic progressions
|
||||
- **For new readers**: Discover related teachings
|
||||
- **For memorizers**: Build mental maps of Scripture
|
||||
|
||||
---
|
||||
|
||||
## ✨ Feature Specifications
|
||||
|
||||
### 1. Cross-Reference Data Model
|
||||
|
||||
```typescript
|
||||
interface CrossReference {
|
||||
id: string
|
||||
fromVerse: VerseReference
|
||||
toVerse: VerseReference
|
||||
type: ReferenceType
|
||||
category: string
|
||||
strength: number // 0-100, relevance score
|
||||
direction: 'forward' | 'backward' | 'bidirectional'
|
||||
source: 'openbible' | 'user' | 'treasury' | 'commentaries'
|
||||
description?: string
|
||||
addedBy?: string // User ID for custom references
|
||||
votes?: number // Community voting on quality
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
interface VerseReference {
|
||||
book: string
|
||||
chapter: number
|
||||
verse: number
|
||||
endVerse?: number // For ranges
|
||||
}
|
||||
|
||||
type ReferenceType =
|
||||
| 'quotation' // Direct quote (OT → NT)
|
||||
| 'allusion' // Indirect reference
|
||||
| 'parallel' // Parallel account (Gospels, Kings/Chronicles)
|
||||
| 'thematic' // Same theme/topic
|
||||
| 'fulfillment' // Prophecy fulfillment
|
||||
| 'contrast' // Contrasting teaching
|
||||
| 'expansion' // Elaboration/explanation
|
||||
| 'application' // Practical application
|
||||
| 'historical' // Historical context
|
||||
| 'wordStudy' // Same Hebrew/Greek word
|
||||
```
|
||||
|
||||
### 2. Cross-Reference Categories
|
||||
|
||||
```typescript
|
||||
const REFERENCE_CATEGORIES = {
|
||||
// Structural
|
||||
'parallel-passages': 'Parallel Passages',
|
||||
'quotations': 'Quotations',
|
||||
'allusions': 'Allusions',
|
||||
|
||||
// Thematic
|
||||
'salvation': 'Salvation',
|
||||
'faith': 'Faith',
|
||||
'love': 'Love',
|
||||
'judgment': 'Judgment',
|
||||
'prophecy': 'Prophecy',
|
||||
'miracles': 'Miracles',
|
||||
'parables': 'Parables',
|
||||
'promises': 'Promises',
|
||||
'commands': 'Commands',
|
||||
'covenants': 'Covenants',
|
||||
|
||||
// Character Studies
|
||||
'christ-prefigured': 'Christ Prefigured',
|
||||
'messianic': 'Messianic References',
|
||||
'holy-spirit': 'Holy Spirit',
|
||||
|
||||
// Literary
|
||||
'poetry': 'Poetic Parallels',
|
||||
'wisdom': 'Wisdom Literature',
|
||||
'apocalyptic': 'Apocalyptic Literature',
|
||||
|
||||
// Historical
|
||||
'chronological': 'Chronological Sequence',
|
||||
'geographical': 'Same Location',
|
||||
|
||||
// Custom
|
||||
'user-defined': 'User Added'
|
||||
}
|
||||
```
|
||||
|
||||
### 3. UI Layout Options
|
||||
|
||||
```
|
||||
Desktop - Sidebar (Default):
|
||||
┌────────────────────────────┬──────────────────┐
|
||||
│ Genesis 1:1-31 │ Cross-References │
|
||||
│ │ │
|
||||
│ 1 In the beginning God │ ▸ Quotations (3) │
|
||||
│ created the heaven and │ • John 1:1-3 │
|
||||
│ the earth. │ • Heb 11:3 │
|
||||
│ │ • Rev 4:11 │
|
||||
│ 2 And the earth was │ │
|
||||
│ without form... │ ▸ Parallel (2) │
|
||||
│ │ • Ps 33:6 │
|
||||
│ │ • Col 1:16 │
|
||||
│ │ │
|
||||
│ [verse 3 selected] │ ▸ Thematic (12) │
|
||||
│ 3 And God said, Let │ • Gen 2:3 │
|
||||
│ there be light: and │ • 2 Cor 4:6 │
|
||||
│ there was light. │ • Jas 1:17 │
|
||||
│ │ + 9 more │
|
||||
└────────────────────────────┴──────────────────┘
|
||||
|
||||
Mobile - Bottom Sheet:
|
||||
┌─────────────────────────┐
|
||||
│ Genesis 1:3 │
|
||||
│ │
|
||||
│ And God said, Let there │
|
||||
│ be light: and there was │
|
||||
│ light. │
|
||||
│ │
|
||||
│ [Tap for references] ▲ │
|
||||
└─────────────────────────┘
|
||||
↓ Swipe up
|
||||
┌─────────────────────────┐
|
||||
│ ≡ Cross-References (17) │
|
||||
├─────────────────────────┤
|
||||
│ Quotations (3) │
|
||||
│ • John 1:1-3 → │
|
||||
│ • Hebrews 11:3 → │
|
||||
│ │
|
||||
│ Thematic (12) │
|
||||
│ • Genesis 2:3 → │
|
||||
│ • 2 Cor 4:6 → │
|
||||
│ + 10 more │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### 4. Collapsible Sidebar Component
|
||||
|
||||
```typescript
|
||||
interface CrossReferencePanelProps {
|
||||
verse: VerseReference | null
|
||||
position: 'left' | 'right' | 'bottom'
|
||||
defaultOpen: boolean
|
||||
width: number // pixels or percentage
|
||||
}
|
||||
|
||||
export const CrossReferencePanel: React.FC<CrossReferencePanelProps> = ({
|
||||
verse,
|
||||
position = 'right',
|
||||
defaultOpen = true,
|
||||
width = 320
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||
const [references, setReferences] = useState<CrossReference[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [groupBy, setGroupBy] = useState<'type' | 'category'>('type')
|
||||
const [sortBy, setSortBy] = useState<'relevance' | 'book' | 'votes'>('relevance')
|
||||
|
||||
useEffect(() => {
|
||||
if (!verse) {
|
||||
setReferences([])
|
||||
return
|
||||
}
|
||||
|
||||
loadReferences(verse)
|
||||
}, [verse])
|
||||
|
||||
const loadReferences = async (verse: VerseReference) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/cross-references?book=${verse.book}&chapter=${verse.chapter}&verse=${verse.verse}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setReferences(data.references)
|
||||
} catch (error) {
|
||||
console.error('Failed to load cross-references:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const groupedReferences = useMemo(() => {
|
||||
if (groupBy === 'type') {
|
||||
return groupByType(references)
|
||||
} else {
|
||||
return groupByCategory(references)
|
||||
}
|
||||
}, [references, groupBy])
|
||||
|
||||
const sortedGroups = useMemo(() => {
|
||||
return Object.entries(groupedReferences).map(([key, refs]) => ({
|
||||
key,
|
||||
references: sortReferences(refs, sortBy)
|
||||
}))
|
||||
}, [groupedReferences, sortBy])
|
||||
|
||||
if (!verse) return null
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor={position}
|
||||
open={isOpen}
|
||||
variant="persistent"
|
||||
sx={{
|
||||
width: isOpen ? width : 0,
|
||||
flexShrink: 0,
|
||||
'& .MuiDrawer-paper': {
|
||||
width,
|
||||
boxSizing: 'border-box',
|
||||
top: 64, // Below header
|
||||
height: 'calc(100% - 64px)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">
|
||||
Cross-References
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={() => setIsOpen(false)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{verse.book} {verse.chapter}:{verse.verse}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Controls */}
|
||||
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Box display="flex" gap={1} mb={1}>
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>Group By</InputLabel>
|
||||
<Select
|
||||
value={groupBy}
|
||||
onChange={(e) => setGroupBy(e.target.value as any)}
|
||||
label="Group By"
|
||||
>
|
||||
<MenuItem value="type">Type</MenuItem>
|
||||
<MenuItem value="category">Category</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl size="small" fullWidth>
|
||||
<InputLabel>Sort By</InputLabel>
|
||||
<Select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
label="Sort By"
|
||||
>
|
||||
<MenuItem value="relevance">Relevance</MenuItem>
|
||||
<MenuItem value="book">Book Order</MenuItem>
|
||||
<MenuItem value="votes">Most Voted</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" gap={1}>
|
||||
<Button size="small" variant="outlined" fullWidth>
|
||||
<AddIcon /> Add Reference
|
||||
</Button>
|
||||
<IconButton size="small">
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* References List */}
|
||||
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
|
||||
{loading ? (
|
||||
<Box display="flex" justifyContent="center" p={3}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : references.length === 0 ? (
|
||||
<Alert severity="info">
|
||||
No cross-references found for this verse.
|
||||
</Alert>
|
||||
) : (
|
||||
sortedGroups.map(group => (
|
||||
<ReferenceGroup
|
||||
key={group.key}
|
||||
title={group.key}
|
||||
references={group.references}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Reference Group Component
|
||||
|
||||
```typescript
|
||||
interface ReferenceGroupProps {
|
||||
title: string
|
||||
references: CrossReference[]
|
||||
defaultExpanded?: boolean
|
||||
}
|
||||
|
||||
const ReferenceGroup: React.FC<ReferenceGroupProps> = ({
|
||||
title,
|
||||
references,
|
||||
defaultExpanded = true
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||
const [previewVerse, setPreviewVerse] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<Box mb={2}>
|
||||
<Box
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
p: 1,
|
||||
borderRadius: 1,
|
||||
'&:hover': { bgcolor: 'action.hover' }
|
||||
}}
|
||||
>
|
||||
<IconButton size="small">
|
||||
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
<Typography variant="subtitle2" fontWeight="600">
|
||||
{title} ({references.length})
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Collapse in={expanded}>
|
||||
<List dense>
|
||||
{references.map(ref => (
|
||||
<ReferenceItem
|
||||
key={ref.id}
|
||||
reference={ref}
|
||||
onHover={setPreviewVerse}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Collapse>
|
||||
|
||||
{/* Preview popover */}
|
||||
{previewVerse && (
|
||||
<VersePreviewPopover
|
||||
verseText={previewVerse}
|
||||
onClose={() => setPreviewVerse(null)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Reference Item with Preview
|
||||
|
||||
```typescript
|
||||
interface ReferenceItemProps {
|
||||
reference: CrossReference
|
||||
onHover: (verseText: string | null) => void
|
||||
}
|
||||
|
||||
const ReferenceItem: React.FC<ReferenceItemProps> = ({
|
||||
reference,
|
||||
onHover
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const [verseText, setVerseText] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleMouseEnter = async () => {
|
||||
if (verseText) {
|
||||
onHover(verseText)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/bible/verses?` +
|
||||
`book=${reference.toVerse.book}&` +
|
||||
`chapter=${reference.toVerse.chapter}&` +
|
||||
`verse=${reference.toVerse.verse}`
|
||||
)
|
||||
const data = await response.json()
|
||||
const text = data.verses[0]?.text || ''
|
||||
setVerseText(text)
|
||||
onHover(text)
|
||||
} catch (error) {
|
||||
console.error('Failed to load verse preview:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
const { book, chapter, verse } = reference.toVerse
|
||||
router.push(`/bible/${book.toLowerCase()}/${chapter}#verse-${verse}`)
|
||||
}
|
||||
|
||||
const formatReference = (ref: VerseReference): string => {
|
||||
const baseRef = `${ref.book} ${ref.chapter}:${ref.verse}`
|
||||
return ref.endVerse ? `${baseRef}-${ref.endVerse}` : baseRef
|
||||
}
|
||||
|
||||
const getTypeIcon = (type: ReferenceType) => {
|
||||
const icons = {
|
||||
quotation: <FormatQuoteIcon fontSize="small" />,
|
||||
parallel: <CompareArrowsIcon fontSize="small" />,
|
||||
thematic: <CategoryIcon fontSize="small" />,
|
||||
fulfillment: <CheckCircleIcon fontSize="small" />,
|
||||
allusion: <LinkIcon fontSize="small" />,
|
||||
// ... more mappings
|
||||
}
|
||||
return icons[type] || <ArticleIcon fontSize="small" />
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
button
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
mb: 0.5,
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
{getTypeIcon(reference.type)}
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2" fontWeight="500">
|
||||
{formatReference(reference.toVerse)}
|
||||
</Typography>
|
||||
{reference.strength >= 80 && (
|
||||
<Chip label="High" size="small" color="success" />
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={reference.description}
|
||||
/>
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton size="small" edge="end">
|
||||
<ArrowForwardIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Visual Indicators in Text
|
||||
|
||||
```typescript
|
||||
// Add superscript indicators in verse text
|
||||
const VerseWithReferences: React.FC<{
|
||||
verse: BibleVerse
|
||||
references: CrossReference[]
|
||||
}> = ({ verse, references }) => {
|
||||
const hasReferences = references.length > 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="verse"
|
||||
data-verse={verse.verseNum}
|
||||
sx={{ position: 'relative' }}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
className="verse-number"
|
||||
sx={{ mr: 1, fontWeight: 600, color: 'text.secondary' }}
|
||||
>
|
||||
{verse.verseNum}
|
||||
</Typography>
|
||||
|
||||
<Typography component="span" className="verse-text">
|
||||
{verse.text}
|
||||
</Typography>
|
||||
|
||||
{hasReferences && (
|
||||
<Tooltip title={`${references.length} cross-references`}>
|
||||
<IconButton
|
||||
size="small"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
width: 20,
|
||||
height: 20,
|
||||
fontSize: '0.75rem'
|
||||
}}
|
||||
>
|
||||
<Badge badgeContent={references.length} color="primary">
|
||||
<LinkIcon fontSize="inherit" />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Add Custom Cross-Reference
|
||||
|
||||
```typescript
|
||||
interface AddReferenceDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
fromVerse: VerseReference
|
||||
}
|
||||
|
||||
const AddReferenceDialog: React.FC<AddReferenceDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
fromVerse
|
||||
}) => {
|
||||
const [toVerse, setToVerse] = useState<VerseReference | null>(null)
|
||||
const [type, setType] = useState<ReferenceType>('thematic')
|
||||
const [category, setCategory] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!toVerse) return
|
||||
|
||||
try {
|
||||
await fetch('/api/cross-references', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fromVerse,
|
||||
toVerse,
|
||||
type,
|
||||
category,
|
||||
description,
|
||||
source: 'user'
|
||||
})
|
||||
})
|
||||
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error('Failed to add cross-reference:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Add Cross-Reference</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
From: {fromVerse.book} {fromVerse.chapter}:{fromVerse.verse}
|
||||
</Typography>
|
||||
|
||||
<VerseSelector
|
||||
label="To Verse"
|
||||
value={toVerse}
|
||||
onChange={setToVerse}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select value={type} onChange={(e) => setType(e.target.value as ReferenceType)}>
|
||||
<MenuItem value="quotation">Quotation</MenuItem>
|
||||
<MenuItem value="parallel">Parallel</MenuItem>
|
||||
<MenuItem value="thematic">Thematic</MenuItem>
|
||||
<MenuItem value="allusion">Allusion</MenuItem>
|
||||
<MenuItem value="fulfillment">Fulfillment</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Category"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Description (optional)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} variant="contained">
|
||||
Add Reference
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Bidirectional Linking
|
||||
|
||||
```typescript
|
||||
// Automatically create reverse references
|
||||
const createBidirectionalReference = async (
|
||||
fromVerse: VerseReference,
|
||||
toVerse: VerseReference,
|
||||
type: ReferenceType
|
||||
) => {
|
||||
// Create forward reference
|
||||
await createReference({
|
||||
fromVerse,
|
||||
toVerse,
|
||||
type,
|
||||
direction: 'forward'
|
||||
})
|
||||
|
||||
// Create backward reference automatically
|
||||
await createReference({
|
||||
fromVerse: toVerse,
|
||||
toVerse: fromVerse,
|
||||
type,
|
||||
direction: 'backward'
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Search Cross-References
|
||||
|
||||
```typescript
|
||||
interface ReferenceSearchProps {
|
||||
onSelect: (reference: CrossReference) => void
|
||||
}
|
||||
|
||||
const ReferenceSearch: React.FC<ReferenceSearchProps> = ({ onSelect }) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<CrossReference[]>([])
|
||||
|
||||
const handleSearch = useDebounce(async (searchQuery: string) => {
|
||||
if (searchQuery.length < 3) {
|
||||
setResults([])
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/cross-references/search?q=${encodeURIComponent(searchQuery)}`
|
||||
)
|
||||
const data = await response.json()
|
||||
setResults(data.references)
|
||||
}, 300)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TextField
|
||||
placeholder="Search references by verse, theme, or keyword..."
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
handleSearch(e.target.value)
|
||||
}}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
startAdornment: <SearchIcon />
|
||||
}}
|
||||
/>
|
||||
|
||||
<List>
|
||||
{results.map(ref => (
|
||||
<ListItem
|
||||
key={ref.id}
|
||||
button
|
||||
onClick={() => onSelect(ref)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={formatReference(ref.fromVerse)}
|
||||
secondary={ref.description}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Schema
|
||||
|
||||
```prisma
|
||||
model CrossReference {
|
||||
id String @id @default(cuid())
|
||||
|
||||
// From verse
|
||||
fromBook String
|
||||
fromChapter Int
|
||||
fromVerse Int
|
||||
fromEndVerse Int?
|
||||
|
||||
// To verse
|
||||
toBook String
|
||||
toChapter Int
|
||||
toVerse Int
|
||||
toEndVerse Int?
|
||||
|
||||
// Metadata
|
||||
type String // ReferenceType enum
|
||||
category String?
|
||||
strength Int @default(50) // 0-100
|
||||
direction String @default("bidirectional")
|
||||
source String @default("openbible") // openbible, user, treasury
|
||||
description String?
|
||||
|
||||
// User tracking (for custom references)
|
||||
addedBy String?
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
|
||||
// Community features
|
||||
votes Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([fromBook, fromChapter, fromVerse])
|
||||
@@index([toBook, toChapter, toVerse])
|
||||
@@index([type, category])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model ReferenceVote {
|
||||
id String @id @default(cuid())
|
||||
referenceId String
|
||||
userId String
|
||||
value Int // +1 or -1
|
||||
|
||||
reference CrossReference @relation(fields: [referenceId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([referenceId, userId])
|
||||
@@index([referenceId])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 API Endpoints
|
||||
|
||||
```typescript
|
||||
// Get cross-references for a verse
|
||||
GET /api/cross-references
|
||||
Query params:
|
||||
- book: string
|
||||
- chapter: number
|
||||
- verse: number
|
||||
- type?: ReferenceType[]
|
||||
- category?: string[]
|
||||
- minStrength?: number (0-100)
|
||||
Response: {
|
||||
references: CrossReference[]
|
||||
count: number
|
||||
}
|
||||
|
||||
// Add custom cross-reference
|
||||
POST /api/cross-references
|
||||
Body: {
|
||||
fromVerse: VerseReference
|
||||
toVerse: VerseReference
|
||||
type: ReferenceType
|
||||
category?: string
|
||||
description?: string
|
||||
}
|
||||
Response: {
|
||||
success: boolean
|
||||
reference: CrossReference
|
||||
}
|
||||
|
||||
// Vote on reference quality
|
||||
POST /api/cross-references/:id/vote
|
||||
Body: { value: 1 | -1 }
|
||||
|
||||
// Search cross-references
|
||||
GET /api/cross-references/search
|
||||
Query: q=keyword
|
||||
Response: { references: CrossReference[] }
|
||||
|
||||
// Bulk import cross-references (admin)
|
||||
POST /api/admin/cross-references/import
|
||||
Body: { references: CrossReference[], source: string }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 Implementation Timeline
|
||||
|
||||
### Week 1: Foundation & Data
|
||||
**Day 1-2: Database & Data Import**
|
||||
- [ ] Create database schema
|
||||
- [ ] Import OpenBible.info dataset (~65,000 references)
|
||||
- [ ] Build API endpoints
|
||||
- [ ] Test data queries
|
||||
|
||||
**Day 3-4: UI Components**
|
||||
- [ ] Create sidebar component
|
||||
- [ ] Build reference list UI
|
||||
- [ ] Implement grouping/sorting
|
||||
- [ ] Add loading states
|
||||
|
||||
**Day 5: Navigation & Preview**
|
||||
- [ ] Implement click navigation
|
||||
- [ ] Build hover preview
|
||||
- [ ] Add verse indicators
|
||||
- [ ] Test UX flow
|
||||
|
||||
**Deliverable:** Working cross-reference viewer
|
||||
|
||||
### Week 2: Advanced Features
|
||||
**Day 1-2: Custom References**
|
||||
- [ ] Build add reference dialog
|
||||
- [ ] Implement bidirectional linking
|
||||
- [ ] Add edit/delete functionality
|
||||
- [ ] Test CRUD operations
|
||||
|
||||
**Day 3-4: Search & Filter**
|
||||
- [ ] Implement search
|
||||
- [ ] Add advanced filters
|
||||
- [ ] Build category browser
|
||||
- [ ] Add sorting options
|
||||
|
||||
**Day 5: Polish & Mobile**
|
||||
- [ ] Optimize mobile layout
|
||||
- [ ] Performance tuning
|
||||
- [ ] Bug fixes
|
||||
- [ ] Documentation
|
||||
|
||||
**Deliverable:** Production-ready cross-reference system
|
||||
|
||||
---
|
||||
|
||||
## 📚 Data Sources
|
||||
|
||||
### OpenBible.info Cross-Reference Dataset
|
||||
- **URL**: https://openbible.info/labs/cross-references/
|
||||
- **Size**: ~340,000 cross-references
|
||||
- **License**: CC BY 4.0
|
||||
- **Coverage**: Old & New Testament
|
||||
- **Format**: CSV/JSON
|
||||
|
||||
### Treasury of Scripture Knowledge
|
||||
- **Coverage**: Extensive OT/NT references
|
||||
- **Public Domain**: Yes
|
||||
- **Quality**: High (curated by scholars)
|
||||
|
||||
### User-Generated References
|
||||
- Allow community contributions
|
||||
- Implement voting/quality system
|
||||
- Moderate for accuracy
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Plan
|
||||
|
||||
### Pre-Launch
|
||||
- [ ] Import cross-reference dataset
|
||||
- [ ] Test with 1000+ verses
|
||||
- [ ] Performance optimization
|
||||
- [ ] Mobile testing
|
||||
- [ ] Accessibility audit
|
||||
|
||||
### Rollout
|
||||
1. **Beta**: 10% users, collect feedback
|
||||
2. **Staged**: 50% users
|
||||
3. **Full**: 100% deployment
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2025-10-13
|
||||
**Owner:** Development Team
|
||||
**Status:** Ready for Implementation
|
||||
Reference in New Issue
Block a user