add Ko-fi bean
This commit is contained in:
102
frontend/src/components/journal/JournalList.tsx
Normal file
102
frontend/src/components/journal/JournalList.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user