./knowledge-base/frontend/src/components/BrowseLists.tsx
import {
Box,
Typography,
Grid,
Card,
CardActionArea,
Chip,
Button,
} from "@mui/material";
import { ArrowBack as ArrowBackIcon } from "@mui/icons-material";
import type { EraMaster, ProgramMaster, DocumentSummary } from "../types";
import {
DocumentGenreSelector,
DOCUMENT_GENRE_OPTIONS,
} from "./DocumentGenreSelector";
const UNSET_LABEL = "未設定";
function getCountByKey(map: Map<string, number>, key: string): number {
return map.get(key) || map.get(key.replace(/戰/g, "戦")) || 0;
}
export function BrowseEraList({
eraList,
eraCounts,
unsetFilterValue,
onSelectEra,
}: {
eraList: EraMaster[];
eraCounts: Map<string, number>;
unsetFilterValue: string;
onSelectEra: (era: string) => void;
}) {
const eraItems = [
...eraList.map((era) => ({
id: era.id,
label: era.label,
filterValue: era.label,
count: getCountByKey(eraCounts, era.label),
})),
{
id: "unset-era",
label: UNSET_LABEL,
filterValue: unsetFilterValue,
count: getCountByKey(eraCounts, unsetFilterValue),
},
];
return (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
時代を選んでください
</Typography>
<Grid container spacing={2}>
{eraItems.map((era) => (
<Grid item xs={12} sm={6} md={4} key={era.id}>
<Card
sx={{ cursor: "pointer" }}
onClick={() => onSelectEra(era.filterValue)}
>
<CardActionArea sx={{ p: 2 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 1,
}}
>
<Typography variant="body1" fontWeight="bold">
{era.label}
</Typography>
<Chip
label={`${era.count}冊`}
size="small"
color="primary"
variant="outlined"
/>
</Box>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</Box>
);
}
export function BrowseProgramList({
programList,
programCounts,
unsetFilterValue,
onSelectProgram,
}: {
programList: ProgramMaster[];
programCounts: Map<string, number>;
unsetFilterValue: string;
onSelectProgram: (program: string) => void;
}) {
const programItems = [
...programList.map((program) => ({
id: program.id,
name: program.name,
filterValue: program.name,
count: getCountByKey(programCounts, program.name),
})),
...Array.from(programCounts.entries())
.filter(
([name]) =>
name !== unsetFilterValue &&
!programList.some((program) => program.name === name),
)
.map(([name]) => ({
id: `program-extra-${name}`,
name,
filterValue: name,
count: getCountByKey(programCounts, name),
})),
{
id: "unset-program",
name: UNSET_LABEL,
filterValue: unsetFilterValue,
count: getCountByKey(programCounts, unsetFilterValue),
},
];
return (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
番組名を選んでください
</Typography>
{programItems.length === 0 ? (
<Typography color="text.secondary">番組が登録されていません</Typography>
) : (
<Grid container spacing={2}>
{programItems.map((program) => (
<Grid item xs={12} sm={6} md={4} key={program.id}>
<Card
sx={{ cursor: "pointer" }}
onClick={() => onSelectProgram(program.filterValue)}
>
<CardActionArea sx={{ p: 2 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
gap: 1,
}}
>
<Typography variant="body1" fontWeight="bold">
{program.name}
</Typography>
<Chip
label={`${program.count}冊`}
size="small"
color="primary"
variant="outlined"
/>
</Box>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
);
}
export function BrowseBookTitleList({
documentList,
browseBookGenre,
onSelectGenre,
onSelectBook,
skipGenreStep = false,
}: {
documentList: DocumentSummary[];
browseBookGenre: string | null;
onSelectGenre: (genre: string | null) => void;
onSelectBook: (title: string) => void;
skipGenreStep?: boolean;
}) {
// ジャンルで絞り込んだ書籍リスト(skipGenreStep=true のときは全件表示)
const filteredList =
skipGenreStep || !browseBookGenre
? documentList
: documentList.filter((doc) => doc.document_genre === browseBookGenre);
// skipGenreStep=true: ジャンル選択ステップをスキップして全書籍を直接表示
if (!skipGenreStep && !browseBookGenre) {
return (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
ジャンルを選んでください
</Typography>
<DocumentGenreSelector
value=""
onChange={(genre) => onSelectGenre(genre)}
/>
</Box>
);
}
// Step2 / skipGenreStep=true: 書籍一覧画面
const genreOption = browseBookGenre
? DOCUMENT_GENRE_OPTIONS.find((g) => g.value === browseBookGenre)
: null;
const heading = skipGenreStep
? "書籍タイトルの一覧"
: `${browseBookGenre}の一覧`;
return (
<Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 2 }}>
{!skipGenreStep && (
<Button
size="small"
startIcon={<ArrowBackIcon />}
onClick={() => onSelectGenre(null)}
variant="outlined"
>
ジャンル選択に戻る
</Button>
)}
{genreOption?.image && (
<Box
component="img"
src={genreOption.image}
alt={browseBookGenre ?? ""}
sx={{
width: 32,
height: 32,
objectFit: "cover",
borderRadius: 0.5,
}}
/>
)}
<Typography variant="h6">{heading}</Typography>
</Box>
{filteredList.length === 0 ? (
<Typography color="text.secondary">
該当する書籍が登録されていません
</Typography>
) : (
<Grid container spacing={2}>
{filteredList.map((doc) => (
<Grid item xs={12} sm={6} md={4} key={doc.document_id}>
<Card
sx={{ cursor: "pointer" }}
onClick={() => onSelectBook(doc.title || "(タイトル未設定)")}
>
<CardActionArea sx={{ p: 2 }}>
<Typography variant="body1" fontWeight="bold">
{doc.title || "(タイトル未設定)"}
</Typography>
{doc.document_genre && (
<Chip
label={doc.document_genre}
size="small"
variant="outlined"
sx={{ mt: 0.5 }}
/>
)}
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
);
}