From 7e13155f142e6c3c996d95cf39fd1af59dc5bb06 Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Fri, 3 Jul 2026 22:19:40 -0400 Subject: [PATCH 1/2] feat: remove use page props hook --- README.md | 16 ++++++++-------- package.json | 8 ++++---- src/hooks/use-page-props.ts | 5 ----- src/lib/fire-toasts.ts | 28 ++++++++++++++++++++++++++++ src/types/index.ts | 6 ++++++ 5 files changed, 46 insertions(+), 17 deletions(-) delete mode 100644 src/hooks/use-page-props.ts create mode 100644 src/lib/fire-toasts.ts diff --git a/README.md b/README.md index 3cc9a0d..d6d1677 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,6 @@ Import from `@offload-project/hallogen/hooks/`: | `use-media-query` | Subscribe to a CSS media query | | `use-mobile` | Boolean for the mobile breakpoint | | `use-navbar` | Navbar open/collapsed state | -| `use-page-props` | Typed access to Inertia page props | | `use-sidebar` | Sidebar open/collapsed state | | `use-table-context` | Access `resource-table` context | @@ -190,13 +189,14 @@ Import from `@offload-project/hallogen/hooks/`: Import from `@offload-project/hallogen/lib/`: -| Module | Exports | -|-----------------|-------------------------------------------------------------------------------------------------------------------------------| -| `lib/cn` | `cn()` — merge class names (clsx + tailwind-merge) | -| `lib/primitive` | `cx()` — class-name composer for variant primitives | -| `lib/number` | `formatNumber`, `formatCurrency`, `formatKilo`, `formatFileSize` | -| `lib/date` | `formatDate`, `formatDatetime`, `formatHumans`, `formatAge`, `calculateDuration`, `parseTimeStringToTimeObject`, `dayOfWeeks` | -| `lib/filter` | `parseSortDescriptor`, `handleSearch`, `handleSortChange`, `hasActiveFilters` | +| Module | Exports | +|-------------------|-------------------------------------------------------------------------------------------------------------------------------| +| `lib/cn` | `cn()` — merge class names (clsx + tailwind-merge) | +| `lib/primitive` | `cx()` — class-name composer for variant primitives | +| `lib/number` | `formatNumber`, `formatCurrency`, `formatKilo`, `formatFileSize` | +| `lib/date` | `formatDate`, `formatDatetime`, `formatHumans`, `formatAge`, `calculateDuration`, `parseTimeStringToTimeObject`, `dayOfWeeks` | +| `lib/filter` | `parseSortDescriptor`, `handleSearch`, `handleSortChange`, `hasActiveFilters` | +| `lib/fire-toasts` | `fireToasts(toasts)` — fire an array of `ToastMessage`s via `sonner` (needs the `sonner` peer) | ## Development diff --git a/package.json b/package.json index eb6f440..dd1dff4 100644 --- a/package.json +++ b/package.json @@ -342,10 +342,6 @@ "types": "./dist/hooks/use-navbar.d.ts", "import": "./dist/hooks/use-navbar.js" }, - "./hooks/use-page-props": { - "types": "./dist/hooks/use-page-props.d.ts", - "import": "./dist/hooks/use-page-props.js" - }, "./hooks/use-sidebar": { "types": "./dist/hooks/use-sidebar.d.ts", "import": "./dist/hooks/use-sidebar.js" @@ -382,6 +378,10 @@ "types": "./dist/lib/filter.d.ts", "import": "./dist/lib/filter.js" }, + "./lib/fire-toasts": { + "types": "./dist/lib/fire-toasts.d.ts", + "import": "./dist/lib/fire-toasts.js" + }, "./lib/number": { "types": "./dist/lib/number.d.ts", "import": "./dist/lib/number.js" diff --git a/src/hooks/use-page-props.ts b/src/hooks/use-page-props.ts deleted file mode 100644 index eea3462..0000000 --- a/src/hooks/use-page-props.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { usePage } from "@inertiajs/react"; - -export function usePageProps() { - return usePage().props; -} diff --git a/src/lib/fire-toasts.ts b/src/lib/fire-toasts.ts new file mode 100644 index 0000000..1ff5005 --- /dev/null +++ b/src/lib/fire-toasts.ts @@ -0,0 +1,28 @@ +import { toast } from "sonner"; +import type { ToastMessage } from "@/types"; + +export function fireToasts(toasts?: ToastMessage[]) { + if (!toasts?.length) return; + + for (const t of toasts) { + const options = { description: t.title ? t.message : undefined }; + const message = t.title || t.message; + + switch (t.type) { + case "success": + toast.success(message, options); + break; + case "error": + toast.error(message, options); + break; + case "info": + toast.info(message, options); + break; + case "warning": + toast.warning(message, options); + break; + default: + toast(message, options); + } + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 79b5f43..bfc98f2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -79,3 +79,9 @@ export interface HeaderProps extends HTMLAttributes, HeadingType title?: string; description?: string; } + +export interface ToastMessage { + type: "success" | "info" | "warning" | "error"; + title?: string; + message: string; +} From 63cb9780eb98f9a0fcb154b6d2d434f17bd362b9 Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Sat, 4 Jul 2026 00:09:03 -0400 Subject: [PATCH 2/2] feat: add use index filters hook --- README.md | 3 +- package.json | 4 +++ src/hooks/use-index-filters.ts | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/hooks/use-index-filters.ts diff --git a/README.md b/README.md index d6d1677..2139799 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ bun add react react-dom @inertiajs/react lucide-react tailwindcss |------------------------|---------|-----------|-----------------------------------------------------------------------------------| | `react` | `^19` | required | everything | | `react-dom` | `^19` | required | everything | -| `@inertiajs/react` | `^3` | required | navbar, sidebar, pagination, `link`, composite render components, page-prop hooks | +| `@inertiajs/react` | `^3` | required | navbar, sidebar, pagination, `link`, composite render components, filter/URL hooks | | `lucide-react` | `^1` | required | component icons | | `tailwindcss` | `^4` | required | styling | | `recharts` | `^3` | optional | `area-chart`, `bar-chart`, `line-chart`, `pie-chart`, `chart` | @@ -178,6 +178,7 @@ Import from `@offload-project/hallogen/hooks/`: | `use-command-menu` | Open/close state for `command-menu` | | `use-confirm-action` | Confirmation-dialog flow for destructive actions | | `use-current-url` | Current Inertia URL helpers | +| `use-index-filters` | Inertia filter/sort wiring for index pages (`applyFilters`, `sortDescriptor`, `onSortChange`, `onSearchSubmit`) | | `use-initials` | Derive initials from a name (for `avatar`) | | `use-media-query` | Subscribe to a CSS media query | | `use-mobile` | Boolean for the mobile breakpoint | diff --git a/package.json b/package.json index dd1dff4..0bc4f67 100644 --- a/package.json +++ b/package.json @@ -326,6 +326,10 @@ "types": "./dist/hooks/use-current-url.d.ts", "import": "./dist/hooks/use-current-url.js" }, + "./hooks/use-index-filters": { + "types": "./dist/hooks/use-index-filters.d.ts", + "import": "./dist/hooks/use-index-filters.js" + }, "./hooks/use-initials": { "types": "./dist/hooks/use-initials.d.ts", "import": "./dist/hooks/use-initials.js" diff --git a/src/hooks/use-index-filters.ts b/src/hooks/use-index-filters.ts new file mode 100644 index 0000000..a242da4 --- /dev/null +++ b/src/hooks/use-index-filters.ts @@ -0,0 +1,53 @@ +import { router } from "@inertiajs/react"; +import { useCallback, useMemo } from "react"; +import { handleSearch, handleSortChange, parseSortDescriptor } from "@/lib/filter.ts"; + +type FilterValues = Record; +type FilterPatch = Partial; + +interface Options { + /** The index route URL to push filter/sort state to. */ + url: string; + /** Current filter state from the server (used to merge partial updates). */ + filters: FilterValues; + /** Current sort string (e.g. `-created_at`). */ + sort: string; +} + +/** + * Shared filter/sort wiring for admin index pages. Merges a partial patch over + * the current filters, serializes them to `filter[key]` query params (omitting + * empties), and navigates while preserving state/scroll. Returns memoized + * handlers so the table's collection caching isn't fighting new identities. + */ +export function useIndexFilters({ url, filters, sort }: Options) { + const applyFilters = useCallback( + (patch: FilterPatch) => { + const params: Record = {}; + + for (const [key, value] of Object.entries({ ...filters, ...patch })) { + if (key === "sort") { + continue; + } + if (Array.isArray(value) ? value.length > 0 : value) { + params[`filter[${key}]`] = value as string | string[]; + } + } + + const sortValue = "sort" in patch ? patch.sort : sort; + if (sortValue) { + params.sort = sortValue; + } + + router.get(url, params, { preserveState: true, preserveScroll: true }); + }, + [url, filters, sort], + ); + + return { + applyFilters, + sortDescriptor: parseSortDescriptor(sort), + onSortChange: useMemo(() => handleSortChange(applyFilters), [applyFilters]), + onSearchSubmit: useMemo(() => handleSearch(applyFilters), [applyFilters]), + }; +}