From 4c2cb4b1ceb5d105aab353d41046595601ca75dd Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 23 Jan 2026 05:50:16 +0000 Subject: [PATCH] Generated by Spark: Expand custom hook library, expand ui component library --- LIBRARIES.md | 227 +++++++++++++++++++++++++ src/components/ui/README.md | 168 ++++++++++++++++++ src/components/ui/data-table.tsx | 77 +++++++++ src/components/ui/filter-bar.tsx | 38 +++++ src/components/ui/grid.tsx | 39 +++++ src/components/ui/list.tsx | 67 ++++++++ src/components/ui/metric-card.tsx | 114 +++++++++++++ src/components/ui/modal.tsx | 110 ++++++++++++ src/components/ui/page-header.tsx | 62 +++++++ src/components/ui/quick-pagination.tsx | 49 ++++++ src/components/ui/section.tsx | 62 +++++++ src/components/ui/stack.tsx | 47 +++++ src/components/ui/tag.tsx | 56 ++++++ src/hooks/README.md | 168 +++++++++++++++++- src/hooks/index.ts | 23 +++ src/hooks/use-array.ts | 59 +++++++ src/hooks/use-clipboard.ts | 29 ++++ src/hooks/use-confirmation.ts | 61 +++++++ src/hooks/use-countdown.ts | 39 +++++ src/hooks/use-disclosure.ts | 17 ++ src/hooks/use-download.ts | 65 +++++++ src/hooks/use-focus-trap.ts | 42 +++++ src/hooks/use-form-state.ts | 72 ++++++++ src/hooks/use-interval.ts | 16 ++ src/hooks/use-map.ts | 48 ++++++ src/hooks/use-multi-step-form.ts | 98 +++++++++++ src/hooks/use-query-params.ts | 64 +++++++ src/hooks/use-set.ts | 58 +++++++ src/hooks/use-steps.ts | 40 +++++ src/hooks/use-table.ts | 106 ++++++++++++ src/hooks/use-timeout.ts | 16 ++ src/hooks/use-undo.ts | 66 +++++++ 32 files changed, 2202 insertions(+), 1 deletion(-) create mode 100644 LIBRARIES.md create mode 100644 src/components/ui/data-table.tsx create mode 100644 src/components/ui/filter-bar.tsx create mode 100644 src/components/ui/grid.tsx create mode 100644 src/components/ui/list.tsx create mode 100644 src/components/ui/metric-card.tsx create mode 100644 src/components/ui/modal.tsx create mode 100644 src/components/ui/page-header.tsx create mode 100644 src/components/ui/quick-pagination.tsx create mode 100644 src/components/ui/section.tsx create mode 100644 src/components/ui/stack.tsx create mode 100644 src/components/ui/tag.tsx create mode 100644 src/hooks/use-array.ts create mode 100644 src/hooks/use-clipboard.ts create mode 100644 src/hooks/use-confirmation.ts create mode 100644 src/hooks/use-countdown.ts create mode 100644 src/hooks/use-disclosure.ts create mode 100644 src/hooks/use-download.ts create mode 100644 src/hooks/use-focus-trap.ts create mode 100644 src/hooks/use-form-state.ts create mode 100644 src/hooks/use-interval.ts create mode 100644 src/hooks/use-map.ts create mode 100644 src/hooks/use-multi-step-form.ts create mode 100644 src/hooks/use-query-params.ts create mode 100644 src/hooks/use-set.ts create mode 100644 src/hooks/use-steps.ts create mode 100644 src/hooks/use-table.ts create mode 100644 src/hooks/use-timeout.ts create mode 100644 src/hooks/use-undo.ts diff --git a/LIBRARIES.md b/LIBRARIES.md new file mode 100644 index 0000000..6a6ba71 --- /dev/null +++ b/LIBRARIES.md @@ -0,0 +1,227 @@ +# WorkForce Pro - Developer Libraries + +## Overview + +This document provides an overview of the expanded custom hook library and UI component library available in the WorkForce Pro application. These libraries provide production-ready, reusable building blocks for rapid feature development. + +## Custom Hook Library + +The application includes **40+ custom React hooks** organized into the following categories: + +### State Management (8 hooks) +- `useToggle` - Boolean state toggle +- `usePrevious` - Access previous value +- `useLocalStorage` - Persist to localStorage +- `useDisclosure` - Modal/drawer state +- `useUndo` - Undo/redo functionality +- `useFormState` - Form state with validation +- `useArray` - Array manipulation utilities +- `useMap` - Map data structure utilities +- `useSet` - Set data structure utilities + +### Async Operations (5 hooks) +- `useAsync` - Async operation handling +- `useDebounce` - Debounce values +- `useThrottle` - Throttle function calls +- `useInterval` - Declarative intervals +- `useTimeout` - Declarative timeouts + +### UI & Interaction (12 hooks) +- `useMediaQuery` - Media query matching +- `useIsMobile` - Mobile detection +- `useWindowSize` - Window dimensions +- `useScrollPosition` - Scroll tracking +- `useOnClickOutside` - Outside click detection +- `useIntersectionObserver` - Visibility detection +- `useKeyboardShortcut` - Keyboard shortcuts +- `useIdleTimer` - Idle state detection +- `useCopyToClipboard` - Copy to clipboard +- `useClipboard` - Enhanced clipboard +- `useFocusTrap` - Focus management +- `useCountdown` - Countdown timer + +### Data Management (5 hooks) +- `useFilter` - Array filtering +- `useSort` - Array sorting +- `usePagination` - Data pagination +- `useSelection` - Multi-select management +- `useTable` - Complete table utilities + +### Forms & Validation (4 hooks) +- `useFormValidation` - Form validation +- `useWizard` - Multi-step wizards +- `useMultiStepForm` - Advanced multi-step forms +- `useSteps` - Step navigation +- `useConfirmation` - Confirmation dialogs + +### Browser & Navigation (2 hooks) +- `useQueryParams` - URL query params +- `useDownload` - File downloads + +### Application-Specific (2 hooks) +- `useNotifications` - Notification system +- `useSampleData` - Sample data initialization + +## UI Component Library + +The application includes **70+ UI components** from shadcn v4, plus custom components: + +### Display Components +- `EmptyState` - Empty state placeholder +- `StatusBadge` - Status indicators +- `StatCard` - Simple metric cards +- `MetricCard` - Advanced metric cards with trends +- `DataList` - Key-value pair lists +- `DataTable` - Generic data tables +- `Timeline` - Event timelines +- `List` / `ListItem` - Structured lists +- `Chip` - Tag chips +- `Badge` - Status badges +- `Avatar` - User avatars +- `Card` - Content cards +- `Alert` - Alert messages + +### Layout Components +- `PageHeader` / `PageTitle` / `PageDescription` / `PageActions` - Page headers +- `Section` / `SectionHeader` / `SectionTitle` / `SectionContent` - Content sections +- `Stack` - Flex containers with spacing +- `Grid` - Responsive grid layouts +- `Separator` - Dividers +- `Divider` - Enhanced dividers with labels +- `ScrollArea` - Scrollable containers +- `Resizable` - Resizable panels + +### Input Components +- `Input` - Text inputs +- `Textarea` - Multi-line text +- `Select` - Dropdowns +- `Checkbox` - Checkboxes +- `RadioGroup` - Radio buttons +- `Switch` - Toggle switches +- `Slider` - Range sliders +- `Calendar` - Date picker +- `SearchInput` - Search with clear +- `FileUpload` - File upload with drag-drop +- `InputOTP` - OTP inputs +- `Form` - Form components + +### Navigation Components +- `Tabs` - Tab navigation +- `Breadcrumb` - Breadcrumb navigation +- `Pagination` - Full pagination +- `QuickPagination` - Simple pagination +- `Stepper` - Multi-step indicator +- `NavigationMenu` - Navigation menus +- `Menubar` - Menu bars +- `Sidebar` - Application sidebar + +### Modal & Dialog Components +- `Dialog` - Standard dialogs +- `AlertDialog` - Confirmation dialogs +- `Modal` / `ModalHeader` / `ModalBody` / `ModalFooter` - Custom modals +- `Sheet` - Side panels +- `Drawer` - Drawer panels +- `Popover` - Popovers +- `HoverCard` - Hover cards +- `Tooltip` - Tooltips +- `ContextMenu` - Context menus +- `DropdownMenu` - Dropdown menus + +### Filter & Search Components +- `FilterBar` / `FilterGroup` - Filter controls +- `Tag` / `TagGroup` - Removable tags +- `Command` - Command palette +- `Combobox` - Searchable selects + +### Utility Components +- `LoadingSpinner` - Loading indicators +- `LoadingOverlay` - Full-screen loading +- `Progress` - Progress bars +- `Skeleton` - Loading skeletons +- `CopyButton` - Copy to clipboard +- `CodeBlock` - Code display +- `InfoBox` - Info messages +- `Kbd` - Keyboard shortcuts display +- `SortableHeader` - Sortable table headers + +### Chart Components +- `Chart` - Recharts wrapper with themes + +## Best Practices + +### When to Use Hooks vs Components + +**Use Hooks When:** +- Managing state or side effects +- Sharing logic between components +- Accessing browser APIs +- Managing complex interactions + +**Use Components When:** +- Creating reusable UI elements +- Defining visual structures +- Composing layouts +- Building forms + +### Performance Considerations + +1. **Memoization**: Most hooks use `useCallback` and `useMemo` internally +2. **Lazy Loading**: Import only what you need +3. **State Colocation**: Keep state close to where it's used +4. **Pagination**: Use `useTable` or `usePagination` for large datasets + +### Composition Patterns + +**Hooks Composition:** +```tsx +function useTimesheetForm() { + const { values, errors, handleChange } = useFormState(initialData) + const { isOpen, open, close } = useDisclosure() + const { copy } = useClipboard() + + return { values, errors, handleChange, isOpen, open, close, copy } +} +``` + +**Component Composition:** +```tsx + + + Title + + + + + +``` + +## Quick Reference + +### Most Commonly Used Hooks +1. `useKV` - Data persistence (Spark SDK) +2. `useTable` - Table management +3. `useDisclosure` - Modal state +4. `useFormState` - Form handling +5. `useDebounce` - Search optimization + +### Most Commonly Used Components +1. `Button` - Primary actions +2. `Card` - Content containers +3. `Dialog` - Modals and confirmations +4. `Input` / `Select` - Form fields +5. `Table` - Data display +6. `Badge` - Status indicators +7. `Skeleton` - Loading states + +## Documentation + +- **Hooks**: See `/src/hooks/README.md` for detailed examples +- **Components**: See `/src/components/ui/README.md` for usage guides +- **Types**: All hooks and components are fully typed with TypeScript + +## Support + +For questions or issues with the libraries, please refer to: +1. Individual hook/component files for implementation details +2. README files in respective directories +3. TypeScript type definitions for API contracts diff --git a/src/components/ui/README.md b/src/components/ui/README.md index 7ccd7a6..c511bc1 100644 --- a/src/components/ui/README.md +++ b/src/components/ui/README.md @@ -38,6 +38,23 @@ Metric display card with optional trend indicator. /> ``` +#### MetricCard +Flexible metric card with composable parts. + +```tsx + + + Active Workers + + + + 1,234 + +12% from last month + ↑ 12% + + +``` + #### DataList Key-value pair display list. @@ -52,6 +69,22 @@ Key-value pair display list. /> ``` +#### DataTable +Generic data table with custom column rendering. + +```tsx + }, + { key: 'amount', header: 'Amount', width: '120px' } + ]} + data={timesheets} + onRowClick={(row) => viewDetails(row)} + emptyMessage="No data found" +/> +``` + #### Timeline Chronological event timeline with completion states. @@ -65,6 +98,80 @@ Chronological event timeline with completion states. /> ``` +#### List +Composable list component for structured data. + +```tsx + + + John Smith + Software Engineer + + +``` + +### Layout Components + +#### PageHeader +Page header with title, description, and actions. + +```tsx + + +
+ Timesheets + Manage and approve worker timesheets +
+ + + + +
+
+``` + +#### Section +Content section with header. + +```tsx +
+ + Recent Activity + Your latest updates and changes + + + {/* content */} + +
+``` + +#### Stack +Flexible container for arranging items with consistent spacing. + +```tsx + + + + + + + + Left + Right + +``` + +#### Grid +Responsive grid layout. + +```tsx + + Card 1 + Card 2 + Card 3 + +``` + ### Input Components #### SearchInput @@ -91,6 +198,36 @@ Drag-and-drop file upload area. /> ``` +#### FilterBar +Container for filter controls. + +```tsx + + + + + + + + +``` + +#### Tag +Removable tag component with variants. + +```tsx + + removeTag('js')}> + JavaScript + + Active + Pending + +``` + ### Navigation Components #### Stepper @@ -108,6 +245,37 @@ Multi-step progress indicator. /> ``` +#### QuickPagination +Simple pagination controls. + +```tsx + +``` + +### Modal Components + +#### Modal +Flexible modal dialog with composable parts. + +```tsx + + + Edit Timesheet + + + {/* form content */} + + + + + + +``` + ### Utility Components #### LoadingSpinner diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx new file mode 100644 index 0000000..09caafc --- /dev/null +++ b/src/components/ui/data-table.tsx @@ -0,0 +1,77 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface DataTableProps extends React.HTMLAttributes { + columns: Array<{ + key: keyof T + header: string + width?: string + sortable?: boolean + render?: (value: any, row: T) => React.ReactNode + }> + data: T[] + onRowClick?: (row: T) => void + emptyMessage?: string +} + +export function DataTable>({ + columns, + data, + onRowClick, + emptyMessage = 'No data available', + className, + ...props +}: DataTableProps) { + return ( +
+
+ + + + {columns.map((column) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, rowIndex) => ( + onRowClick?.(row)} + className={cn( + 'bg-card hover:bg-muted/50 transition-colors', + onRowClick && 'cursor-pointer' + )} + > + {columns.map((column) => ( + + ))} + + )) + )} + +
+ {column.header} +
+ {emptyMessage} +
+ {column.render + ? column.render(row[column.key], row) + : String(row[column.key] ?? '')} +
+
+
+ ) +} diff --git a/src/components/ui/filter-bar.tsx b/src/components/ui/filter-bar.tsx new file mode 100644 index 0000000..ba5500d --- /dev/null +++ b/src/components/ui/filter-bar.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface FilterBarProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function FilterBar({ children, className, ...props }: FilterBarProps) { + return ( +
+ {children} +
+ ) +} + +export interface FilterGroupProps extends React.HTMLAttributes { + label?: string + children: React.ReactNode +} + +export function FilterGroup({ label, children, className, ...props }: FilterGroupProps) { + return ( +
+ {label && ( + + )} + {children} +
+ ) +} diff --git a/src/components/ui/grid.tsx b/src/components/ui/grid.tsx new file mode 100644 index 0000000..a6e8bf7 --- /dev/null +++ b/src/components/ui/grid.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface GridProps extends React.HTMLAttributes { + children: React.ReactNode + cols?: 1 | 2 | 3 | 4 | 5 | 6 + gap?: 'none' | 'sm' | 'md' | 'lg' | 'xl' +} + +export function Grid({ + children, + cols = 1, + gap = 'md', + className, + ...props +}: GridProps) { + return ( +
+ {children} +
+ ) +} diff --git a/src/components/ui/list.tsx b/src/components/ui/list.tsx new file mode 100644 index 0000000..33beaf1 --- /dev/null +++ b/src/components/ui/list.tsx @@ -0,0 +1,67 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface ListProps extends React.HTMLAttributes { + children: React.ReactNode + variant?: 'default' | 'bordered' | 'divided' +} + +export function List({ children, variant = 'default', className, ...props }: ListProps) { + return ( +
    + {children} +
+ ) +} + +export interface ListItemProps extends React.HTMLAttributes { + children: React.ReactNode + interactive?: boolean +} + +export function ListItem({ children, interactive, className, ...props }: ListItemProps) { + return ( +
  • + {children} +
  • + ) +} + +export interface ListItemTitleProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function ListItemTitle({ children, className, ...props }: ListItemTitleProps) { + return ( +
    + {children} +
    + ) +} + +export interface ListItemDescriptionProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function ListItemDescription({ children, className, ...props }: ListItemDescriptionProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/components/ui/metric-card.tsx b/src/components/ui/metric-card.tsx new file mode 100644 index 0000000..dcfb69b --- /dev/null +++ b/src/components/ui/metric-card.tsx @@ -0,0 +1,114 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +const MetricCard = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +MetricCard.displayName = 'MetricCard' + +const MetricCardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +MetricCardHeader.displayName = 'MetricCardHeader' + +const MetricCardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +MetricCardTitle.displayName = 'MetricCardTitle' + +const MetricCardIcon = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +MetricCardIcon.displayName = 'MetricCardIcon' + +const MetricCardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +MetricCardContent.displayName = 'MetricCardContent' + +const MetricCardValue = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +MetricCardValue.displayName = 'MetricCardValue' + +const MetricCardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +MetricCardDescription.displayName = 'MetricCardDescription' + +const MetricCardTrend = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { trend?: 'up' | 'down' | 'neutral' } +>(({ className, trend = 'neutral', ...props }, ref) => ( +

    +)) +MetricCardTrend.displayName = 'MetricCardTrend' + +export { + MetricCard, + MetricCardHeader, + MetricCardTitle, + MetricCardIcon, + MetricCardContent, + MetricCardValue, + MetricCardDescription, + MetricCardTrend +} diff --git a/src/components/ui/modal.tsx b/src/components/ui/modal.tsx new file mode 100644 index 0000000..c949d98 --- /dev/null +++ b/src/components/ui/modal.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' +import { Button } from './button' +import { X } from '@phosphor-icons/react' + +export interface ModalProps { + isOpen: boolean + onClose: () => void + children: React.ReactNode + size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' + className?: string +} + +export function Modal({ + isOpen, + onClose, + children, + size = 'md', + className +}: ModalProps) { + if (!isOpen) return null + + return ( +
    +
    +
    + {children} +
    +
    + ) +} + +export interface ModalHeaderProps extends React.HTMLAttributes { + children: React.ReactNode + onClose?: () => void +} + +export function ModalHeader({ children, onClose, className, ...props }: ModalHeaderProps) { + return ( +
    +
    {children}
    + {onClose && ( + + )} +
    + ) +} + +export interface ModalTitleProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function ModalTitle({ children, className, ...props }: ModalTitleProps) { + return ( +

    + {children} +

    + ) +} + +export interface ModalBodyProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function ModalBody({ children, className, ...props }: ModalBodyProps) { + return ( +
    + {children} +
    + ) +} + +export interface ModalFooterProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function ModalFooter({ children, className, ...props }: ModalFooterProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/components/ui/page-header.tsx b/src/components/ui/page-header.tsx new file mode 100644 index 0000000..824efc0 --- /dev/null +++ b/src/components/ui/page-header.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface PageHeaderProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function PageHeader({ children, className, ...props }: PageHeaderProps) { + return ( +
    + {children} +
    + ) +} + +export interface PageTitleProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function PageTitle({ children, className, ...props }: PageTitleProps) { + return ( +

    + {children} +

    + ) +} + +export interface PageDescriptionProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function PageDescription({ children, className, ...props }: PageDescriptionProps) { + return ( +

    + {children} +

    + ) +} + +export interface PageActionsProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function PageActions({ children, className, ...props }: PageActionsProps) { + return ( +
    + {children} +
    + ) +} + +export interface PageHeaderRowProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function PageHeaderRow({ children, className, ...props }: PageHeaderRowProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/components/ui/quick-pagination.tsx b/src/components/ui/quick-pagination.tsx new file mode 100644 index 0000000..1ff8610 --- /dev/null +++ b/src/components/ui/quick-pagination.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' +import { Button } from './button' +import { CaretLeft, CaretRight } from '@phosphor-icons/react' + +export interface QuickPaginationProps { + currentPage: number + totalPages: number + onPageChange: (page: number) => void + className?: string +} + +export function QuickPagination({ + currentPage, + totalPages, + onPageChange, + className +}: QuickPaginationProps) { + const canGoPrevious = currentPage > 1 + const canGoNext = currentPage < totalPages + + return ( +
    + + + + Page {currentPage} of {totalPages} + + + +
    + ) +} diff --git a/src/components/ui/section.tsx b/src/components/ui/section.tsx new file mode 100644 index 0000000..8b41fe4 --- /dev/null +++ b/src/components/ui/section.tsx @@ -0,0 +1,62 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface SectionProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function Section({ children, className, ...props }: SectionProps) { + return ( +
    + {children} +
    + ) +} + +export interface SectionHeaderProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function SectionHeader({ children, className, ...props }: SectionHeaderProps) { + return ( +
    + {children} +
    + ) +} + +export interface SectionTitleProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function SectionTitle({ children, className, ...props }: SectionTitleProps) { + return ( +

    + {children} +

    + ) +} + +export interface SectionDescriptionProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function SectionDescription({ children, className, ...props }: SectionDescriptionProps) { + return ( +

    + {children} +

    + ) +} + +export interface SectionContentProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function SectionContent({ children, className, ...props }: SectionContentProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/components/ui/stack.tsx b/src/components/ui/stack.tsx new file mode 100644 index 0000000..790ffb0 --- /dev/null +++ b/src/components/ui/stack.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' + +export interface StackProps extends React.HTMLAttributes { + children: React.ReactNode + direction?: 'horizontal' | 'vertical' + spacing?: 'none' | 'sm' | 'md' | 'lg' | 'xl' + align?: 'start' | 'center' | 'end' | 'stretch' + justify?: 'start' | 'center' | 'end' | 'between' | 'around' +} + +export function Stack({ + children, + direction = 'vertical', + spacing = 'md', + align = 'stretch', + justify = 'start', + className, + ...props +}: StackProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/components/ui/tag.tsx b/src/components/ui/tag.tsx new file mode 100644 index 0000000..31bf63b --- /dev/null +++ b/src/components/ui/tag.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { cn } from '@/lib/utils' +import { Button } from './button' +import { X } from '@phosphor-icons/react' + +export interface TagProps extends React.HTMLAttributes { + children: React.ReactNode + onRemove?: () => void + variant?: 'default' | 'primary' | 'success' | 'warning' | 'destructive' +} + +export function Tag({ + children, + onRemove, + variant = 'default', + className, + ...props +}: TagProps) { + return ( +
    + {children} + {onRemove && ( + + )} +
    + ) +} + +export interface TagGroupProps extends React.HTMLAttributes { + children: React.ReactNode +} + +export function TagGroup({ children, className, ...props }: TagGroupProps) { + return ( +
    + {children} +
    + ) +} diff --git a/src/hooks/README.md b/src/hooks/README.md index 1ea1b6c..abb2590 100644 --- a/src/hooks/README.md +++ b/src/hooks/README.md @@ -8,11 +8,15 @@ A comprehensive collection of React hooks for the WorkForce Pro platform. - **useToggle** - Boolean state toggle with setter - **usePrevious** - Access previous value of state - **useLocalStorage** - Persist state in localStorage +- **useDisclosure** - Modal/drawer open/close state +- **useUndo** - Undo/redo state management +- **useFormState** - Form state with validation and dirty tracking ### Async Operations - **useAsync** - Handle async operations with loading/error states - **useDebounce** - Debounce rapidly changing values - **useThrottle** - Throttle function calls +- **useInterval** - Declarative setInterval hook ### UI & Interaction - **useMediaQuery** - Responsive media query matching @@ -24,16 +28,25 @@ A comprehensive collection of React hooks for the WorkForce Pro platform. - **useKeyboardShortcut** - Global keyboard shortcuts - **useIdleTimer** - Detect user idle state - **useCopyToClipboard** - Copy text to clipboard +- **useClipboard** - Enhanced clipboard with timeout +- **useFocusTrap** - Trap focus within element ### Data Management - **useFilter** - Filter arrays with debouncing - **useSort** - Sort arrays by key and direction - **usePagination** - Paginate large datasets - **useSelection** - Multi-select management +- **useTable** - Complete table with sort/filter/pagination ### Forms & Validation - **useFormValidation** - Form validation with error handling - **useWizard** - Multi-step form/wizard state +- **useMultiStepForm** - Advanced multi-step form with validation +- **useConfirmation** - Confirmation dialog state + +### Browser & Navigation +- **useQueryParams** - URL query parameter management +- **useDownload** - File download utilities (JSON, CSV, etc.) ### Application-Specific - **useNotifications** - Notification system state @@ -41,6 +54,151 @@ A comprehensive collection of React hooks for the WorkForce Pro platform. ## Usage Examples +### useTable +```tsx +import { useTable } from '@/hooks' + +const { + data, + page, + totalPages, + sortKey, + sortDirection, + handleSort, + handleFilter, + nextPage, + prevPage +} = useTable({ + data: allTimesheets, + pageSize: 10, + initialSort: { key: 'date', direction: 'desc' } +}) +``` + +### useMultiStepForm +```tsx +import { useMultiStepForm } from '@/hooks' + +const { + currentStep, + formData, + errors, + isLastStep, + updateData, + nextStep, + prevStep, + handleSubmit +} = useMultiStepForm({ + initialData: { name: '', email: '', address: '' }, + steps: ['Personal', 'Contact', 'Review'], + onComplete: async (data) => await saveData(data) +}) +``` + +### useConfirmation +```tsx +import { useConfirmation } from '@/hooks' + +const { isOpen, confirm, onConfirm, onCancel, title, message } = useConfirmation() + +const handleDelete = async () => { + const confirmed = await confirm({ + title: 'Delete Item', + message: 'Are you sure? This cannot be undone.', + variant: 'destructive' + }) + if (confirmed) { + await deleteItem() + } +} +``` + +### useDownload +```tsx +import { useDownload } from '@/hooks' + +const { isDownloading, downloadJSON, downloadCSV } = useDownload() + +const exportData = () => { + downloadCSV(timesheets, 'timesheets.csv') +} +``` + +### useUndo +```tsx +import { useUndo } from '@/hooks' + +const { + state, + setState, + undo, + redo, + canUndo, + canRedo +} = useUndo({ text: '' }) +``` + +### useQueryParams +```tsx +import { useQueryParams } from '@/hooks' + +const { params, updateParams, clearParams } = useQueryParams<{ + search: string + status: string +}>() + +updateParams({ search: 'john', status: 'active' }) +``` + +### useClipboard +```tsx +import { useClipboard } from '@/hooks' + +const { isCopied, copy } = useClipboard() + + +``` + +### useDisclosure +```tsx +import { useDisclosure } from '@/hooks' + +const { isOpen, open, close, toggle } = useDisclosure() + + + + ... + +``` + +### useFocusTrap +```tsx +import { useFocusTrap } from '@/hooks' + +const ref = useFocusTrap(isModalOpen) + +
    + {/* Focus will be trapped within this element */} +
    +``` + +### useFormState +```tsx +import { useFormState } from '@/hooks' + +const { + values, + errors, + touched, + isDirty, + setValue, + handleChange, + handleBlur +} = useFormState({ name: '', email: '' }) +``` + ### useDebounce ```tsx import { useDebounce } from '@/hooks' @@ -49,7 +207,6 @@ const [searchTerm, setSearchTerm] = useState('') const debouncedSearch = useDebounce(searchTerm, 500) useEffect(() => { - // API call with debounced value searchAPI(debouncedSearch) }, [debouncedSearch]) ``` @@ -119,3 +276,12 @@ useKeyboardShortcut( () => saveDocument() ) ``` + +### useInterval +```tsx +import { useInterval } from '@/hooks' + +useInterval(() => { + checkForUpdates() +}, 5000) +``` diff --git a/src/hooks/index.ts b/src/hooks/index.ts index dfa6562..a5f6feb 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -21,9 +21,32 @@ export { useThrottle } from './use-throttle' export { useToggle } from './use-toggle' export { useWindowSize } from './use-window-size' export { useWizard } from './use-wizard' +export { useTable } from './use-table' +export { useMultiStepForm } from './use-multi-step-form' +export { useConfirmation } from './use-confirmation' +export { useInterval } from './use-interval' +export { useClipboard } from './use-clipboard' +export { useDownload } from './use-download' +export { useUndo } from './use-undo' +export { useQueryParams } from './use-query-params' +export { useFocusTrap } from './use-focus-trap' +export { useDisclosure } from './use-disclosure' +export { useFormState } from './use-form-state' +export { useCountdown } from './use-countdown' +export { useSteps } from './use-steps' +export { useArray } from './use-array' +export { useTimeout } from './use-timeout' +export { useMap } from './use-map' +export { useSet } from './use-set' export type { AsyncState } from './use-async' export type { FormErrors } from './use-form-validation' export type { IntersectionObserverOptions } from './use-intersection-observer' export type { SortDirection } from './use-sort' export type { Step } from './use-wizard' +export type { UseTableOptions } from './use-table' +export type { UseMultiStepFormOptions } from './use-multi-step-form' +export type { ConfirmationOptions, ConfirmationState } from './use-confirmation' +export type { UseStepsOptions } from './use-steps' +export type { UseMapActions } from './use-map' +export type { UseSetActions } from './use-set' diff --git a/src/hooks/use-array.ts b/src/hooks/use-array.ts new file mode 100644 index 0000000..67c24b4 --- /dev/null +++ b/src/hooks/use-array.ts @@ -0,0 +1,59 @@ +import { useState, useCallback } from 'react' + +export function useArray(initialValue: T[] = []) { + const [array, setArray] = useState(initialValue) + + const push = useCallback((element: T) => { + setArray((prev) => [...prev, element]) + }, []) + + const remove = useCallback((index: number) => { + setArray((prev) => prev.filter((_, i) => i !== index)) + }, []) + + const filter = useCallback((callback: (item: T, index: number) => boolean) => { + setArray((prev) => prev.filter(callback)) + }, []) + + const update = useCallback((index: number, newElement: T) => { + setArray((prev) => prev.map((item, i) => (i === index ? newElement : item))) + }, []) + + const clear = useCallback(() => { + setArray([]) + }, []) + + const set = useCallback((newArray: T[]) => { + setArray(newArray) + }, []) + + const insert = useCallback((index: number, element: T) => { + setArray((prev) => { + const newArray = [...prev] + newArray.splice(index, 0, element) + return newArray + }) + }, []) + + const swap = useCallback((indexA: number, indexB: number) => { + setArray((prev) => { + const newArray = [...prev] + const temp = newArray[indexA] + newArray[indexA] = newArray[indexB] + newArray[indexB] = temp + return newArray + }) + }, []) + + return { + array, + set, + push, + remove, + filter, + update, + clear, + insert, + swap + } +} diff --git a/src/hooks/use-clipboard.ts b/src/hooks/use-clipboard.ts new file mode 100644 index 0000000..1e3e167 --- /dev/null +++ b/src/hooks/use-clipboard.ts @@ -0,0 +1,29 @@ +import { useState, useCallback } from 'react' + +export function useClipboard(timeout = 2000) { + const [isCopied, setIsCopied] = useState(false) + + const copy = useCallback(async (text: string) => { + if (!navigator?.clipboard) { + console.warn('Clipboard API not available') + return false + } + + try { + await navigator.clipboard.writeText(text) + setIsCopied(true) + + setTimeout(() => { + setIsCopied(false) + }, timeout) + + return true + } catch (error) { + console.warn('Copy failed', error) + setIsCopied(false) + return false + } + }, [timeout]) + + return { isCopied, copy } +} diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts new file mode 100644 index 0000000..2868d1f --- /dev/null +++ b/src/hooks/use-confirmation.ts @@ -0,0 +1,61 @@ +import { useState, useCallback } from 'react' + +export interface ConfirmationOptions { + title?: string + message?: string + confirmLabel?: string + cancelLabel?: string + variant?: 'default' | 'destructive' +} + +export interface ConfirmationState extends ConfirmationOptions { + isOpen: boolean + onConfirm: (() => void) | null + onCancel: (() => void) | null +} + +export function useConfirmation() { + const [state, setState] = useState({ + isOpen: false, + onConfirm: null, + onCancel: null, + title: 'Are you sure?', + message: 'This action cannot be undone.', + confirmLabel: 'Confirm', + cancelLabel: 'Cancel', + variant: 'default' + }) + + const confirm = useCallback((options?: ConfirmationOptions) => { + return new Promise((resolve) => { + setState({ + isOpen: true, + title: options?.title || 'Are you sure?', + message: options?.message || 'This action cannot be undone.', + confirmLabel: options?.confirmLabel || 'Confirm', + cancelLabel: options?.cancelLabel || 'Cancel', + variant: options?.variant || 'default', + onConfirm: () => { + setState(prev => ({ ...prev, isOpen: false })) + resolve(true) + }, + onCancel: () => { + setState(prev => ({ ...prev, isOpen: false })) + resolve(false) + } + }) + }) + }, []) + + const close = useCallback(() => { + if (state.onCancel) { + state.onCancel() + } + }, [state]) + + return { + ...state, + confirm, + close + } +} diff --git a/src/hooks/use-countdown.ts b/src/hooks/use-countdown.ts new file mode 100644 index 0000000..d009229 --- /dev/null +++ b/src/hooks/use-countdown.ts @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react' + +export function useCountdown(targetDate: Date | string | number) { + const calculateTimeLeft = () => { + const target = new Date(targetDate).getTime() + const now = new Date().getTime() + const difference = target - now + + if (difference <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isComplete: true + } + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), + minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)), + seconds: Math.floor((difference % (1000 * 60)) / 1000), + isComplete: false + } + } + + const [timeLeft, setTimeLeft] = useState(calculateTimeLeft()) + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft()) + }, 1000) + + return () => clearInterval(timer) + }, [targetDate]) + + return timeLeft +} diff --git a/src/hooks/use-disclosure.ts b/src/hooks/use-disclosure.ts new file mode 100644 index 0000000..bf55560 --- /dev/null +++ b/src/hooks/use-disclosure.ts @@ -0,0 +1,17 @@ +import { useState, useCallback } from 'react' + +export function useDisclosure(initialState = false) { + const [isOpen, setIsOpen] = useState(initialState) + + const open = useCallback(() => setIsOpen(true), []) + const close = useCallback(() => setIsOpen(false), []) + const toggle = useCallback(() => setIsOpen(prev => !prev), []) + + return { + isOpen, + open, + close, + toggle, + onOpenChange: setIsOpen + } +} diff --git a/src/hooks/use-download.ts b/src/hooks/use-download.ts new file mode 100644 index 0000000..0e088bc --- /dev/null +++ b/src/hooks/use-download.ts @@ -0,0 +1,65 @@ +import { useCallback, useState } from 'react' + +export function useDownload() { + const [isDownloading, setIsDownloading] = useState(false) + + const downloadFile = useCallback(async ( + data: string | Blob, + filename: string, + type?: string + ) => { + setIsDownloading(true) + + try { + const blob = typeof data === 'string' + ? new Blob([data], { type: type || 'text/plain' }) + : data + + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + + return true + } catch (error) { + console.error('Download failed:', error) + return false + } finally { + setIsDownloading(false) + } + }, []) + + const downloadJSON = useCallback((data: any, filename: string) => { + const json = JSON.stringify(data, null, 2) + return downloadFile(json, filename, 'application/json') + }, [downloadFile]) + + const downloadCSV = useCallback((data: any[], filename: string) => { + if (data.length === 0) return Promise.resolve(false) + + const headers = Object.keys(data[0]) + const csv = [ + headers.join(','), + ...data.map(row => + headers.map(header => { + const value = row[header] + const stringValue = String(value ?? '') + return stringValue.includes(',') ? `"${stringValue}"` : stringValue + }).join(',') + ) + ].join('\n') + + return downloadFile(csv, filename, 'text/csv') + }, [downloadFile]) + + return { + isDownloading, + downloadFile, + downloadJSON, + downloadCSV + } +} diff --git a/src/hooks/use-focus-trap.ts b/src/hooks/use-focus-trap.ts new file mode 100644 index 0000000..a01aaac --- /dev/null +++ b/src/hooks/use-focus-trap.ts @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react' + +export function useFocusTrap(active = true) { + const ref = useRef(null) + + useEffect(() => { + if (!active || !ref.current) return + + const element = ref.current + const focusableElements = element.querySelectorAll( + 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])' + ) + + const firstElement = focusableElements[0] + const lastElement = focusableElements[focusableElements.length - 1] + + const handleTabKey = (e: KeyboardEvent) => { + if (e.key !== 'Tab') return + + if (e.shiftKey) { + if (document.activeElement === firstElement) { + lastElement?.focus() + e.preventDefault() + } + } else { + if (document.activeElement === lastElement) { + firstElement?.focus() + e.preventDefault() + } + } + } + + element.addEventListener('keydown', handleTabKey) + firstElement?.focus() + + return () => { + element.removeEventListener('keydown', handleTabKey) + } + }, [active]) + + return ref +} diff --git a/src/hooks/use-form-state.ts b/src/hooks/use-form-state.ts new file mode 100644 index 0000000..715d304 --- /dev/null +++ b/src/hooks/use-form-state.ts @@ -0,0 +1,72 @@ +import { useState, useCallback } from 'react' + +export function useFormState>(initialState: T) { + const [values, setValues] = useState(initialState) + const [errors, setErrors] = useState>>({}) + const [touched, setTouched] = useState>>({}) + const [isDirty, setIsDirty] = useState(false) + + const setValue = useCallback((field: K, value: T[K]) => { + setValues(prev => ({ ...prev, [field]: value })) + setIsDirty(true) + }, []) + + const setFieldError = useCallback((field: K, error: string) => { + setErrors(prev => ({ ...prev, [field]: error })) + }, []) + + const clearFieldError = useCallback((field: K) => { + setErrors(prev => { + const newErrors = { ...prev } + delete newErrors[field] + return newErrors + }) + }, []) + + const touchField = useCallback((field: K) => { + setTouched(prev => ({ ...prev, [field]: true })) + }, []) + + const handleChange = useCallback((field: K) => { + return (value: T[K]) => { + setValue(field, value) + clearFieldError(field) + } + }, [setValue, clearFieldError]) + + const handleBlur = useCallback((field: K) => { + return () => { + touchField(field) + } + }, [touchField]) + + const reset = useCallback(() => { + setValues(initialState) + setErrors({}) + setTouched({}) + setIsDirty(false) + }, [initialState]) + + const setAllValues = useCallback((newValues: T) => { + setValues(newValues) + setIsDirty(true) + }, []) + + const hasErrors = Object.keys(errors).length > 0 + + return { + values, + errors, + touched, + isDirty, + hasErrors, + setValue, + setFieldError, + clearFieldError, + touchField, + handleChange, + handleBlur, + reset, + setAllValues + } +} diff --git a/src/hooks/use-interval.ts b/src/hooks/use-interval.ts new file mode 100644 index 0000000..2c5445f --- /dev/null +++ b/src/hooks/use-interval.ts @@ -0,0 +1,16 @@ +import { useEffect, useRef } from 'react' + +export function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback) + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + if (delay === null) return + + const id = setInterval(() => savedCallback.current(), delay) + return () => clearInterval(id) + }, [delay]) +} diff --git a/src/hooks/use-map.ts b/src/hooks/use-map.ts new file mode 100644 index 0000000..7ed0236 --- /dev/null +++ b/src/hooks/use-map.ts @@ -0,0 +1,48 @@ +import { useState, useCallback } from 'react' + +export interface UseMapActions { + set: (key: K, value: V) => void + remove: (key: K) => void + clear: () => void + setAll: (entries: [K, V][]) => void +} + +export function useMap( + initialValue?: Map +): [Map, UseMapActions] { + const [map, setMap] = useState>(initialValue || new Map()) + + const set = useCallback((key: K, value: V) => { + setMap((prev) => { + const newMap = new Map(prev) + newMap.set(key, value) + return newMap + }) + }, []) + + const remove = useCallback((key: K) => { + setMap((prev) => { + const newMap = new Map(prev) + newMap.delete(key) + return newMap + }) + }, []) + + const clear = useCallback(() => { + setMap(new Map()) + }, []) + + const setAll = useCallback((entries: [K, V][]) => { + setMap(new Map(entries)) + }, []) + + return [ + map, + { + set, + remove, + clear, + setAll + } + ] +} diff --git a/src/hooks/use-multi-step-form.ts b/src/hooks/use-multi-step-form.ts new file mode 100644 index 0000000..89ae336 --- /dev/null +++ b/src/hooks/use-multi-step-form.ts @@ -0,0 +1,98 @@ +import { useState, useCallback } from 'react' + +export interface UseMultiStepFormOptions { + initialData: T + steps: string[] + onComplete?: (data: T) => void | Promise +} + +export function useMultiStepForm>({ + initialData, + steps, + onComplete +}: UseMultiStepFormOptions) { + const [currentStep, setCurrentStep] = useState(0) + const [formData, setFormData] = useState(initialData) + const [isSubmitting, setIsSubmitting] = useState(false) + const [errors, setErrors] = useState>>({}) + + const updateData = useCallback((updates: Partial) => { + setFormData(prev => ({ ...prev, ...updates })) + }, []) + + const setFieldError = useCallback((field: keyof T, error: string) => { + setErrors(prev => ({ ...prev, [field]: error })) + }, []) + + const clearFieldError = useCallback((field: keyof T) => { + setErrors(prev => { + const newErrors = { ...prev } + delete newErrors[field] + return newErrors + }) + }, []) + + const clearAllErrors = useCallback(() => { + setErrors({}) + }, []) + + const nextStep = useCallback(() => { + if (currentStep < steps.length - 1) { + setCurrentStep(prev => prev + 1) + clearAllErrors() + } + }, [currentStep, steps.length, clearAllErrors]) + + const prevStep = useCallback(() => { + if (currentStep > 0) { + setCurrentStep(prev => prev - 1) + clearAllErrors() + } + }, [currentStep, clearAllErrors]) + + const goToStep = useCallback((step: number) => { + if (step >= 0 && step < steps.length) { + setCurrentStep(step) + clearAllErrors() + } + }, [steps.length, clearAllErrors]) + + const handleSubmit = useCallback(async () => { + if (!onComplete) return + + setIsSubmitting(true) + try { + await onComplete(formData) + } finally { + setIsSubmitting(false) + } + }, [formData, onComplete]) + + const reset = useCallback(() => { + setCurrentStep(0) + setFormData(initialData) + setErrors({}) + setIsSubmitting(false) + }, [initialData]) + + return { + currentStep, + currentStepName: steps[currentStep], + formData, + errors, + isSubmitting, + isFirstStep: currentStep === 0, + isLastStep: currentStep === steps.length - 1, + totalSteps: steps.length, + progress: ((currentStep + 1) / steps.length) * 100, + updateData, + setFieldError, + clearFieldError, + clearAllErrors, + nextStep, + prevStep, + goToStep, + handleSubmit, + reset + } +} diff --git a/src/hooks/use-query-params.ts b/src/hooks/use-query-params.ts new file mode 100644 index 0000000..9a8e472 --- /dev/null +++ b/src/hooks/use-query-params.ts @@ -0,0 +1,64 @@ +import { useState, useCallback, useEffect } from 'react' + +export function useQueryParams>() { + const [params, setParams] = useState(() => { + const searchParams = new URLSearchParams(window.location.search) + const result = {} as T + searchParams.forEach((value, key) => { + result[key as keyof T] = value as T[keyof T] + }) + return result + }) + + useEffect(() => { + const handlePopState = () => { + const searchParams = new URLSearchParams(window.location.search) + const result = {} as T + searchParams.forEach((value, key) => { + result[key as keyof T] = value as T[keyof T] + }) + setParams(result) + } + + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + const updateParams = useCallback((updates: Partial) => { + const searchParams = new URLSearchParams(window.location.search) + + Object.entries(updates).forEach(([key, value]) => { + if (value === null || value === undefined || value === '') { + searchParams.delete(key) + } else { + searchParams.set(key, String(value)) + } + }) + + const newUrl = `${window.location.pathname}?${searchParams.toString()}` + window.history.pushState({}, '', newUrl) + + setParams(prev => { + const newParams = { ...prev } + Object.entries(updates).forEach(([key, value]) => { + if (value === null || value === undefined || value === '') { + delete newParams[key as keyof T] + } else { + newParams[key as keyof T] = value as T[keyof T] + } + }) + return newParams + }) + }, []) + + const clearParams = useCallback(() => { + window.history.pushState({}, '', window.location.pathname) + setParams({} as T) + }, []) + + return { + params, + updateParams, + clearParams + } +} diff --git a/src/hooks/use-set.ts b/src/hooks/use-set.ts new file mode 100644 index 0000000..0dc819f --- /dev/null +++ b/src/hooks/use-set.ts @@ -0,0 +1,58 @@ +import { useState, useCallback } from 'react' + +export interface UseSetActions { + add: (item: T) => void + remove: (item: T) => void + toggle: (item: T) => void + clear: () => void + has: (item: T) => boolean +} + +export function useSet( + initialValue?: Set +): [Set, UseSetActions] { + const [set, setSet] = useState>(initialValue || new Set()) + + const add = useCallback((item: T) => { + setSet((prev) => new Set(prev).add(item)) + }, []) + + const remove = useCallback((item: T) => { + setSet((prev) => { + const newSet = new Set(prev) + newSet.delete(item) + return newSet + }) + }, []) + + const toggle = useCallback((item: T) => { + setSet((prev) => { + const newSet = new Set(prev) + if (newSet.has(item)) { + newSet.delete(item) + } else { + newSet.add(item) + } + return newSet + }) + }, []) + + const clear = useCallback(() => { + setSet(new Set()) + }, []) + + const has = useCallback((item: T) => { + return set.has(item) + }, [set]) + + return [ + set, + { + add, + remove, + toggle, + clear, + has + } + ] +} diff --git a/src/hooks/use-steps.ts b/src/hooks/use-steps.ts new file mode 100644 index 0000000..c016ee2 --- /dev/null +++ b/src/hooks/use-steps.ts @@ -0,0 +1,40 @@ +import { useState, useCallback } from 'react' + +export interface UseStepsOptions { + initialStep?: number + totalSteps: number +} + +export function useSteps({ initialStep = 0, totalSteps }: UseStepsOptions) { + const [currentStep, setCurrentStep] = useState(initialStep) + + const nextStep = useCallback(() => { + setCurrentStep((prev) => Math.min(prev + 1, totalSteps - 1)) + }, [totalSteps]) + + const previousStep = useCallback(() => { + setCurrentStep((prev) => Math.max(prev - 1, 0)) + }, []) + + const goToStep = useCallback((step: number) => { + if (step >= 0 && step < totalSteps) { + setCurrentStep(step) + } + }, [totalSteps]) + + const reset = useCallback(() => { + setCurrentStep(initialStep) + }, [initialStep]) + + return { + currentStep, + isFirstStep: currentStep === 0, + isLastStep: currentStep === totalSteps - 1, + progress: ((currentStep + 1) / totalSteps) * 100, + nextStep, + previousStep, + goToStep, + reset, + setStep: setCurrentStep + } +} diff --git a/src/hooks/use-table.ts b/src/hooks/use-table.ts new file mode 100644 index 0000000..8c62e08 --- /dev/null +++ b/src/hooks/use-table.ts @@ -0,0 +1,106 @@ +import { useState, useMemo } from 'react' + +export interface UseTableOptions { + data: T[] + pageSize?: number + initialSort?: { + key: keyof T + direction: 'asc' | 'desc' + } +} + +export function useTable>({ + data, + pageSize = 10, + initialSort +}: UseTableOptions) { + const [page, setPage] = useState(1) + const [sortKey, setSortKey] = useState(initialSort?.key || null) + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(initialSort?.direction || 'asc') + const [filters, setFilters] = useState>>({}) + + const filteredData = useMemo(() => { + return data.filter(item => { + return Object.entries(filters).every(([key, value]) => { + if (!value) return true + const itemValue = item[key as keyof T] + if (typeof value === 'string') { + return String(itemValue).toLowerCase().includes(value.toLowerCase()) + } + return itemValue === value + }) + }) + }, [data, filters]) + + const sortedData = useMemo(() => { + if (!sortKey) return filteredData + + return [...filteredData].sort((a, b) => { + const aVal = a[sortKey] + const bVal = b[sortKey] + + if (aVal === bVal) return 0 + + let comparison = 0 + if (typeof aVal === 'number' && typeof bVal === 'number') { + comparison = aVal - bVal + } else { + comparison = String(aVal).localeCompare(String(bVal)) + } + + return sortDirection === 'asc' ? comparison : -comparison + }) + }, [filteredData, sortKey, sortDirection]) + + const paginatedData = useMemo(() => { + const start = (page - 1) * pageSize + return sortedData.slice(start, start + pageSize) + }, [sortedData, page, pageSize]) + + const totalPages = Math.ceil(sortedData.length / pageSize) + + const handleSort = (key: keyof T) => { + if (sortKey === key) { + setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc') + } else { + setSortKey(key) + setSortDirection('asc') + } + setPage(1) + } + + const handleFilter = (key: keyof T, value: any) => { + setFilters(prev => ({ ...prev, [key]: value })) + setPage(1) + } + + const clearFilters = () => { + setFilters({}) + setPage(1) + } + + const goToPage = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setPage(newPage) + } + } + + return { + data: paginatedData, + page, + totalPages, + totalItems: sortedData.length, + pageSize, + sortKey, + sortDirection, + filters, + handleSort, + handleFilter, + clearFilters, + goToPage, + nextPage: () => goToPage(page + 1), + prevPage: () => goToPage(page - 1), + goToFirstPage: () => goToPage(1), + goToLastPage: () => goToPage(totalPages) + } +} diff --git a/src/hooks/use-timeout.ts b/src/hooks/use-timeout.ts new file mode 100644 index 0000000..41d8041 --- /dev/null +++ b/src/hooks/use-timeout.ts @@ -0,0 +1,16 @@ +import { useEffect, useRef } from 'react' + +export function useTimeout(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback) + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + if (delay === null) return + + const id = setTimeout(() => savedCallback.current(), delay) + return () => clearTimeout(id) + }, [delay]) +} diff --git a/src/hooks/use-undo.ts b/src/hooks/use-undo.ts new file mode 100644 index 0000000..0ed46f3 --- /dev/null +++ b/src/hooks/use-undo.ts @@ -0,0 +1,66 @@ +import { useState, useCallback } from 'react' + +export function useUndo(initialState: T, maxHistory = 50) { + const [history, setHistory] = useState([initialState]) + const [currentIndex, setCurrentIndex] = useState(0) + + const current = history[currentIndex] + const canUndo = currentIndex > 0 + const canRedo = currentIndex < history.length - 1 + + const setState = useCallback((newState: T | ((prev: T) => T)) => { + setHistory(prevHistory => { + const currentState = prevHistory[currentIndex] + const nextState = typeof newState === 'function' + ? (newState as (prev: T) => T)(currentState) + : newState + + const newHistory = prevHistory.slice(0, currentIndex + 1) + newHistory.push(nextState) + + if (newHistory.length > maxHistory) { + newHistory.shift() + setCurrentIndex(currentIndex) + } else { + setCurrentIndex(currentIndex + 1) + } + + return newHistory + }) + }, [currentIndex, maxHistory]) + + const undo = useCallback(() => { + if (canUndo) { + setCurrentIndex(prev => prev - 1) + } + }, [canUndo]) + + const redo = useCallback(() => { + if (canRedo) { + setCurrentIndex(prev => prev + 1) + } + }, [canRedo]) + + const reset = useCallback(() => { + setHistory([initialState]) + setCurrentIndex(0) + }, [initialState]) + + const clear = useCallback(() => { + setHistory([current]) + setCurrentIndex(0) + }, [current]) + + return { + state: current, + setState, + undo, + redo, + canUndo, + canRedo, + reset, + clear, + history: history.length, + currentIndex + } +}