Spaces:
Runtime error
Runtime error
| import { useState } from 'react' | |
| import { motion } from 'framer-motion' | |
| import { useGame } from '../store/gameStore' | |
| import { TOPIC_MASTER } from '../data/accounts' | |
| import type { ProblemDocument } from '../types' | |
| interface Props { | |
| // 第2問/第3問から直接 props で渡す場合(省略時は gameStore から取得) | |
| eventText?: string | |
| hint?: string | null | |
| documents?: ProblemDocument[] | |
| sectionLabel?: string | |
| topicLabel?: string | |
| problemId?: string | |
| } | |
| export function EventCard(props: Props) { | |
| // props がなければ gameStore から読む(第1問) | |
| const storeProblem = useGame((s) => s.problem) | |
| const storeTopic = useGame((s) => s.topic) | |
| const [showHint, setShowHint] = useState(false) | |
| const eventText = props.eventText ?? storeProblem?.event_text | |
| const hint = props.hint !== undefined ? props.hint : storeProblem?.hint | |
| const documents = props.documents ?? storeProblem?.documents | |
| const sectionLabel = props.sectionLabel ?? '第1問 仕訳' | |
| const topicLabel = props.topicLabel ?? (TOPIC_MASTER[storeTopic]?.label ?? storeTopic) | |
| const problemId = props.problemId ?? storeProblem?.id ?? 'card' | |
| if (!eventText) return null | |
| return ( | |
| <motion.div | |
| key={problemId} | |
| initial={{ opacity: 0, y: 12 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="rounded-2xl border border-cyan-500/30 bg-gradient-to-br from-slate-800 to-slate-900 p-5 shadow-xl" | |
| > | |
| <div className="mb-2 flex items-center gap-2 text-xs"> | |
| <span className="rounded-full bg-cyan-500/20 px-2 py-0.5 text-cyan-200">{topicLabel}</span> | |
| <span className="rounded-full bg-slate-600/50 px-2 py-0.5 text-slate-200">{sectionLabel}</span> | |
| </div> | |
| <p className="text-base font-bold leading-relaxed text-slate-50">{eventText}</p> | |
| {/* 証憑テーブル */} | |
| {documents && documents.length > 0 && ( | |
| <div className="mt-4 space-y-3"> | |
| {documents.map((doc, di) => ( | |
| <DocumentTable key={di} doc={doc} /> | |
| ))} | |
| </div> | |
| )} | |
| {hint && ( | |
| <div className="mt-3"> | |
| {showHint ? ( | |
| <p className="rounded-lg bg-amber-500/10 px-3 py-2 text-sm text-amber-200">💡 {hint}</p> | |
| ) : ( | |
| <button | |
| onClick={() => setShowHint(true)} | |
| className="text-sm text-amber-300/80 underline-offset-2 hover:underline" | |
| > | |
| ヒントを見る | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| </motion.div> | |
| ) | |
| } | |
| function DocumentTable({ doc }: { doc: ProblemDocument }) { | |
| const isSummaryRow = (row: string[]) => | |
| row[0] === '小計' || row[0] === '合計' || row[0].includes('消費税') | |
| return ( | |
| <div className="overflow-x-auto rounded-xl border border-slate-600"> | |
| {doc.title && ( | |
| <div className="border-b border-slate-600 bg-slate-700/50 px-3 py-1.5 text-xs font-semibold text-slate-300"> | |
| {doc.title} | |
| </div> | |
| )} | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="bg-slate-700/40"> | |
| {doc.headers.map((h, i) => ( | |
| <th key={i} className={`px-3 py-2 text-xs font-semibold text-slate-300 ${i === 0 ? 'text-left' : 'text-right'}`}> | |
| {h} | |
| </th> | |
| ))} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {doc.rows.map((row, ri) => ( | |
| <tr key={ri} className={isSummaryRow(row) ? 'border-t border-slate-600 bg-slate-700/30 font-semibold' : 'border-t border-slate-700/50'}> | |
| {row.map((cell, ci) => ( | |
| <td key={ci} className={`px-3 py-1.5 tabular-nums text-slate-200 ${ci === 0 ? 'text-left' : 'text-right'}`}> | |
| {cell} | |
| </td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| ) | |
| } | |