'use client' import { useCallback, useEffect, useState } from 'react' import type { ComicIssue, ComicSeries } from '@/types' import ComicSeriesView from './ComicSeriesView' import ComicIssueView from './ComicIssueView' import FilterPanel from '@/components/FilterPanel' import TagSelector from '@/components/tags/TagSelector' interface Props { libraryId: string readOnly?: boolean } export default function ComicsView({ libraryId, readOnly }: Props) { const [items, setItems] = useState<(ComicIssue | ComicSeries)[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedSeries, setSelectedSeries] = useState(null) const [selectedIssue, setSelectedIssue] = useState(null) const [tagPanel, setTagPanel] = useState<{ itemKey: string; title: string } | null>(null) const [search, setSearch] = useState('') const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [seriesIssueMeta, setSeriesIssueMeta] = useState< Record >({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) const [showFilters, setShowFilters] = useState( () => typeof window !== 'undefined' && window.innerWidth >= 768 ) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { const next = new Set(prev) next.has(tagId) ? next.delete(tagId) : next.add(tagId) return next }) const fetchItems = useCallback(() => { fetch(`/api/comics?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then((data: (ComicIssue | ComicSeries)[]) => { setItems(data) setLoading(false) }) .catch(() => { setError('Failed to load comics') setLoading(false) }) }, [libraryId]) useEffect(() => { fetchItems() }, [fetchItems]) const fetchAssignments = useCallback(() => { fetch(`/api/tags/library-assignments?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then(setAssignments) .catch(() => {}) }, [libraryId]) useEffect(() => { fetchAssignments() }, [fetchAssignments]) const fetchSeriesIssueMeta = useCallback(() => { fetch(`/api/comics/series-issue-tags?libraryId=${encodeURIComponent(libraryId)}`) .then((r) => r.json()) .then(setSeriesIssueMeta) .catch(() => {}) }, [libraryId]) useEffect(() => { fetchSeriesIssueMeta() }, [fetchSeriesIssueMeta]) const onTagsChanged = useCallback(() => { setFilterRefreshKey((k) => k + 1) fetchAssignments() fetchSeriesIssueMeta() }, [fetchAssignments, fetchSeriesIssueMeta]) const filtered = items.filter((item) => { const isSeries = 'issueCount' in item if (isSeries) { const meta = seriesIssueMeta[item.item_key ?? ''] ?? { tagIds: [], issueTitles: [] } if (search) { const q = search.toLowerCase() const titleMatch = item.title.toLowerCase().includes(q) const issueMatch = meta.issueTitles.some((t) => t.toLowerCase().includes(q)) if (!titleMatch && !issueMatch) return false } if (selectedTagIds.size > 0) { const seriesTags = assignments[item.item_key ?? ''] ?? [] const allTags = [...new Set([...seriesTags, ...meta.tagIds])] if (![...selectedTagIds].every((id) => allTags.includes(id))) return false } return true } // Standalone issue if (search && !item.title.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0) { const tags = assignments[item.item_key ?? ''] ?? [] if (![...selectedTagIds].every((id) => tags.includes(id))) return false } return true }) const filtersActive = search !== '' || selectedTagIds.size > 0 return ( <>
{showFilters && (
)}
{loading ? ( ) : error ? (
{error}
) : items.length === 0 ? (

No comics found

Add .cbz files or folders of .cbz files to this library and scan.

) : (
{filtered.map((item) => 'issueCount' in item ? ( setSelectedSeries(item as ComicSeries)} onTagClick={(item as ComicSeries).item_key && !readOnly ? () => setTagPanel({ itemKey: (item as ComicSeries).item_key!, title: item.title }) : undefined} /> ) : ( setSelectedIssue(item as ComicIssue)} onTagClick={(item as ComicIssue).item_key && !readOnly ? () => setTagPanel({ itemKey: (item as ComicIssue).item_key!, title: item.title }) : undefined} /> ) )}
)}
{/* Tag panel modal */} {tagPanel && (
{ if (e.target === e.currentTarget) setTagPanel(null) }} >

Tags

{tagPanel.title}

)} {selectedSeries && ( setSelectedSeries(null)} onTagsChanged={onTagsChanged} readOnly={readOnly} /> )} {selectedIssue && ( setSelectedIssue(null)} onTagsChanged={onTagsChanged} readOnly={readOnly} /> )} ) } function SeriesCard({ series, onClick, onTagClick, readOnly, }: { series: ComicSeries onClick: () => void onTagClick?: () => void readOnly?: boolean }) { return (
{onTagClick && !readOnly && ( )}
) } function IssueCard({ issue, onClick, onTagClick, readOnly, }: { issue: ComicIssue onClick: () => void onTagClick?: () => void readOnly?: boolean }) { return (
{onTagClick && !readOnly && ( )}
) } function LoadingGrid() { return (
{Array.from({ length: 12 }, (_, i) => (
))}
) }