-
Notifications
You must be signed in to change notification settings - Fork 18
feat(skills): add filters skill #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marekgieldatsh
wants to merge
6
commits into
main
Choose a base branch
from
feat/add-filters-skill
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
649eb01
feat(skills): add filters skill
marekgieldatsh 421c950
feat(skills): add filters skill - resolve conflicts
marekgieldatsh 881f900
feat(skills): add filters skill - apply CR changes, part 1
marekgieldatsh af182de
feat(skills): add filters skill - apply CR changes, part 2
marekgieldatsh e3675b8
feat(skills): add filters skill - apply CR changes, part 3
marekgieldatsh 974a0cb
feat(skills): add filters skill - apply CR changes, part 4
marekgieldatsh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
111 changes: 111 additions & 0 deletions
111
.github/skills/tsh-implementing-filters/references/nextjs-patterns.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| # Next.js Filter Patterns | ||
|
|
||
| Next.js App Router-specific patterns for the tsh-implementing-filters skill. Load this reference when the project uses Next.js. | ||
|
|
||
| ## Table of Contents | ||
|
|
||
| - [Reading Search Params](#reading-search-params) | ||
| - [Navigation (Push/Replace)](#navigation-pushreplace) | ||
| - [Server Component Constraints](#server-component-constraints) | ||
| - [Bracket Notation with Next.js](#bracket-notation-with-nextjs) | ||
| - [Anti-Patterns](#anti-patterns) | ||
|
|
||
| ## Reading Search Params | ||
|
|
||
| Next.js App Router provides different APIs for reading URL search params depending on the context: | ||
|
|
||
| | Context | API | Notes | | ||
| | ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | ||
| | Client Component | `useSearchParams()` from `next/navigation` | Returns `ReadonlyURLSearchParams`. Only available in Client Components. | | ||
| | Server Component | `searchParams` prop from page/layout (async in Next.js 15+ — must be `await`ed) | Passed as a prop to `page.tsx`. Access `searchParams.filter` etc. | | ||
| | Route Handler | `request.nextUrl.searchParams` | In `route.ts` handlers. | | ||
|
|
||
| Key difference from React Router: `useSearchParams()` returns a **read-only** object. You cannot call `setSearchParams()` — you must construct a new URL and navigate. | ||
|
|
||
| ## Navigation (Push/Replace) | ||
|
|
||
| How to navigate with updated filters in Next.js App Router: | ||
|
|
||
| ```typescript | ||
| import { useRouter, usePathname, useSearchParams } from "next/navigation"; | ||
|
|
||
| const useFilterNavigation = () => { | ||
| const router = useRouter(); | ||
| const pathname = usePathname(); | ||
| const searchParams = useSearchParams(); | ||
|
|
||
| const navigate = ( | ||
| newParams: URLSearchParams, | ||
| options?: { replace?: boolean }, | ||
| ) => { | ||
| const url = `${pathname}?${newParams.toString()}`; | ||
| if (options?.replace) { | ||
| router.replace(url); | ||
| } else { | ||
| router.push(url); | ||
| } | ||
| }; | ||
|
|
||
| return { navigate, searchParams, pathname }; | ||
| }; | ||
| ``` | ||
|
|
||
| `router.push()` and `router.replace()` use **soft navigation** by default — no full page reload, which is the desired behavior for filter changes. | ||
|
|
||
| ## Server Component Constraints | ||
|
|
||
| - Server Components **cannot** use `useSearchParams()` — they receive `searchParams` as a page prop. | ||
| - Deserialization logic (bracket notation parsing, type coercion) works identically in both contexts — only the source of raw params differs. | ||
| - Pattern: Define serialization/deserialization as plain functions (not hooks). The `useFilters` hook wraps them for Client Components. Server Components call the deserialization function directly. | ||
|
|
||
| ```typescript | ||
| // Shared — works in both contexts | ||
| const deserializeFilters = (params: URLSearchParams): ProductFilters => { | ||
| /* ... */ | ||
| }; | ||
|
|
||
| // Client Component — via hook | ||
| const useFilters = (defaults: ProductFilters) => { | ||
| const searchParams = useSearchParams(); | ||
| const filters = deserializeFilters( | ||
| new URLSearchParams(searchParams.toString()), | ||
| ); | ||
| // ... | ||
| }; | ||
|
|
||
| // Server Component — direct call | ||
| export default async function ProductsPage({ | ||
| searchParams, | ||
| }: { | ||
| searchParams: Promise<Record<string, string | string[]>>; | ||
| }) { | ||
| const resolvedParams = await searchParams; | ||
| const params = new URLSearchParams(); | ||
| // Convert resolvedParams record to URLSearchParams... | ||
| const filters = deserializeFilters(params); | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Bracket Notation with Next.js | ||
|
|
||
| Next.js does not natively parse bracket notation (`filter[color]=blue`) into structured objects. The `searchParams` prop gives flat key-value pairs where the key is the literal string `filter[color]`. | ||
|
|
||
| The deserialization function must parse bracket keys manually. This logic is framework-agnostic — identical to the React Router approach. Only the source of `URLSearchParams` differs. | ||
|
|
||
| ```typescript | ||
| // searchParams in Next.js page: { "filter[color]": "blue", "filter[tags]": ["a", "b"] } | ||
| // Parse bracket keys to extract field names | ||
| ``` | ||
|
|
||
| ## Anti-Patterns | ||
|
|
||
| | Anti-Pattern | Instead Do | | ||
| | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | ||
| | Using `useSearchParams()` in Server Components | Use the `searchParams` page prop in Server Components | | ||
| | Calling `setSearchParams()` like React Router | Construct new URL string, use `router.push()` / `router.replace()` | | ||
| | Wrapping entire page in Client Component for filter access | Keep filter display in Server Components via `searchParams` prop; only filter controls need Client Component | | ||
| | Using `router.push()` with full URL including origin | Use pathname-relative URLs: `` `${pathname}?${params}` `` | | ||
| | Using nested objects for range filters (e.g., `{ price: { min, max } }`) | Use flat properties (`priceMin`, `priceMax`) so partial updates with shallow spread merge correctly | | ||
|
|
||
| > **Note:** When composing filter update functions, use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates with shallow spread merge correctly. |
147 changes: 147 additions & 0 deletions
147
.github/skills/tsh-implementing-filters/references/react-patterns.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| # React Filter Patterns | ||
|
|
||
| React Router v6+-specific patterns for the `tsh-implementing-filters` skill. Load this reference when the project uses React Router. | ||
|
|
||
| ## Table of Contents | ||
|
|
||
| - [Reading Search Params](#reading-search-params) | ||
| - [Navigation (Push/Replace)](#navigation-pushreplace) | ||
| - [Hook Binding](#hook-binding) | ||
| - [Bracket Notation Parsing](#bracket-notation-parsing) | ||
| - [Anti-Patterns](#anti-patterns) | ||
|
|
||
| ## Reading Search Params | ||
|
|
||
| | API | Usage | Notes | | ||
| | -------------------------- | ----------------------------------------- | ---------------------------------------------------------------------- | | ||
| | `useSearchParams()` | Returns `[searchParams, setSearchParams]` | Read-write tuple. `searchParams` is a `URLSearchParams` instance. | | ||
| | `searchParams.get(key)` | Single value | Returns `string \| null` | | ||
| | `searchParams.getAll(key)` | Multi-value (arrays) | Returns `string[]` — use for repeated bracket keys like `filter[tags]` | | ||
| | `searchParams.toString()` | Full query string | For URL construction | | ||
|
|
||
| Key difference from Next.js: React Router's `useSearchParams()` returns a **read-write** tuple — you can use `setSearchParams()` directly to update the URL without constructing a full URL string. | ||
|
|
||
| ## Navigation (Push/Replace) | ||
|
|
||
| **Approach 1: `setSearchParams()` (preferred for filter updates)** | ||
|
|
||
| ```typescript | ||
| import { useSearchParams } from "react-router-dom"; | ||
|
|
||
| const useFilterNavigation = () => { | ||
| const [searchParams, setSearchParams] = useSearchParams(); | ||
|
|
||
| const navigate = ( | ||
| newParams: URLSearchParams, | ||
| options?: { replace?: boolean }, | ||
| ) => { | ||
| setSearchParams(newParams, { replace: options?.replace }); | ||
| }; | ||
|
|
||
| return { navigate, searchParams }; | ||
| }; | ||
| ``` | ||
|
|
||
| `setSearchParams` also supports a functional updater for deriving from the previous params: | ||
|
|
||
| ```typescript | ||
| setSearchParams( | ||
| (prev) => { | ||
| const next = new URLSearchParams(prev); | ||
| next.set("filter[color]", "blue"); | ||
| return next; | ||
| }, | ||
| { replace: true }, | ||
| ); | ||
| ``` | ||
|
|
||
| **Approach 2: `useNavigate()` (when path + search need to change together)** | ||
|
|
||
| ```typescript | ||
| import { useNavigate, useSearchParams } from "react-router-dom"; | ||
|
|
||
| const useFilterNavigation = () => { | ||
| const navigate = useNavigate(); | ||
| const [searchParams] = useSearchParams(); | ||
|
|
||
| const navigateWithFilters = ( | ||
| newParams: URLSearchParams, | ||
| options?: { replace?: boolean; pathname?: string }, | ||
| ) => { | ||
| navigate( | ||
| { pathname: options?.pathname ?? ".", search: newParams.toString() }, | ||
| { replace: options?.replace }, | ||
| ); | ||
| }; | ||
|
|
||
| return { navigateWithFilters, searchParams }; | ||
| }; | ||
| ``` | ||
|
|
||
| Use `setSearchParams()` when only filters change. Use `useNavigate()` when both the path segment and query string need to update simultaneously (e.g., changing category + resetting filters). `setSearchParams()` with `{ replace: true }` maps directly to the skill's navigation strategy — use it for search-as-you-type, omit it (defaults to push) for discrete filter actions. | ||
|
|
||
| ## Hook Binding | ||
|
|
||
| Concrete React Router binding for the generic `useFilters` hook described in SKILL.md Step 4: | ||
|
|
||
| ```typescript | ||
| import { useSearchParams } from "react-router-dom"; | ||
|
|
||
| const useFilters = <T extends Record<string, unknown>>({ | ||
| defaults, | ||
| serialize, | ||
| deserialize, | ||
| }: { | ||
| defaults: T; | ||
| serialize: (filters: T) => URLSearchParams; | ||
| deserialize: (params: URLSearchParams) => T; | ||
| }) => { | ||
| const [searchParams, setSearchParams] = useSearchParams(); | ||
|
|
||
| const filters = deserialize(searchParams); | ||
|
|
||
| const updateFilters = ( | ||
| partial: Partial<T>, | ||
| options?: { replace?: boolean }, | ||
| ) => { | ||
| setSearchParams( | ||
| (prev) => { | ||
| const current = deserialize(prev); | ||
| const next = { ...current, ...partial }; | ||
| return serialize(next); | ||
| }, | ||
| { replace: options?.replace }, | ||
| ); | ||
| }; | ||
|
|
||
| const resetFilters = () => { | ||
| setSearchParams(serialize(defaults)); | ||
| }; | ||
|
|
||
| return { filters, updateFilters, resetFilters }; | ||
| }; | ||
| ``` | ||
|
|
||
| The hook reads `searchParams` for the current filter state and uses `setSearchParams` with a functional updater for writes. No `useEffect` or `useState` needed. | ||
|
|
||
| Note: `{ ...filters, ...partial }` is a shallow merge. Use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates merge correctly. | ||
|
|
||
| ## Bracket Notation Parsing | ||
|
|
||
| React Router does not parse bracket notation (`filter[color]=blue`) into structured objects — the `searchParams` instance treats `filter[color]` as a literal key string. The deserialization function must parse bracket keys manually. This logic is framework-agnostic. See SKILL.md Step 3 for the serialization/deserialization pattern. | ||
|
|
||
| ```typescript | ||
| // searchParams.get('filter[color]') → 'blue' | ||
| // searchParams.getAll('filter[tags]') → ['a', 'b'] | ||
| // Parse bracket keys to extract field names and structure | ||
| ``` | ||
|
|
||
| ## Anti-Patterns | ||
|
|
||
| | Anti-Pattern | Instead Do | | ||
| | ------------------------------------------------------------------- | --------------------------------------------------------------------------- | | ||
| | Using `useNavigate()` when only filters change | Use `setSearchParams()` — simpler, doesn't require path construction | | ||
| | Mutating the `searchParams` object directly and expecting re-render | Create new `URLSearchParams` via `serialize()`, pass to `setSearchParams()` | | ||
| | Using `setSearchParams` with string instead of `URLSearchParams` | Pass `URLSearchParams` object for type safety and correct encoding | | ||
| | Wrapping `setSearchParams` in `useEffect` to sync state | Call `setSearchParams` directly in event handlers — no effect sync needed | | ||
| | Using `navigate(-1)` to "undo" filter changes | Rely on browser Back button — push navigation already handles this | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| --- | ||
| sidebar_position: 29 | ||
|
marekgieldatsh marked this conversation as resolved.
|
||
| title: Implementing Filters | ||
| --- | ||
|
|
||
| # Implementing Filters | ||
|
|
||
| **Folder:** `.github/skills/tsh-implementing-filters/` | ||
| **Used by:** Software Engineer | ||
|
|
||
| Type-safe URL filter synchronization with clean path and query string routing. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns. | ||
|
|
||
|
marekgieldatsh marked this conversation as resolved.
|
||
| ## Key Areas | ||
|
|
||
| | Area | Coverage | | ||
| | ----------------------- | ---------------------------------------------------------------------------------------- | | ||
| | **Filter Schema** | TypeScript types, defaults, supported param types (single, multi-select, range, boolean) | | ||
| | **URL Structure** | Path-vs-query golden rule, bracket notation (`filter[field]=value`) | | ||
| | **Serialization** | Serialize/deserialize functions, bracket notation, snake_case keys, default omission | | ||
| | **Filter Sync Hook** | `useFilters` hook — typed state from URL, `updateFilters`, `resetFilters` | | ||
| | **Navigation Strategy** | Push vs replace per action type, debounced inputs, search-as-you-type | | ||
| | **API Contract** | TSH bracket notation default, external API adaptation, response envelope | | ||
|
|
||
| ## When to Use | ||
|
|
||
| - Implementing filterable lists, search pages, or faceted navigation | ||
| - Persisting filter state in the URL for shareable links and back/forward navigation | ||
| - Building filter sync hooks that derive state from URL search params | ||
| - Adapting serialization for external APIs with non-bracket conventions | ||
|
|
||
| ## Connected Skills | ||
|
|
||
| - `tsh-implementing-frontend` — component patterns that consume headless filter logic | ||
| - `tsh-implementing-forms` — form-based filter UIs | ||
| - `tsh-writing-hooks` — hook composition patterns for the filter sync hook | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| --- | ||
| sidebar_position: 29 | ||
| sidebar_position: 30 | ||
| title: Frontend Optimization | ||
| --- | ||
|
|
||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.