Skip to main content

Filter UI Components

The template/components/filters/ module provides a comprehensive filtering system for directory listings. It includes category filters, tag filters, location-based filters, search controls, sort controls, active filter chips, and pagination -- all managed through a centralized filter context with URL synchronization.

Architecture Overview

Source File Structure

filters/
index.ts # Barrel exports
types.ts # TypeScript interfaces
constants.ts # Configuration constants
context/
filter-context.tsx # FilterProvider and useFilters hook
location-distance-context.tsx # Distance data context
hooks/
use-filter-state.ts # Core filter state management
use-filter-url-sync.ts # URL synchronization
use-sticky-header.ts # Scroll-based sticky behavior
use-tag-visibility.ts # Tag show/hide management
components/
active-filters/active-filters.tsx
categories/
controls/
location/
pagination/
tags/
utils/
style-utils.ts
tag-utils.ts
text-utils.ts

Core Types

// Sort options
type SortOption = 'popularity' | 'name-asc' | 'name-desc' | 'date-desc' | 'date-asc';

// Selection identifiers
type CategoryId = string;
type TagId = string;

// Location filter state
interface NearMeCoordinates {
latitude: number;
longitude: number;
radius: number; // km
}

interface LocationFilterState {
nearMe?: NearMeCoordinates;
city?: string;
country?: string;
sortByDistance?: boolean;
}

// Full filter context
interface FilterContextType {
searchTerm: string;
setSearchTerm: (term: string) => void;
selectedTags: TagId[];
setSelectedTags: Dispatch<SetStateAction<TagId[]>>;
selectedCategories: CategoryId[];
setSelectedCategories: Dispatch<SetStateAction<CategoryId[]>>;
sortBy: SortOption;
setSortBy: Dispatch<SetStateAction<SortOption>>;
clearAllFilters: () => void;
toggleSelectedTag: (tagId: TagId) => void;
toggleSelectedCategory: (categoryId: string) => void;
locationFilter: LocationFilterState;
setNearMe: (coords: NearMeCoordinates | null) => void;
setLocationCity: (city: string | null) => void;
setLocationCountry: (country: string | null) => void;
clearLocationFilter: () => void;
isFiltersLoading: boolean;
// ... additional convenience methods
}

FilterProvider

The top-level context provider that supplies filter state to all child components. Wraps children in a <Suspense> boundary with a <ListingSkeleton> fallback.

Props

PropTypeDefaultDescription
childrenReactNoderequiredChild components
initialTagstring | nullundefinedPre-selected tag from route
initialCategorystring | nullundefinedPre-selected category from route
initialSortBystringundefinedInitial sort option

Usage

import { FilterProvider, useFilters } from '@/components/filters';

// In a page layout
function ListingPage({ initialTag }) {
return (
<FilterProvider initialTag={initialTag}>
<Categories total={100} categories={categories} />
<Tags tags={tags} />
<FilterControls sortBy="popularity" setSortBy={setSortBy} />
<ItemList />
</FilterProvider>
);
}

// In a child component
function ItemList() {
const { searchTerm, selectedTags, selectedCategories, sortBy } = useFilters();
// ... filter and render items
}

Categories Component

Renders a list of category filters with support for navigation mode (link-based) and filter mode (toggle-based).

Props -- CategoriesProps

PropTypeDescription
totalnumberTotal item count for "All" category
categoriesCategory[]Available categories to display

Props -- CategoriesListProps

PropTypeDefaultDescription
categoriesCategory[]requiredCategories array
mode'navigation' | 'filter''navigation'Interaction mode
selectedCategoriesstring[]undefinedCurrently selected category IDs
onCategoryToggle(id: string) => voidundefinedToggle callback (filter mode)

Tags Component

Renders a horizontal tag bar with visibility management (show more / show less).

Props -- TagsProps

PropTypeDefaultDescription
tagsTag[]requiredAvailable tags
basePathstringundefinedBase URL path for tag links
resetPathstringundefinedURL to reset tag filter
enableStickybooleantrueEnable sticky scroll behavior
maxVisibleTagsnumber8Maximum visible tags before collapse
totalnumberundefinedTotal items count
mode'navigation' | 'filter''navigation'Interaction mode
allItemsItemData[]undefinedAll items for count calculation

ActiveFilters

Displays currently active filter chips with individual remove buttons and a "Clear All" action. Shows chips for: search term, selected tags, selected categories, and non-default sort option.

Props -- ActiveFiltersProps

PropTypeDescription
searchTermstringCurrent search text
setSearchTerm(term: string) => voidSearch setter
selectedTagsTagId[]Active tag IDs
setSelectedTags(tags: TagId[]) => voidTags setter
selectedCategoriesstring[]Active category IDs
setSelectedCategories(cats: string[]) => voidCategories setter
sortBySortOptionCurrent sort option
setSortBy(sort: SortOption) => voidSort setter
availableTagsTag[]All available tags for name lookup
availableCategoriesCategory[]All available categories for name lookup
clearAllFilters() => voidClears all active filters

FilterControls

Combines sort controls into a compact control bar.

Props

PropTypeDescription
sortBySortOptionCurrent sort option
setSortBy(sort: SortOption) => voidSort change handler

LocationFilter

Provides location-based filtering with "Near Me" geolocation, radius adjustment, city, and country selection.

import { LocationFilter } from '@/components/filters';

// Automatically reads settings and filter state from context
<LocationFilter />

The component conditionally renders based on settings.enabled and settings.distanceFilterEnabled. Sub-components:

ComponentDescription
NearMeButtonUses browser geolocation API for proximity filtering
RadiusSliderAdjusts search radius (visible when Near Me is active)
CityFilterFilter by city name
CountryFilterFilter by country

LocationDistanceProvider

Provides per-item distance data to child components without prop drilling.

import { LocationDistanceProvider, useItemDistance } from '@/components/filters';

<LocationDistanceProvider distances={distanceMap}>
{children}
</LocationDistanceProvider>

// In a child component:
const distance = useItemDistance("item-slug");
// Returns: number | undefined

Constants

const FILTER_CONSTANTS = {
MAX_VISIBLE_TAGS: 8,
TEXT_TRUNCATE_LENGTH: 20,
SCROLL_THRESHOLD: 250,
STICKY_OFFSET: 4,
SCROLL_DURATION: 600,
TOOLTIP_DELAY: 300,
TRANSITION_DURATION: 300,
MOBILE_BREAKPOINT: 'md',
};

const SORT_OPTIONS = {
POPULARITY: 'popularity',
NAME_ASC: 'name-asc',
NAME_DESC: 'name-desc',
DATE_DESC: 'date-desc',
DATE_ASC: 'date-asc',
};