mirror of
https://github.com/johndoe6345789/workforce-pay-bill-p.git
synced 2026-04-24 13:24:57 +00:00
Generated by Spark: Expand custom hook library, expand ui component library
This commit is contained in:
227
LIBRARIES.md
Normal file
227
LIBRARIES.md
Normal file
@@ -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
|
||||
<PageHeader>
|
||||
<PageHeaderRow>
|
||||
<PageTitle>Title</PageTitle>
|
||||
<PageActions>
|
||||
<Button>Action</Button>
|
||||
</PageActions>
|
||||
</PageHeaderRow>
|
||||
</PageHeader>
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -38,6 +38,23 @@ Metric display card with optional trend indicator.
|
||||
/>
|
||||
```
|
||||
|
||||
#### MetricCard
|
||||
Flexible metric card with composable parts.
|
||||
|
||||
```tsx
|
||||
<MetricCard>
|
||||
<MetricCardHeader>
|
||||
<MetricCardTitle>Active Workers</MetricCardTitle>
|
||||
<MetricCardIcon><Users /></MetricCardIcon>
|
||||
</MetricCardHeader>
|
||||
<MetricCardContent>
|
||||
<MetricCardValue>1,234</MetricCardValue>
|
||||
<MetricCardDescription>+12% from last month</MetricCardDescription>
|
||||
<MetricCardTrend trend="up">↑ 12%</MetricCardTrend>
|
||||
</MetricCardContent>
|
||||
</MetricCard>
|
||||
```
|
||||
|
||||
#### DataList
|
||||
Key-value pair display list.
|
||||
|
||||
@@ -52,6 +69,22 @@ Key-value pair display list.
|
||||
/>
|
||||
```
|
||||
|
||||
#### DataTable
|
||||
Generic data table with custom column rendering.
|
||||
|
||||
```tsx
|
||||
<DataTable
|
||||
columns={[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'status', header: 'Status', render: (val) => <StatusBadge status={val} /> },
|
||||
{ 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
|
||||
<List variant="bordered">
|
||||
<ListItem interactive onClick={handleClick}>
|
||||
<ListItemTitle>John Smith</ListItemTitle>
|
||||
<ListItemDescription>Software Engineer</ListItemDescription>
|
||||
</ListItem>
|
||||
</List>
|
||||
```
|
||||
|
||||
### Layout Components
|
||||
|
||||
#### PageHeader
|
||||
Page header with title, description, and actions.
|
||||
|
||||
```tsx
|
||||
<PageHeader>
|
||||
<PageHeaderRow>
|
||||
<div>
|
||||
<PageTitle>Timesheets</PageTitle>
|
||||
<PageDescription>Manage and approve worker timesheets</PageDescription>
|
||||
</div>
|
||||
<PageActions>
|
||||
<Button>Export</Button>
|
||||
<Button variant="primary">Create</Button>
|
||||
</PageActions>
|
||||
</PageHeaderRow>
|
||||
</PageHeader>
|
||||
```
|
||||
|
||||
#### Section
|
||||
Content section with header.
|
||||
|
||||
```tsx
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Recent Activity</SectionTitle>
|
||||
<SectionDescription>Your latest updates and changes</SectionDescription>
|
||||
</SectionHeader>
|
||||
<SectionContent>
|
||||
{/* content */}
|
||||
</SectionContent>
|
||||
</Section>
|
||||
```
|
||||
|
||||
#### Stack
|
||||
Flexible container for arranging items with consistent spacing.
|
||||
|
||||
```tsx
|
||||
<Stack direction="vertical" spacing="md" align="center">
|
||||
<Button>Item 1</Button>
|
||||
<Button>Item 2</Button>
|
||||
<Button>Item 3</Button>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="horizontal" spacing="sm" justify="between">
|
||||
<span>Left</span>
|
||||
<span>Right</span>
|
||||
</Stack>
|
||||
```
|
||||
|
||||
#### Grid
|
||||
Responsive grid layout.
|
||||
|
||||
```tsx
|
||||
<Grid cols={3} gap="lg">
|
||||
<Card>Card 1</Card>
|
||||
<Card>Card 2</Card>
|
||||
<Card>Card 3</Card>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
### Input Components
|
||||
|
||||
#### SearchInput
|
||||
@@ -91,6 +198,36 @@ Drag-and-drop file upload area.
|
||||
/>
|
||||
```
|
||||
|
||||
#### FilterBar
|
||||
Container for filter controls.
|
||||
|
||||
```tsx
|
||||
<FilterBar>
|
||||
<FilterGroup label="Status">
|
||||
<Select value={status} onValueChange={setStatus}>
|
||||
<option value="all">All</option>
|
||||
<option value="pending">Pending</option>
|
||||
</Select>
|
||||
</FilterGroup>
|
||||
<FilterGroup label="Date Range">
|
||||
<DatePicker />
|
||||
</FilterGroup>
|
||||
</FilterBar>
|
||||
```
|
||||
|
||||
#### Tag
|
||||
Removable tag component with variants.
|
||||
|
||||
```tsx
|
||||
<TagGroup>
|
||||
<Tag variant="primary" onRemove={() => removeTag('js')}>
|
||||
JavaScript
|
||||
</Tag>
|
||||
<Tag variant="success">Active</Tag>
|
||||
<Tag variant="warning">Pending</Tag>
|
||||
</TagGroup>
|
||||
```
|
||||
|
||||
### Navigation Components
|
||||
|
||||
#### Stepper
|
||||
@@ -108,6 +245,37 @@ Multi-step progress indicator.
|
||||
/>
|
||||
```
|
||||
|
||||
#### QuickPagination
|
||||
Simple pagination controls.
|
||||
|
||||
```tsx
|
||||
<QuickPagination
|
||||
currentPage={page}
|
||||
totalPages={10}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
```
|
||||
|
||||
### Modal Components
|
||||
|
||||
#### Modal
|
||||
Flexible modal dialog with composable parts.
|
||||
|
||||
```tsx
|
||||
<Modal isOpen={isOpen} onClose={close} size="lg">
|
||||
<ModalHeader onClose={close}>
|
||||
<ModalTitle>Edit Timesheet</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
{/* form content */}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="ghost" onClick={close}>Cancel</Button>
|
||||
<Button variant="primary" onClick={save}>Save</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
```
|
||||
|
||||
### Utility Components
|
||||
|
||||
#### LoadingSpinner
|
||||
|
||||
77
src/components/ui/data-table.tsx
Normal file
77
src/components/ui/data-table.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface DataTableProps<T> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
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<T extends Record<string, any>>({
|
||||
columns,
|
||||
data,
|
||||
onRowClick,
|
||||
emptyMessage = 'No data available',
|
||||
className,
|
||||
...props
|
||||
}: DataTableProps<T>) {
|
||||
return (
|
||||
<div className={cn('rounded-md border border-border overflow-hidden', className)} {...props}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted">
|
||||
<tr>
|
||||
{columns.map((column) => (
|
||||
<th
|
||||
key={String(column.key)}
|
||||
className="px-4 py-3 text-left text-sm font-medium text-muted-foreground"
|
||||
style={{ width: column.width }}
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="px-4 py-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
className={cn(
|
||||
'bg-card hover:bg-muted/50 transition-colors',
|
||||
onRowClick && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<td key={String(column.key)} className="px-4 py-3 text-sm">
|
||||
{column.render
|
||||
? column.render(row[column.key], row)
|
||||
: String(row[column.key] ?? '')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
38
src/components/ui/filter-bar.tsx
Normal file
38
src/components/ui/filter-bar.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface FilterBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function FilterBar({ children, className, ...props }: FilterBarProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap items-center gap-3 p-4 bg-muted/50 rounded-lg border border-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface FilterGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
label?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function FilterGroup({ label, children, className, ...props }: FilterGroupProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-1.5', className)} {...props}>
|
||||
{label && (
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
src/components/ui/grid.tsx
Normal file
39
src/components/ui/grid.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface GridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
'grid',
|
||||
cols === 1 && 'grid-cols-1',
|
||||
cols === 2 && 'grid-cols-1 md:grid-cols-2',
|
||||
cols === 3 && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
cols === 4 && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
||||
cols === 5 && 'grid-cols-1 md:grid-cols-3 lg:grid-cols-5',
|
||||
cols === 6 && 'grid-cols-1 md:grid-cols-3 lg:grid-cols-6',
|
||||
gap === 'none' && 'gap-0',
|
||||
gap === 'sm' && 'gap-2',
|
||||
gap === 'md' && 'gap-4',
|
||||
gap === 'lg' && 'gap-6',
|
||||
gap === 'xl' && 'gap-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
src/components/ui/list.tsx
Normal file
67
src/components/ui/list.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface ListProps extends React.HTMLAttributes<HTMLUListElement> {
|
||||
children: React.ReactNode
|
||||
variant?: 'default' | 'bordered' | 'divided'
|
||||
}
|
||||
|
||||
export function List({ children, variant = 'default', className, ...props }: ListProps) {
|
||||
return (
|
||||
<ul
|
||||
className={cn(
|
||||
'space-y-0',
|
||||
variant === 'bordered' && 'border border-border rounded-lg overflow-hidden',
|
||||
variant === 'divided' && 'divide-y divide-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
|
||||
children: React.ReactNode
|
||||
interactive?: boolean
|
||||
}
|
||||
|
||||
export function ListItem({ children, interactive, className, ...props }: ListItemProps) {
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
'px-4 py-3 bg-card',
|
||||
interactive && 'hover:bg-muted/50 cursor-pointer transition-colors',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ListItemTitleProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ListItemTitle({ children, className, ...props }: ListItemTitleProps) {
|
||||
return (
|
||||
<div className={cn('font-medium text-foreground', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ListItemDescriptionProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ListItemDescription({ children, className, ...props }: ListItemDescriptionProps) {
|
||||
return (
|
||||
<div className={cn('text-sm text-muted-foreground mt-1', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
114
src/components/ui/metric-card.tsx
Normal file
114
src/components/ui/metric-card.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const MetricCard = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border border-border bg-card p-6 shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCard.displayName = 'MetricCard'
|
||||
|
||||
const MetricCardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center justify-between mb-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardHeader.displayName = 'MetricCardHeader'
|
||||
|
||||
const MetricCardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm font-medium text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardTitle.displayName = 'MetricCardTitle'
|
||||
|
||||
const MetricCardIcon = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardIcon.displayName = 'MetricCardIcon'
|
||||
|
||||
const MetricCardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('', className)} {...props} />
|
||||
))
|
||||
MetricCardContent.displayName = 'MetricCardContent'
|
||||
|
||||
const MetricCardValue = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-3xl font-bold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardValue.displayName = 'MetricCardValue'
|
||||
|
||||
const MetricCardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-xs text-muted-foreground mt-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardDescription.displayName = 'MetricCardDescription'
|
||||
|
||||
const MetricCardTrend = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { trend?: 'up' | 'down' | 'neutral' }
|
||||
>(({ className, trend = 'neutral', ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 text-xs font-medium mt-2',
|
||||
trend === 'up' && 'text-success',
|
||||
trend === 'down' && 'text-destructive',
|
||||
trend === 'neutral' && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MetricCardTrend.displayName = 'MetricCardTrend'
|
||||
|
||||
export {
|
||||
MetricCard,
|
||||
MetricCardHeader,
|
||||
MetricCardTitle,
|
||||
MetricCardIcon,
|
||||
MetricCardContent,
|
||||
MetricCardValue,
|
||||
MetricCardDescription,
|
||||
MetricCardTrend
|
||||
}
|
||||
110
src/components/ui/modal.tsx
Normal file
110
src/components/ui/modal.tsx
Normal file
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 bg-card rounded-lg shadow-lg max-h-[90vh] overflow-auto',
|
||||
size === 'sm' && 'w-full max-w-sm',
|
||||
size === 'md' && 'w-full max-w-md',
|
||||
size === 'lg' && 'w-full max-w-lg',
|
||||
size === 'xl' && 'w-full max-w-xl',
|
||||
size === 'full' && 'w-[calc(100%-2rem)] max-w-6xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function ModalHeader({ children, onClose, className, ...props }: ModalHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between px-6 py-4 border-b border-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex-1">{children}</div>
|
||||
{onClose && (
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ModalTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ModalTitle({ children, className, ...props }: ModalTitleProps) {
|
||||
return (
|
||||
<h2 className={cn('text-xl font-semibold text-foreground', className)} {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ModalBody({ children, className, ...props }: ModalBodyProps) {
|
||||
return (
|
||||
<div className={cn('px-6 py-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ModalFooter({ children, className, ...props }: ModalFooterProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-end gap-2 px-6 py-4 border-t border-border',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
src/components/ui/page-header.tsx
Normal file
62
src/components/ui/page-header.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageHeader({ children, className, ...props }: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn('space-y-2 mb-6', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface PageTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageTitle({ children, className, ...props }: PageTitleProps) {
|
||||
return (
|
||||
<h1 className={cn('text-3xl font-bold text-foreground', className)} {...props}>
|
||||
{children}
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
|
||||
export interface PageDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageDescription({ children, className, ...props }: PageDescriptionProps) {
|
||||
return (
|
||||
<p className={cn('text-muted-foreground', className)} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export interface PageActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageActions({ children, className, ...props }: PageActionsProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface PageHeaderRowProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageHeaderRow({ children, className, ...props }: PageHeaderRowProps) {
|
||||
return (
|
||||
<div className={cn('flex items-start justify-between gap-4', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
49
src/components/ui/quick-pagination.tsx
Normal file
49
src/components/ui/quick-pagination.tsx
Normal file
@@ -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 (
|
||||
<div className={cn('flex items-center justify-between gap-2', className)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={!canGoPrevious}
|
||||
>
|
||||
<CaretLeft className="h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={!canGoNext}
|
||||
>
|
||||
Next
|
||||
<CaretRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
src/components/ui/section.tsx
Normal file
62
src/components/ui/section.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface SectionProps extends React.HTMLAttributes<HTMLElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function Section({ children, className, ...props }: SectionProps) {
|
||||
return (
|
||||
<section className={cn('space-y-4', className)} {...props}>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SectionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SectionHeader({ children, className, ...props }: SectionHeaderProps) {
|
||||
return (
|
||||
<div className={cn('', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SectionTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SectionTitle({ children, className, ...props }: SectionTitleProps) {
|
||||
return (
|
||||
<h2 className={cn('text-lg font-semibold text-foreground', className)} {...props}>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SectionDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SectionDescription({ children, className, ...props }: SectionDescriptionProps) {
|
||||
return (
|
||||
<p className={cn('text-sm text-muted-foreground mt-1', className)} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SectionContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function SectionContent({ children, className, ...props }: SectionContentProps) {
|
||||
return (
|
||||
<div className={cn('', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
src/components/ui/stack.tsx
Normal file
47
src/components/ui/stack.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex',
|
||||
direction === 'horizontal' ? 'flex-row' : 'flex-col',
|
||||
spacing === 'none' && 'gap-0',
|
||||
spacing === 'sm' && 'gap-2',
|
||||
spacing === 'md' && 'gap-4',
|
||||
spacing === 'lg' && 'gap-6',
|
||||
spacing === 'xl' && 'gap-8',
|
||||
align === 'start' && 'items-start',
|
||||
align === 'center' && 'items-center',
|
||||
align === 'end' && 'items-end',
|
||||
align === 'stretch' && 'items-stretch',
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
justify === 'end' && 'justify-end',
|
||||
justify === 'between' && 'justify-between',
|
||||
justify === 'around' && 'justify-around',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
56
src/components/ui/tag.tsx
Normal file
56
src/components/ui/tag.tsx
Normal file
@@ -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<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
onRemove?: () => void
|
||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'destructive'
|
||||
}
|
||||
|
||||
export function Tag({
|
||||
children,
|
||||
onRemove,
|
||||
variant = 'default',
|
||||
className,
|
||||
...props
|
||||
}: TagProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium',
|
||||
variant === 'default' && 'bg-muted text-foreground',
|
||||
variant === 'primary' && 'bg-primary/10 text-primary',
|
||||
variant === 'success' && 'bg-success/10 text-success',
|
||||
variant === 'warning' && 'bg-warning/10 text-warning',
|
||||
variant === 'destructive' && 'bg-destructive/10 text-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span>{children}</span>
|
||||
{onRemove && (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className="hover:opacity-70 transition-opacity"
|
||||
aria-label="Remove"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TagGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function TagGroup({ children, className, ...props }: TagGroupProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-wrap items-center gap-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
<Button onClick={() => copy('Text to copy')}>
|
||||
{isCopied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
### useDisclosure
|
||||
```tsx
|
||||
import { useDisclosure } from '@/hooks'
|
||||
|
||||
const { isOpen, open, close, toggle } = useDisclosure()
|
||||
|
||||
<Button onClick={open}>Open Modal</Button>
|
||||
<Dialog open={isOpen} onOpenChange={close}>
|
||||
...
|
||||
</Dialog>
|
||||
```
|
||||
|
||||
### useFocusTrap
|
||||
```tsx
|
||||
import { useFocusTrap } from '@/hooks'
|
||||
|
||||
const ref = useFocusTrap<HTMLDivElement>(isModalOpen)
|
||||
|
||||
<div ref={ref}>
|
||||
{/* Focus will be trapped within this element */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
@@ -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'
|
||||
|
||||
59
src/hooks/use-array.ts
Normal file
59
src/hooks/use-array.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export function useArray<T>(initialValue: T[] = []) {
|
||||
const [array, setArray] = useState<T[]>(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
|
||||
}
|
||||
}
|
||||
29
src/hooks/use-clipboard.ts
Normal file
29
src/hooks/use-clipboard.ts
Normal file
@@ -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 }
|
||||
}
|
||||
61
src/hooks/use-confirmation.ts
Normal file
61
src/hooks/use-confirmation.ts
Normal file
@@ -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<ConfirmationState>({
|
||||
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<boolean>((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
|
||||
}
|
||||
}
|
||||
39
src/hooks/use-countdown.ts
Normal file
39
src/hooks/use-countdown.ts
Normal file
@@ -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
|
||||
}
|
||||
17
src/hooks/use-disclosure.ts
Normal file
17
src/hooks/use-disclosure.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
65
src/hooks/use-download.ts
Normal file
65
src/hooks/use-download.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
42
src/hooks/use-focus-trap.ts
Normal file
42
src/hooks/use-focus-trap.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export function useFocusTrap<T extends HTMLElement = HTMLElement>(active = true) {
|
||||
const ref = useRef<T>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !ref.current) return
|
||||
|
||||
const element = ref.current
|
||||
const focusableElements = element.querySelectorAll<HTMLElement>(
|
||||
'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
|
||||
}
|
||||
72
src/hooks/use-form-state.ts
Normal file
72
src/hooks/use-form-state.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export function useFormState<T extends Record<string, any>>(initialState: T) {
|
||||
const [values, setValues] = useState<T>(initialState)
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
|
||||
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({})
|
||||
const [isDirty, setIsDirty] = useState(false)
|
||||
|
||||
const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
|
||||
setValues(prev => ({ ...prev, [field]: value }))
|
||||
setIsDirty(true)
|
||||
}, [])
|
||||
|
||||
const setFieldError = useCallback(<K extends keyof T>(field: K, error: string) => {
|
||||
setErrors(prev => ({ ...prev, [field]: error }))
|
||||
}, [])
|
||||
|
||||
const clearFieldError = useCallback(<K extends keyof T>(field: K) => {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors[field]
|
||||
return newErrors
|
||||
})
|
||||
}, [])
|
||||
|
||||
const touchField = useCallback(<K extends keyof T>(field: K) => {
|
||||
setTouched(prev => ({ ...prev, [field]: true }))
|
||||
}, [])
|
||||
|
||||
const handleChange = useCallback(<K extends keyof T>(field: K) => {
|
||||
return (value: T[K]) => {
|
||||
setValue(field, value)
|
||||
clearFieldError(field)
|
||||
}
|
||||
}, [setValue, clearFieldError])
|
||||
|
||||
const handleBlur = useCallback(<K extends keyof T>(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
|
||||
}
|
||||
}
|
||||
16
src/hooks/use-interval.ts
Normal file
16
src/hooks/use-interval.ts
Normal file
@@ -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])
|
||||
}
|
||||
48
src/hooks/use-map.ts
Normal file
48
src/hooks/use-map.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export interface UseMapActions<K, V> {
|
||||
set: (key: K, value: V) => void
|
||||
remove: (key: K) => void
|
||||
clear: () => void
|
||||
setAll: (entries: [K, V][]) => void
|
||||
}
|
||||
|
||||
export function useMap<K, V>(
|
||||
initialValue?: Map<K, V>
|
||||
): [Map<K, V>, UseMapActions<K, V>] {
|
||||
const [map, setMap] = useState<Map<K, V>>(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
|
||||
}
|
||||
]
|
||||
}
|
||||
98
src/hooks/use-multi-step-form.ts
Normal file
98
src/hooks/use-multi-step-form.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export interface UseMultiStepFormOptions<T> {
|
||||
initialData: T
|
||||
steps: string[]
|
||||
onComplete?: (data: T) => void | Promise<void>
|
||||
}
|
||||
|
||||
export function useMultiStepForm<T extends Record<string, any>>({
|
||||
initialData,
|
||||
steps,
|
||||
onComplete
|
||||
}: UseMultiStepFormOptions<T>) {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [formData, setFormData] = useState<T>(initialData)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({})
|
||||
|
||||
const updateData = useCallback((updates: Partial<T>) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
64
src/hooks/use-query-params.ts
Normal file
64
src/hooks/use-query-params.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
|
||||
export function useQueryParams<T extends Record<string, string>>() {
|
||||
const [params, setParams] = useState<T>(() => {
|
||||
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<T>) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
58
src/hooks/use-set.ts
Normal file
58
src/hooks/use-set.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export interface UseSetActions<T> {
|
||||
add: (item: T) => void
|
||||
remove: (item: T) => void
|
||||
toggle: (item: T) => void
|
||||
clear: () => void
|
||||
has: (item: T) => boolean
|
||||
}
|
||||
|
||||
export function useSet<T>(
|
||||
initialValue?: Set<T>
|
||||
): [Set<T>, UseSetActions<T>] {
|
||||
const [set, setSet] = useState<Set<T>>(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
|
||||
}
|
||||
]
|
||||
}
|
||||
40
src/hooks/use-steps.ts
Normal file
40
src/hooks/use-steps.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
106
src/hooks/use-table.ts
Normal file
106
src/hooks/use-table.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
export interface UseTableOptions<T> {
|
||||
data: T[]
|
||||
pageSize?: number
|
||||
initialSort?: {
|
||||
key: keyof T
|
||||
direction: 'asc' | 'desc'
|
||||
}
|
||||
}
|
||||
|
||||
export function useTable<T extends Record<string, any>>({
|
||||
data,
|
||||
pageSize = 10,
|
||||
initialSort
|
||||
}: UseTableOptions<T>) {
|
||||
const [page, setPage] = useState(1)
|
||||
const [sortKey, setSortKey] = useState<keyof T | null>(initialSort?.key || null)
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(initialSort?.direction || 'asc')
|
||||
const [filters, setFilters] = useState<Partial<Record<keyof T, any>>>({})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
16
src/hooks/use-timeout.ts
Normal file
16
src/hooks/use-timeout.ts
Normal file
@@ -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])
|
||||
}
|
||||
66
src/hooks/use-undo.ts
Normal file
66
src/hooks/use-undo.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export function useUndo<T>(initialState: T, maxHistory = 50) {
|
||||
const [history, setHistory] = useState<T[]>([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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user