From f7c32485b9e9d3da42d4dd3665a7950b6a2134d7 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Wed, 19 Nov 2025 11:17:54 +0800 Subject: [PATCH] feat: implement SourceBadgeList and SourceBadgeItem components for improved video source filtering --- components/search/SourceBadgeItem.tsx | 73 +++++++++++++++ components/search/SourceBadgeList.tsx | 123 ++++++++++++++++++++++++++ components/search/SourceBadges.tsx | 46 ++-------- components/search/TypeBadgeList.tsx | 4 +- 4 files changed, 205 insertions(+), 41 deletions(-) create mode 100644 components/search/SourceBadgeItem.tsx create mode 100644 components/search/SourceBadgeList.tsx diff --git a/components/search/SourceBadgeItem.tsx b/components/search/SourceBadgeItem.tsx new file mode 100644 index 0000000..5125149 --- /dev/null +++ b/components/search/SourceBadgeItem.tsx @@ -0,0 +1,73 @@ +/** + * SourceBadgeItem - Individual source badge component + * Displays a single video source badge with count, supports selection state + */ + +interface SourceBadgeItemProps { + id: string; + name: string; + count: number; + isSelected: boolean; + onToggle: () => void; + isFocused?: boolean; + onFocus?: () => void; + innerRef?: (el: HTMLButtonElement | null) => void; +} + +export function SourceBadgeItem({ + id, + name, + count, + isSelected, + onToggle, + isFocused = false, + onFocus, + innerRef, +}: SourceBadgeItemProps) { + const handleClick = () => { + // Immediate visual feedback before state update + onToggle(); + }; + + return ( + + ); +} diff --git a/components/search/SourceBadgeList.tsx b/components/search/SourceBadgeList.tsx new file mode 100644 index 0000000..117bae8 --- /dev/null +++ b/components/search/SourceBadgeList.tsx @@ -0,0 +1,123 @@ +/** + * SourceBadgeList - Badge list container with responsive layout + * Desktop: Expandable grid with show more/less + * Mobile: Horizontal scroll with snap + */ + +'use client'; + +import { useState, useRef, useCallback } from 'react'; +import { Icons } from '@/components/ui/Icon'; +import { SourceBadgeItem } from './SourceBadgeItem'; +import { useKeyboardNavigation } from '@/lib/hooks/useKeyboardNavigation'; + +interface Source { + id: string; + name: string; + count: number; +} + +interface SourceBadgeListProps { + sources: Source[]; + selectedSources: Set; + onToggleSource: (sourceId: string) => void; +} + +export function SourceBadgeList({ sources, selectedSources, onToggleSource }: SourceBadgeListProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [focusedIndex, setFocusedIndex] = useState(-1); + const containerRef = useRef(null); + const badgeRefs = useRef<(HTMLButtonElement | null)[]>([]); + + // Keyboard navigation + useKeyboardNavigation({ + enabled: true, + containerRef: containerRef, + currentIndex: focusedIndex, + itemCount: sources.length, + orientation: 'horizontal', + onNavigate: useCallback((index: number) => { + setFocusedIndex(index); + badgeRefs.current[index]?.focus(); + // Scroll into view for mobile + badgeRefs.current[index]?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'center', + }); + }, []), + onSelect: useCallback((index: number) => { + onToggleSource(sources[index].id); + }, [sources, onToggleSource]), + }); + + return ( + <> + {/* Desktop: Expandable Grid */} +
+
+ {sources.map((source, index) => ( + onToggleSource(source.id)} + isFocused={focusedIndex === index} + onFocus={() => setFocusedIndex(index)} + innerRef={(el: HTMLButtonElement | null) => { badgeRefs.current[index] = el; }} + /> + ))} +
+ + {sources.length > 5 && ( + + )} +
+ + {/* Mobile & Tablet: Horizontal Scroll */} +
+
+ {sources.map((source, index) => ( + onToggleSource(source.id)} + isFocused={focusedIndex === index} + onFocus={() => setFocusedIndex(index)} + innerRef={(el: HTMLButtonElement | null) => { badgeRefs.current[index] = el; }} + /> + ))} +
+
+ + ); +} diff --git a/components/search/SourceBadges.tsx b/components/search/SourceBadges.tsx index d7497a7..8282db6 100644 --- a/components/search/SourceBadges.tsx +++ b/components/search/SourceBadges.tsx @@ -2,15 +2,15 @@ * SourceBadges - Clickable video source badges with filtering * Shows available video sources with counts * Click to filter videos by source - * Similar functionality to TypeBadges + * Responsive: Desktop shows expand/collapse, Mobile shows horizontal scroll */ 'use client'; import { memo } from 'react'; import { Card } from '@/components/ui/Card'; -import { Badge } from '@/components/ui/Badge'; import { Icons } from '@/components/ui/Icon'; +import { SourceBadgeList } from './SourceBadgeList'; interface Source { id: string; @@ -52,43 +52,11 @@ export const SourceBadges = memo(function SourceBadges({ -
- {sources.map((source) => { - const isSelected = selectedSources.has(source.id); - - return ( - - ); - })} -
+ {selectedSources.size > 0 && ( diff --git a/components/search/TypeBadgeList.tsx b/components/search/TypeBadgeList.tsx index 3ec86aa..ed176d3 100644 --- a/components/search/TypeBadgeList.tsx +++ b/components/search/TypeBadgeList.tsx @@ -93,13 +93,13 @@ export function TypeBadgeList({ badges, selectedTypes, onToggleType }: TypeBadge {/* Mobile & Tablet: Horizontal Scroll */}
{badges.map((badge, index) => (