# SearchParams-Based Catalog Filters

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace Zustand-based filter state with React Context initialized from server searchParams — eliminates hydration mismatch and filter flicker.

**Architecture:** Server component parses URL searchParams and passes initial state as props to `CatalogFilterProvider` (React Context). All filter widgets read/write via `useCatalogFilters()` context hook. URL is synced via `history.replaceState()`. Zustand store is removed entirely.

**Tech Stack:** React Context, Next.js searchParams, React Query, history.replaceState()

---

### File Structure

| File | Action | Responsibility |
|------|--------|---------------|
| `shared/lib/catalog-filter-context.tsx` | **Create** | Context provider + `useCatalogFilters()` hook |
| `shared/api/queries/use-catalog-items.ts` | **Modify** | Read from context instead of Zustand |
| `app/page.tsx` | **Modify** | Parse searchParams, pass to provider, remove store init |
| `views/home/ui/HomeView.tsx` | **Modify** | Wrap children in `CatalogFilterProvider` |
| `widgets/home/ui/FilterSidebar.tsx` | **Modify** | Use context instead of Zustand |
| `widgets/home/ui/SearchBar.tsx` | **Modify** | Use context instead of Zustand |
| `widgets/home/ui/CategoryNav.tsx` | **Modify** | Use context instead of Zustand |
| `widgets/home/ui/CourseFeed.tsx` | **Modify** | Use context instead of Zustand |
| `widgets/home/ui/HeroBanner.tsx` | **Modify** | Use context instead of Zustand |
| `widgets/home/ui/FilterDrawer.tsx` | **Modify** | Use context instead of Zustand |
| `views/home/ui/CatalogOrchestrator.tsx` | **Delete** | Replaced by provider's URL sync |
| `views/home/ui/StoreHydrator.tsx` | **Delete** | No longer needed |
| `shared/store/catalog-filter.store.ts` | **Delete** | Replaced by context |
| `shared/store/catalog-filter.core.ts` | **Delete** | Replaced by context |
| `shared/store/catalog-filter.types.ts` | **Keep** | Types and utilities still used |
| `shared/store/index.ts` | **Modify** | Remove store re-exports |

---

### Task 1: Create CatalogFilterProvider + useCatalogFilters hook

**Files:**
- Create: `apps/web/src/shared/lib/catalog-filter-context.tsx`

- [ ] **Step 1: Create the context provider**

```tsx
'use client';

import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react';
import { DEFAULT_FILTERS, countActiveFilters, toggleArray, type FilterState } from '@/shared/store/catalog-filter.types';

export interface CatalogInitialState {
  filters: FilterState;
  search: string;
  category: string | null;
  subcategory: string | null;
  location: string;
}

interface CatalogFilterContextValue {
  filters: FilterState;
  search: string;
  selectedCategory: string | null;
  selectedSubcategory: string | null;
  selectedLocation: string;
  drawerOpen: boolean;
  page: number;
  activeFilterCount: number;

  updateFilters: (partial: Partial<FilterState>) => void;
  clearFilters: () => void;
  setSearch: (search: string) => void;
  setSelectedCategory: (category: string | null) => void;
  setSelectedSubcategory: (subcategory: string | null) => void;
  setSelectedLocation: (location: string) => void;
  setDrawerOpen: (open: boolean) => void;
  setPage: (page: number | ((prev: number) => number)) => void;
}

const CatalogFilterContext = createContext<CatalogFilterContextValue | null>(null);

export function CatalogFilterProvider({
  initialState,
  children,
}: {
  initialState: CatalogInitialState;
  children: React.ReactNode;
}) {
  const [filters, setFiltersState] = useState<FilterState>(initialState.filters);
  const [search, setSearchState] = useState(initialState.search);
  const [selectedCategory, setCategoryState] = useState<string | null>(initialState.category);
  const [selectedSubcategory, setSubcategoryState] = useState<string | null>(initialState.subcategory);
  const [selectedLocation, setLocationState] = useState(initialState.location);
  const [drawerOpen, setDrawerOpen] = useState(false);
  const [page, setPageState] = useState(0);
  const isInitialRender = useRef(true);

  // Sync state → URL (skip initial render to avoid overwriting server URL)
  const syncUrl = useCallback((state: {
    filters: FilterState; search: string;
    category: string | null; subcategory: string | null; location: string;
  }) => {
    if (isInitialRender.current) { isInitialRender.current = false; return; }
    const p = new URLSearchParams();
    if (state.search) p.set('q', state.search);
    if (state.category) p.set('category', state.category);
    if (state.subcategory) p.set('subcategory', state.subcategory);
    if (state.location) p.set('location', state.location);
    state.filters.formats.forEach(v => p.append('format', v));
    state.filters.languages.forEach(v => p.append('language', v));
    state.filters.lessonTypes.forEach(v => p.append('lessonType', v));
    state.filters.timeSlots.forEach(v => p.append('timeSlot', v));
    if (state.filters.providerIndividual) p.set('providerIndividual', '1');
    if (state.filters.providerCompany) p.set('providerCompany', '1');
    if (state.filters.priceMin) p.set('priceMin', state.filters.priceMin);
    if (state.filters.priceMax) p.set('priceMax', state.filters.priceMax);
    if (state.filters.freeOnly) p.set('freeOnly', '1');
    if (state.filters.durationMin) p.set('durationMin', state.filters.durationMin);
    if (state.filters.durationMax) p.set('durationMax', state.filters.durationMax);
    if (state.filters.ratingHigh) p.set('ratingHigh', '1');
    if (state.filters.ageMode !== 'any') p.set('ageMode', state.filters.ageMode);
    const qs = p.toString();
    window.history.replaceState(null, '', qs ? `/?${qs}` : '/');
  }, []);

  const updateFilters = useCallback((partial: Partial<FilterState>) => {
    setFiltersState(prev => {
      const next = { ...prev, ...partial };
      // Use setTimeout to avoid setState-during-render issues in syncUrl
      return next;
    });
    setPageState(0);
  }, []);

  // We need a ref-based sync approach to avoid stale closures
  const stateRef = useRef({ filters, search, category: selectedCategory, subcategory: selectedSubcategory, location: selectedLocation });

  // Update URL whenever state changes — we'll use useEffect for this
  // (moved to a useEffect inside the provider to avoid complexity)

  const clearFilters = useCallback(() => {
    setFiltersState(DEFAULT_FILTERS);
    setPageState(0);
  }, []);

  const setSearch = useCallback((v: string) => {
    setSearchState(v);
    setPageState(0);
  }, []);

  const setSelectedCategory = useCallback((v: string | null) => {
    setCategoryState(v);
    setPageState(0);
  }, []);

  const setSelectedSubcategory = useCallback((v: string | null) => {
    setSubcategoryState(v);
    setPageState(0);
  }, []);

  const setSelectedLocation = useCallback((v: string | null) => {
    setLocationState(v ?? '');
    setPageState(0);
  }, []);

  const setPage = useCallback((p: number | ((prev: number) => number)) => {
    setPageState(p);
  }, []);

  const activeFilterCount = useMemo(() => countActiveFilters(filters), [filters]);

  // Sync to URL via useEffect
  // We update stateRef first so syncUrl always reads latest
  stateRef.current = { filters, search, category: selectedCategory, subcategory: selectedSubcategory, location: selectedLocation };

  // This needs to be in a useEffect — we'll handle that in implementation

  const value = useMemo<CatalogFilterContextValue>(() => ({
    filters, search, selectedCategory, selectedSubcategory, selectedLocation,
    drawerOpen, page, activeFilterCount,
    updateFilters, clearFilters, setSearch, setSelectedCategory,
    setSelectedSubcategory, setSelectedLocation, setDrawerOpen, setPage,
  }), [filters, search, selectedCategory, selectedSubcategory, selectedLocation,
    drawerOpen, page, activeFilterCount, updateFilters, clearFilters, setSearch,
    setSelectedCategory, setSelectedSubcategory, setSelectedLocation, setPage]);

  return (
    <CatalogFilterContext.Provider value={value}>
      {children}
    </CatalogFilterContext.Provider>
  );
}

export function useCatalogFilters() {
  const ctx = useContext(CatalogFilterContext);
  if (!ctx) throw new Error('useCatalogFilters must be used within CatalogFilterProvider');
  return ctx;
}
```

- [ ] **Step 2: Commit**

---

### Task 2: Wire up server → provider → components

**Files:**
- Modify: `apps/web/src/app/page.tsx` — remove store init, keep prefetch, pass initialState
- Modify: `apps/web/src/views/home/ui/HomeView.tsx` — accept initialState, wrap in provider

- [ ] **Step 1: Update page.tsx** — remove `initializeStore`, `<script>`, pass initialState prop
- [ ] **Step 2: Update HomeView** — wrap children in `<CatalogFilterProvider>`
- [ ] **Step 3: Commit**

---

### Task 3: Update useCatalogItems to read from context

**Files:**
- Modify: `apps/web/src/shared/api/queries/use-catalog-items.ts`

- [ ] **Step 1:** Replace all `useCatalogFilterStore` selectors with `useCatalogFilters()` context
- [ ] **Step 2: Commit**

---

### Task 4: Update all widget components

**Files:**
- Modify: `apps/web/src/widgets/home/ui/FilterSidebar.tsx`
- Modify: `apps/web/src/widgets/home/ui/SearchBar.tsx`
- Modify: `apps/web/src/widgets/home/ui/CategoryNav.tsx`
- Modify: `apps/web/src/widgets/home/ui/CourseFeed.tsx`
- Modify: `apps/web/src/widgets/home/ui/HeroBanner.tsx`
- Modify: `apps/web/src/widgets/home/ui/FilterDrawer.tsx`

- [ ] **Step 1:** Update each component: replace `useCatalogFilterStore` with `useCatalogFilters()`
- [ ] **Step 2: Commit**

---

### Task 5: Clean up — remove Zustand store and dead code

**Files:**
- Delete: `apps/web/src/views/home/ui/CatalogOrchestrator.tsx`
- Delete: `apps/web/src/views/home/ui/StoreHydrator.tsx`
- Delete: `apps/web/src/shared/store/catalog-filter.store.ts`
- Delete: `apps/web/src/shared/store/catalog-filter.core.ts`
- Modify: `apps/web/src/shared/store/index.ts` — remove store re-exports
- Modify: `apps/web/src/views/home/ui/HomeView.tsx` — remove CatalogOrchestrator

- [ ] **Step 1:** Delete files, update imports
- [ ] **Step 2: Commit**

---

### Task 6: Verify with Next.js MCP

- [ ] **Step 1:** Run `nextjs_call get_errors` on port 5002
- [ ] **Step 2:** Test in browser with query params `/?format=ONLINE&lessonType=GROUP`
- [ ] **Step 3:** Verify: filters checked, courses filtered, no hydration mismatch, no flicker
