Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -178,25 +178,26 @@ Import from `@offload-project/hallogen/hooks/<name>`:
| `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 |
| `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 |

## Utilities

Import from `@offload-project/hallogen/lib/<name>`:

| 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

Expand Down
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -342,10 +346,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"
Expand Down Expand Up @@ -382,6 +382,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"
Expand Down
53 changes: 53 additions & 0 deletions src/hooks/use-index-filters.ts
Original file line number Diff line number Diff line change
@@ -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<string, string | string[] | null>;
type FilterPatch = Partial<FilterValues & { sort: string }>;

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<string, string | string[]> = {};

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]),
};
}
5 changes: 0 additions & 5 deletions src/hooks/use-page-props.ts

This file was deleted.

28 changes: 28 additions & 0 deletions src/lib/fire-toasts.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
6 changes: 6 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,9 @@ export interface HeaderProps extends HTMLAttributes<HTMLDivElement>, HeadingType
title?: string;
description?: string;
}

export interface ToastMessage {
type: "success" | "info" | "warning" | "error";
title?: string;
message: string;
}