Component props.
Rendered filter chips, or null if no filters are active.
export function FilterChips({
filters,
onRemoveFilter,
onClearAll,
}: Readonly<FilterChipsProps>) {
// Check if confidence is at default range
const isDefaultConfidence =
filters.confidence.min === 0 && filters.confidence.max === 100;
// Check if year range is set
const hasYearRange =
filters.yearRange &&
(filters.yearRange.min !== null || filters.yearRange.max !== null);
// Count total active filters
const activeFilterCount =
(isDefaultConfidence ? 0 : 1) +
filters.formats.length +
filters.genres.length +
filters.publicationStatuses.length +
(hasYearRange ? 1 : 0) +
(filters.tags?.length || 0);
// Return early if no active filters
if (activeFilterCount === 0) {
return null;
}
// Limit displayed genres to prevent overflow
const displayedGenres = filters.genres.slice(0, 5);
const remainingGenresCount = filters.genres.length - displayedGenres.length;
// Limit displayed tags to prevent overflow
const displayedTags = filters.tags?.slice(0, 5) || [];
const remainingTagsCount = (filters.tags?.length || 0) - displayedTags.length;
return (
<div className="animate-in fade-in slide-in-from-top-2 rounded-lg border border-slate-200 bg-slate-50/50 p-3 backdrop-blur-sm dark:border-slate-700 dark:bg-slate-800/30">
<div className="flex flex-wrap items-center gap-2">
{/* Confidence Chip */}
{!isDefaultConfidence && (
<Badge
variant="secondary"
className="gap-1.5 rounded-full bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-300 dark:hover:bg-blue-900/60"
>
<span>
Confidence: {filters.confidence.min}%-{filters.confidence.max}%
</span>
<button
type="button"
onClick={() => onRemoveFilter("confidence")}
aria-label="Remove confidence filter"
className="rounded-full p-0.5 transition-colors hover:bg-blue-200 dark:hover:bg-blue-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{/* Year Range Chip */}
{hasYearRange && filters.yearRange && (
<Badge
variant="secondary"
className="gap-1.5 rounded-full bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-300 dark:hover:bg-amber-900/60"
>
<span>Year: {formatYearRangeLabel(filters.yearRange)}</span>
<button
type="button"
onClick={() => onRemoveFilter("year")}
aria-label="Remove year filter"
className="rounded-full p-0.5 transition-colors hover:bg-amber-200 dark:hover:bg-amber-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
)}
{/* Format Chips */}
{filters.formats.map((format) => (
<Badge
key={format}
variant="secondary"
className="gap-1.5 rounded-full bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/40 dark:text-green-300 dark:hover:bg-green-900/60"
>
<span>{formatLabel(format)}</span>
<button
type="button"
onClick={() => onRemoveFilter("format", format)}
aria-label={`Remove ${formatLabel(format)} format filter`}
className="rounded-full p-0.5 transition-colors hover:bg-green-200 dark:hover:bg-green-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{/* Genre Chips */}
{displayedGenres.map((genre) => (
<Badge
key={genre}
variant="secondary"
className="gap-1.5 rounded-full bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/40 dark:text-purple-300 dark:hover:bg-purple-900/60"
>
<span>{genre}</span>
<button
type="button"
onClick={() => onRemoveFilter("genre", genre)}
aria-label={`Remove ${genre} genre filter`}
className="rounded-full p-0.5 transition-colors hover:bg-purple-200 dark:hover:bg-purple-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{/* Remaining genres badge */}
{remainingGenresCount > 0 && (
<Badge
variant="secondary"
className="rounded-full bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300"
>
+{remainingGenresCount} more
</Badge>
)}
{/* Tag Chips */}
{displayedTags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="gap-1.5 rounded-full bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/40 dark:text-teal-300 dark:hover:bg-teal-900/60"
>
<span>{tag}</span>
<button
type="button"
onClick={() => onRemoveFilter("tag", tag)}
aria-label={`Remove ${tag} tag filter`}
className="rounded-full p-0.5 transition-colors hover:bg-teal-200 dark:hover:bg-teal-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{/* Remaining tags badge */}
{remainingTagsCount > 0 && (
<Badge
variant="secondary"
className="rounded-full bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300"
>
+{remainingTagsCount} more
</Badge>
)}
{/* Status Chips */}
{filters.publicationStatuses.map((status) => (
<Badge
key={status}
variant="secondary"
className="gap-1.5 rounded-full bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/40 dark:text-orange-300 dark:hover:bg-orange-900/60"
>
<span>{formatPublicationStatusLabel(status)}</span>
<button
type="button"
onClick={() => onRemoveFilter("status", status)}
aria-label={`Remove ${formatPublicationStatusLabel(status)} status filter`}
className="rounded-full p-0.5 transition-colors hover:bg-orange-200 dark:hover:bg-orange-800/60"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
{/* Clear All Button */}
{activeFilterCount >= 2 && (
<Button
variant="ghost"
size="sm"
onClick={onClearAll}
className="ml-auto h-7 text-xs"
>
Clear All Filters
</Button>
)}
</div>
</div>
);
}
Displays active filters as removable badge chips. Shows confidence range, formats, genres, and publication statuses with individual removal buttons.