103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { Link } from 'react-router-dom'
|
|
import type { JournalEntry } from '../../types/journal'
|
|
import type { BossResult, BossBattle } from '../../types/game'
|
|
|
|
interface JournalListProps {
|
|
entries: JournalEntry[]
|
|
runId: number
|
|
bossResults?: BossResult[]
|
|
bosses?: BossBattle[]
|
|
onNewEntry?: () => void
|
|
}
|
|
|
|
function formatDate(dateString: string): string {
|
|
return new Date(dateString).toLocaleDateString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
})
|
|
}
|
|
|
|
function getPreviewSnippet(body: string, maxLength = 120): string {
|
|
const stripped = body.replace(/[#*_`~[\]]/g, '').replace(/\n+/g, ' ').trim()
|
|
if (stripped.length <= maxLength) return stripped
|
|
return stripped.slice(0, maxLength).trim() + '...'
|
|
}
|
|
|
|
export function JournalList({
|
|
entries,
|
|
runId,
|
|
bossResults = [],
|
|
bosses = [],
|
|
onNewEntry,
|
|
}: JournalListProps) {
|
|
const bossResultMap = new Map(bossResults.map((br) => [br.id, br]))
|
|
const bossMap = new Map(bosses.map((b) => [b.id, b]))
|
|
|
|
const getBossName = (bossResultId: number | null): string | null => {
|
|
if (bossResultId == null) return null
|
|
const result = bossResultMap.get(bossResultId)
|
|
if (!result) return null
|
|
const boss = bossMap.get(result.bossBattleId)
|
|
return boss?.name ?? null
|
|
}
|
|
|
|
if (entries.length === 0) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<p className="text-text-secondary mb-4">No journal entries yet.</p>
|
|
{onNewEntry && (
|
|
<button
|
|
type="button"
|
|
onClick={onNewEntry}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
Write your first entry
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{onNewEntry && (
|
|
<div className="flex justify-end mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={onNewEntry}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
New Entry
|
|
</button>
|
|
</div>
|
|
)}
|
|
{entries.map((entry) => {
|
|
const bossName = getBossName(entry.bossResultId)
|
|
return (
|
|
<Link
|
|
key={entry.id}
|
|
to={`/runs/${runId}/journal/${entry.id}`}
|
|
className="block p-4 bg-surface-1 rounded-lg hover:bg-surface-2 transition-colors border border-border"
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="min-w-0 flex-1">
|
|
<h3 className="font-semibold text-text-primary truncate">{entry.title}</h3>
|
|
<p className="text-sm text-text-secondary mt-1">{getPreviewSnippet(entry.body)}</p>
|
|
</div>
|
|
<div className="flex flex-col items-end shrink-0 gap-1">
|
|
<time className="text-xs text-text-tertiary">{formatDate(entry.createdAt)}</time>
|
|
{bossName && (
|
|
<span className="text-xs px-2 py-0.5 bg-purple-900/40 text-purple-300 light:bg-purple-100 light:text-purple-800 rounded-full">
|
|
{bossName}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|