journal-quest / src /components /StatementScreen.tsx
MATSUMURA Taishi
feat: replace default number inputs with custom styled SpinBox component
489cb95
Raw
History Blame Contribute Delete
8.6 kB
import { motion } from 'framer-motion'
import { useStatement } from '../store/statementStore'
import { useNav } from '../store/navStore'
import type { StatementCell } from '../types'
import { EventCard } from './EventCard'
import { SpinBox } from './SpinBox'
export function StatementScreen() {
const { phase, problem, inputs, result, setInput, submit, retry, backToSelect } = useStatement()
const { setSection } = useNav()
if (!problem) return null
const isResult = phase === 'result'
return (
<div className="mx-auto max-w-4xl px-4 py-5">
<div className="mb-4 flex items-center justify-between">
<button
onClick={() => { backToSelect(); setSection('journal') }}
className="text-sm text-slate-400 hover:text-slate-200"
>
← 論点選択
</button>
{result && (
<span className={`rounded-full px-3 py-1 text-xs font-bold ${result.correct ? 'bg-emerald-500/20 text-emerald-300' : 'bg-rose-500/20 text-rose-300'}`}>
{result.score} / {result.total} 正解
</span>
)}
</div>
{/* 問題文・試算表資料 */}
<EventCard
problemId={problem.id}
eventText={problem.event_text}
hint={problem.hint}
documents={problem.documents}
sectionLabel="第3問 財務諸表"
topicLabel="貸借対照表の作成"
/>
{/* 貸借対照表 */}
<div className="mt-4 rounded-2xl border border-slate-700 bg-slate-900/60 overflow-x-auto">
<div className="p-3 text-center text-sm font-bold text-slate-200">貸 借 対 照 表</div>
<table className="w-full text-sm">
<thead>
<tr className="border-y border-slate-700">
<th className="px-3 py-1.5 text-left text-xs text-sky-300">資産の部</th>
<th className="w-40 px-3 py-1.5 text-right text-xs text-sky-300">金額</th>
<th className="w-2 bg-slate-700/50" />
<th className="px-3 py-1.5 text-left text-xs text-amber-300">負債・純資産の部</th>
<th className="w-40 px-3 py-1.5 text-right text-xs text-amber-300">金額</th>
</tr>
</thead>
<tbody>
{problem.bs_rows.map((row, ri) => {
const leftOk = result ? result.rowResults.find((r) => r.rowIndex === ri)?.leftOk ?? true : null
const rightOk = result ? result.rowResults.find((r) => r.rowIndex === ri)?.rightOk ?? true : null
return (
<tr key={ri} className="border-b border-slate-700/50">
{/* 左: ラベル */}
<td className={`px-3 py-2 ${leftOk === false ? 'bg-rose-500/5' : ''}`}>
{renderLabelCell(row.left.label, `bs_${ri}_left_label`, inputs, setInput, isResult)}
</td>
{/* 左: 金額 */}
<td className={`px-3 py-2 text-right ${leftOk === false ? 'bg-rose-500/5' : ''}`}>
{renderAmountCell(row.left.amount, `bs_${ri}_left_amount`, `bs_${ri}_left_amount2`, inputs, setInput, isResult)}
</td>
<td className="bg-slate-700/50" />
{/* 右: ラベル */}
<td className={`px-3 py-2 ${rightOk === false ? 'bg-rose-500/5' : ''}`}>
{renderLabelCell(row.right.label, `bs_${ri}_right_label`, inputs, setInput, isResult)}
</td>
{/* 右: 金額 */}
<td className={`px-3 py-2 text-right ${rightOk === false ? 'bg-rose-500/5' : ''}`}>
{renderAmountCell(row.right.amount, `bs_${ri}_right_amount`, `bs_${ri}_right_amount2`, inputs, setInput, isResult)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* 結果 */}
{isResult && result && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
className={`mt-4 rounded-2xl border p-5 ${result.correct ? 'border-emerald-400/50 bg-emerald-500/10' : 'border-rose-400/50 bg-rose-500/10'}`}
>
<div className="mb-2 flex items-center gap-2">
<span className="text-2xl">{result.correct ? '🎉' : '🤔'}</span>
<span className={`text-xl font-bold ${result.correct ? 'text-emerald-300' : 'text-rose-300'}`}>
{result.correct ? '正解!' : `${result.score} / ${result.total} 問正解`}
</span>
</div>
<div className="rounded-lg bg-slate-900/40 p-3 text-sm leading-relaxed text-slate-200">
<p className="mb-1 font-semibold text-cyan-300">解説</p>
{problem.explanation}
</div>
<div className="mt-4 flex gap-3">
<button onClick={retry} className="flex-1 rounded-xl bg-cyan-500 py-2 font-bold text-slate-900 hover:bg-cyan-400">
もう一度
</button>
<button onClick={() => { backToSelect(); setSection('journal') }} className="rounded-xl border border-slate-600 px-4 py-2 text-slate-300 hover:bg-slate-700">
論点選択
</button>
</div>
</motion.div>
)}
{!isResult && (
<button
onClick={submit}
className="mt-4 w-full rounded-2xl bg-emerald-500 py-4 text-lg font-black text-slate-900 shadow-lg transition hover:bg-emerald-400"
>
採点する ✓
</button>
)}
</div>
)
}
function renderLabelCell(
cell: StatementCell,
key: string,
inputs: Record<string, string | number>,
setInput: (k: string, v: string | number) => void,
locked: boolean,
): React.ReactNode {
if (cell.type === 'given_label') {
if (!cell.value) return <span />
return <span className="text-slate-200">{String(cell.value)}</span>
}
if (cell.type === 'blank_account') {
return (
<span className="flex items-center gap-1">
<input
type="text"
value={String(inputs[key] ?? '')}
disabled={locked}
onChange={(e) => setInput(key, e.target.value)}
placeholder="( )"
className="w-16 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60"
/>
{cell.suffix && <span className="text-slate-200">{cell.suffix}</span>}
</span>
)
}
return null
}
function renderAmountCell(
cell: StatementCell,
key: string,
key2: string,
inputs: Record<string, string | number>,
setInput: (k: string, v: string | number) => void,
locked: boolean,
): React.ReactNode {
if (cell.type === 'given_amount') {
if (cell.value === null || cell.value === undefined) return <span />
return <span className="tabular-nums text-slate-200">¥{Number(cell.value).toLocaleString()}</span>
}
if (cell.type === 'blank_amount') {
return (
<SpinBox
inputMode="numeric"
value={inputs[key] ?? ''}
disabled={locked}
onChange={(e) => setInput(key, Number(e.target.value))}
placeholder="0"
className="w-32 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60"
/>
)
}
if (cell.type === 'blank_contra') {
return (
<span className="flex items-center justify-end gap-1">
<span className="text-rose-300"></span>
<SpinBox
inputMode="numeric"
value={inputs[key] ?? ''}
disabled={locked}
onChange={(e) => setInput(key, Number(e.target.value))}
placeholder="0"
className="w-24 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60"
/>
{cell.answer2 !== undefined && (
<>
<span className="text-slate-500"></span>
<SpinBox
inputMode="numeric"
value={inputs[key2] ?? ''}
disabled={locked}
onChange={(e) => setInput(key2, Number(e.target.value))}
placeholder="純額"
className="w-24 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60"
/>
<span className="text-slate-500"></span>
</>
)}
</span>
)
}
return null
}