feat: Added groupedContent useMemo and renderGroupedContent to unify pl…
- "src/frontend/src/pages/ChannelDetail.tsx" GSD-Task: S04/T02
This commit is contained in:
parent
79b2a3a566
commit
1711389d9c
1 changed files with 77 additions and 11 deletions
|
|
@ -177,7 +177,7 @@ export function ChannelDetail() {
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [showFullDescription, setShowFullDescription] = useState(false);
|
const [showFullDescription, setShowFullDescription] = useState(false);
|
||||||
const [scanInProgress, setScanInProgress] = useState(false);
|
const [scanInProgress, setScanInProgress] = useState(false);
|
||||||
const [expandedPlaylists, setExpandedPlaylists] = useState<Set<number | 'uncategorized'>>(new Set());
|
const [expandedGroups, setExpandedGroups] = useState<Set<string | number>>(new Set());
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
|
||||||
const [localCheckInterval, setLocalCheckInterval] = useState<number | ''>('');
|
const [localCheckInterval, setLocalCheckInterval] = useState<number | ''>('');
|
||||||
const [checkIntervalSaved, setCheckIntervalSaved] = useState(false);
|
const [checkIntervalSaved, setCheckIntervalSaved] = useState(false);
|
||||||
|
|
@ -341,13 +341,18 @@ export function ChannelDetail() {
|
||||||
try { localStorage.setItem('tubearr-group-by', value); } catch { /* ignore */ }
|
try { localStorage.setItem('tubearr-group-by', value); } catch { /* ignore */ }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Reset expanded groups when groupBy changes
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedGroups(new Set());
|
||||||
|
}, [groupBy]);
|
||||||
|
|
||||||
const handleSetViewMode = useCallback((mode: 'table' | 'card' | 'list') => {
|
const handleSetViewMode = useCallback((mode: 'table' | 'card' | 'list') => {
|
||||||
setViewMode(mode);
|
setViewMode(mode);
|
||||||
try { localStorage.setItem('tubearr-content-view', mode); } catch { /* ignore */ }
|
try { localStorage.setItem('tubearr-content-view', mode); } catch { /* ignore */ }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const togglePlaylist = useCallback((id: number | 'uncategorized') => {
|
const toggleGroup = useCallback((id: string | number) => {
|
||||||
setExpandedPlaylists((prev) => {
|
setExpandedGroups((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(id)) {
|
if (next.has(id)) {
|
||||||
next.delete(id);
|
next.delete(id);
|
||||||
|
|
@ -450,6 +455,68 @@ export function ChannelDetail() {
|
||||||
return groups.length > 0 ? groups : null;
|
return groups.length > 0 ? groups : null;
|
||||||
}, [channel, playlistData, content]);
|
}, [channel, playlistData, content]);
|
||||||
|
|
||||||
|
// ── Unified group-by logic ──
|
||||||
|
|
||||||
|
const groupedContent = useMemo<{ id: string | number; title: string; items: ContentItem[] }[] | null>(() => {
|
||||||
|
if (groupBy === 'none') return null;
|
||||||
|
|
||||||
|
if (groupBy === 'playlist') {
|
||||||
|
// Delegate to existing playlist grouping for YouTube; null for others
|
||||||
|
return playlistGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupBy === 'year') {
|
||||||
|
const yearMap = new Map<string, ContentItem[]>();
|
||||||
|
for (const item of content) {
|
||||||
|
const year = item.publishedAt ? new Date(item.publishedAt).getFullYear().toString() : 'Unknown';
|
||||||
|
const arr = yearMap.get(year);
|
||||||
|
if (arr) {
|
||||||
|
arr.push(item);
|
||||||
|
} else {
|
||||||
|
yearMap.set(year, [item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort groups by year descending, 'Unknown' last
|
||||||
|
const groups = Array.from(yearMap.entries())
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a[0] === 'Unknown') return 1;
|
||||||
|
if (b[0] === 'Unknown') return -1;
|
||||||
|
return Number(b[0]) - Number(a[0]);
|
||||||
|
})
|
||||||
|
.map(([year, items]) => ({ id: year, title: year, items }));
|
||||||
|
return groups.length > 0 ? groups : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupBy === 'type') {
|
||||||
|
const shorts: ContentItem[] = [];
|
||||||
|
const longform: ContentItem[] = [];
|
||||||
|
const livestream: ContentItem[] = [];
|
||||||
|
const audio: ContentItem[] = [];
|
||||||
|
|
||||||
|
for (const item of content) {
|
||||||
|
if (item.contentType === 'livestream') {
|
||||||
|
livestream.push(item);
|
||||||
|
} else if (item.contentType === 'audio') {
|
||||||
|
audio.push(item);
|
||||||
|
} else if (item.contentType === 'video' && item.duration != null && item.duration <= 60) {
|
||||||
|
shorts.push(item);
|
||||||
|
} else {
|
||||||
|
// video with duration > 60, null duration, or any other video
|
||||||
|
longform.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: { id: string; title: string; items: ContentItem[] }[] = [];
|
||||||
|
if (shorts.length > 0) groups.push({ id: 'short', title: 'Short', items: shorts });
|
||||||
|
if (longform.length > 0) groups.push({ id: 'longform', title: 'Longform', items: longform });
|
||||||
|
if (livestream.length > 0) groups.push({ id: 'livestream', title: 'Livestream', items: livestream });
|
||||||
|
if (audio.length > 0) groups.push({ id: 'audio', title: 'Audio', items: audio });
|
||||||
|
return groups.length > 0 ? groups : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [groupBy, content, playlistGroups]);
|
||||||
|
|
||||||
// ── Content table columns ──
|
// ── Content table columns ──
|
||||||
|
|
||||||
const contentColumns = useMemo<Column<ContentItem>[]>(
|
const contentColumns = useMemo<Column<ContentItem>[]>(
|
||||||
|
|
@ -742,15 +809,15 @@ export function ChannelDetail() {
|
||||||
[selectedIds, toggleSelect, toggleMonitored, downloadContent],
|
[selectedIds, toggleSelect, toggleMonitored, downloadContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPlaylistGroups = useCallback(
|
const renderGroupedContent = useCallback(
|
||||||
(groups: { id: number | 'uncategorized'; title: string; items: ContentItem[] }[]) => (
|
(groups: { id: string | number; title: string; items: ContentItem[] }[]) => (
|
||||||
<div>
|
<div>
|
||||||
{groups.map((group) => {
|
{groups.map((group) => {
|
||||||
const isExpanded = expandedPlaylists.has(group.id);
|
const isExpanded = expandedGroups.has(group.id);
|
||||||
return (
|
return (
|
||||||
<div key={group.id} style={{ borderBottom: '1px solid var(--border)' }}>
|
<div key={group.id} style={{ borderBottom: '1px solid var(--border)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => togglePlaylist(group.id)}
|
onClick={() => toggleGroup(group.id)}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -793,7 +860,7 @@ export function ChannelDetail() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[expandedPlaylists, togglePlaylist, renderTable, renderCardGrid, renderListView, viewMode],
|
[expandedGroups, toggleGroup, renderTable, renderCardGrid, renderListView, viewMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Loading / Error states ──
|
// ── Loading / Error states ──
|
||||||
|
|
@ -844,7 +911,6 @@ export function ChannelDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isYouTube = channel.platform === 'youtube';
|
const isYouTube = channel.platform === 'youtube';
|
||||||
const hasPlaylistGroups = isYouTube && playlistGroups !== null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -1498,8 +1564,8 @@ export function ChannelDetail() {
|
||||||
) : null}
|
) : null}
|
||||||
{contentLoading ? (
|
{contentLoading ? (
|
||||||
<SkeletonTable rows={8} columns={8} />
|
<SkeletonTable rows={8} columns={8} />
|
||||||
) : hasPlaylistGroups ? (
|
) : groupedContent ? (
|
||||||
renderPlaylistGroups(playlistGroups!)
|
renderGroupedContent(groupedContent)
|
||||||
) : viewMode === 'card' ? (
|
) : viewMode === 'card' ? (
|
||||||
renderCardGrid(content)
|
renderCardGrid(content)
|
||||||
) : viewMode === 'list' ? (
|
) : viewMode === 'list' ? (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue