Generated by Spark: Load more of UI from JSON declarations and break up large components into atomic and create hooks as needed

This commit is contained in:
2026-01-17 11:38:28 +00:00
committed by GitHub
parent 19a44c4010
commit 3ae07db657
28 changed files with 2227 additions and 465 deletions
+11 -10
View File
@@ -1,13 +1,14 @@
export { useJSONData } from './use-json-data'
export { useDataSources } from './use-data-sources'
export { useKVDataSource, useComputedDataSource, useStaticDataSource, useMultipleDataSources } from './use-data-source'
export { useCRUD } from './use-crud'
export { useSearch } from './use-search'
export { useSearchFilter } from './use-search-filter'
export { useSort } from './use-sort'
export { useFilter } from './use-filter'
export { useLocalStorage } from './use-local-storage'
export { usePagination } from './use-pagination'
export type { UseJSONDataOptions } from './use-json-data'
export type { UseCRUDOptions } from './use-crud'
export type { UseSearchOptions } from './use-search'
export type { UseSortOptions, SortDirection } from './use-sort'
export type { FilterConfig, UseFilterOptions } from './use-filter'
export { useSelection } from './use-selection'
export { useSeedData } from './use-seed-data'
export type { DataSourceConfig, DataSourceType } from './use-data-source'
export type { CRUDOperations, CRUDConfig } from './use-crud'
export type { SearchFilterConfig } from './use-search-filter'
export type { SortConfig, SortDirection } from './use-sort'
export type { PaginationConfig } from './use-pagination'
export type { SelectionConfig } from './use-selection'
+30 -34
View File
@@ -1,55 +1,51 @@
import { useState, useCallback } from 'react'
import { useKV } from '@github/spark/hooks'
import { useCallback } from 'react'
export interface UseCRUDOptions<T> {
key: string
defaultValue?: T[]
persist?: boolean
getId?: (item: T) => string | number
export interface CRUDOperations<T> {
create: (item: T) => void
read: (id: string | number) => T | undefined
update: (id: string | number, updates: Partial<T>) => void
delete: (id: string | number) => void
list: () => T[]
}
export function useCRUD<T>(options: UseCRUDOptions<T>) {
const { key, defaultValue = [], persist = true, getId = (item: any) => item.id } = options
const [persistedItems, setPersistedItems] = useKV<T[]>(key, defaultValue)
const [localItems, setLocalItems] = useState<T[]>(defaultValue)
const items = persist ? persistedItems : localItems
const setItems = persist ? setPersistedItems : setLocalItems
export interface CRUDConfig<T> {
items: T[]
setItems: (updater: (items: T[]) => T[]) => void
idField?: keyof T
}
export function useCRUD<T extends Record<string, any>>({
items,
setItems,
idField = 'id' as keyof T,
}: CRUDConfig<T>): CRUDOperations<T> {
const create = useCallback((item: T) => {
setItems((current: T[]) => [...current, item])
setItems(current => [...current, item])
}, [setItems])
const read = useCallback((id: string | number): T | undefined => {
return items.find(item => getId(item) === id)
}, [items, getId])
const read = useCallback((id: string | number) => {
return items.find(item => item[idField] === id)
}, [items, idField])
const update = useCallback((id: string | number, updates: Partial<T>) => {
setItems((current: T[]) =>
setItems(current =>
current.map(item =>
getId(item) === id ? { ...item, ...updates } : item
item[idField] === id ? { ...item, ...updates } : item
)
)
}, [setItems, getId])
}, [setItems, idField])
const remove = useCallback((id: string | number) => {
setItems((current: T[]) =>
current.filter(item => getId(item) !== id)
)
}, [setItems, getId])
const deleteItem = useCallback((id: string | number) => {
setItems(current => current.filter(item => item[idField] !== id))
}, [setItems, idField])
const clear = useCallback(() => {
setItems([])
}, [setItems])
const list = useCallback(() => items, [items])
return {
items,
create,
read,
update,
remove,
clear,
setItems,
delete: deleteItem,
list,
}
}
+81
View File
@@ -0,0 +1,81 @@
import { useState, useEffect, useCallback } from 'react'
import { useKV } from '@github/spark/hooks'
export type DataSourceType = 'kv' | 'static' | 'computed'
export interface DataSourceConfig<T = any> {
id: string
type: DataSourceType
key?: string
defaultValue?: T
compute?: (allData: Record<string, any>) => T
dependencies?: string[]
}
export function useKVDataSource<T>(key: string, defaultValue: T) {
const [value, setValue, deleteValue] = useKV<T>(key, defaultValue)
return {
data: value,
setData: setValue,
deleteData: deleteValue,
isLoading: false,
error: null,
}
}
export function useComputedDataSource<T>(
compute: (allData: Record<string, any>) => T,
allData: Record<string, any>,
dependencies: string[],
defaultValue?: T
) {
const [computed, setComputed] = useState<T>(defaultValue as T)
useEffect(() => {
try {
const newValue = compute(allData)
setComputed(newValue)
} catch (error) {
console.error('Error computing data source:', error)
}
}, dependencies.map(dep => allData[dep]))
return {
data: computed,
setData: () => {},
deleteData: () => {},
isLoading: false,
error: null,
}
}
export function useStaticDataSource<T>(value: T) {
return {
data: value,
setData: () => {},
deleteData: () => {},
isLoading: false,
error: null,
}
}
export function useMultipleDataSources(
configs: DataSourceConfig[],
onUpdate?: (data: Record<string, any>) => void
) {
const [allData, setAllData] = useState<Record<string, any>>({})
const updateData = useCallback((id: string, value: any) => {
setAllData(prev => {
const next = { ...prev, [id]: value }
onUpdate?.(next)
return next
})
}, [onUpdate])
return {
allData,
updateData,
}
}
+30 -27
View File
@@ -1,52 +1,55 @@
import { useState, useCallback, useMemo } from 'react'
import { useState, useMemo, useCallback } from 'react'
export interface PaginationConfig {
page: number
pageSize: number
total: number
items: any[]
pageSize?: number
initialPage?: number
}
export function usePagination<T>(items: T[], initialPageSize: number = 10) {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(initialPageSize)
export function usePagination<T>({
items,
pageSize = 10,
initialPage = 1,
}: PaginationConfig) {
const [currentPage, setCurrentPage] = useState(initialPage)
const total = items.length
const totalPages = Math.ceil(total / pageSize)
const totalPages = Math.ceil(items.length / pageSize)
const paginatedItems = useMemo(() => {
const start = (page - 1) * pageSize
const start = (currentPage - 1) * pageSize
const end = start + pageSize
return items.slice(start, end)
}, [items, page, pageSize])
}, [items, currentPage, pageSize])
const goToPage = useCallback((newPage: number) => {
setPage(Math.max(1, Math.min(newPage, totalPages)))
const goToPage = useCallback((page: number) => {
setCurrentPage(Math.max(1, Math.min(page, totalPages)))
}, [totalPages])
const nextPage = useCallback(() => {
goToPage(page + 1)
}, [page, goToPage])
goToPage(currentPage + 1)
}, [currentPage, goToPage])
const prevPage = useCallback(() => {
goToPage(page - 1)
}, [page, goToPage])
goToPage(currentPage - 1)
}, [currentPage, goToPage])
const changePageSize = useCallback((newSize: number) => {
setPageSize(newSize)
setPage(1)
}, [])
const reset = useCallback(() => {
setCurrentPage(initialPage)
}, [initialPage])
return {
items: paginatedItems,
page,
pageSize,
total,
currentPage,
totalPages,
pageSize,
goToPage,
nextPage,
prevPage,
changePageSize,
hasNext: page < totalPages,
hasPrev: page > 1,
reset,
hasNext: currentPage < totalPages,
hasPrev: currentPage > 1,
startIndex: (currentPage - 1) * pageSize + 1,
endIndex: Math.min(currentPage * pageSize, items.length),
totalItems: items.length,
}
}
+56
View File
@@ -0,0 +1,56 @@
import { useState, useMemo, useCallback } from 'react'
export interface SearchFilterConfig<T> {
items: T[]
searchFields?: (keyof T)[]
filterFn?: (item: T, filters: Record<string, any>) => boolean
}
export function useSearchFilter<T extends Record<string, any>>({
items,
searchFields = [],
filterFn,
}: SearchFilterConfig<T>) {
const [searchQuery, setSearchQuery] = useState('')
const [filters, setFilters] = useState<Record<string, any>>({})
const filtered = useMemo(() => {
let result = items
if (searchQuery && searchFields.length > 0) {
const query = searchQuery.toLowerCase()
result = result.filter(item =>
searchFields.some(field => {
const value = item[field]
return String(value).toLowerCase().includes(query)
})
)
}
if (filterFn && Object.keys(filters).length > 0) {
result = result.filter(item => filterFn(item, filters))
}
return result
}, [items, searchQuery, searchFields, filters, filterFn])
const setFilter = useCallback((key: string, value: any) => {
setFilters(prev => ({ ...prev, [key]: value }))
}, [])
const clearFilters = useCallback(() => {
setSearchQuery('')
setFilters({})
}, [])
return {
searchQuery,
setSearchQuery,
filters,
setFilter,
clearFilters,
filtered,
count: filtered.length,
total: items.length,
}
}
+77
View File
@@ -0,0 +1,77 @@
import { useState, useCallback } from 'react'
export interface SelectionConfig<T> {
items: T[]
multiple?: boolean
idField?: keyof T
}
export function useSelection<T extends Record<string, any>>({
items,
multiple = false,
idField = 'id' as keyof T,
}: SelectionConfig<T>) {
const [selected, setSelected] = useState<Set<string | number>>(new Set())
const toggle = useCallback((id: string | number) => {
setSelected(prev => {
const next = new Set(prev)
if (next.has(id)) {
next.delete(id)
} else {
if (!multiple) {
next.clear()
}
next.add(id)
}
return next
})
}, [multiple])
const select = useCallback((id: string | number) => {
setSelected(prev => {
const next: Set<string | number> = multiple ? new Set(prev) : new Set<string | number>()
next.add(id)
return next
})
}, [multiple])
const deselect = useCallback((id: string | number) => {
setSelected(prev => {
const next = new Set(prev)
next.delete(id)
return next
})
}, [])
const selectAll = useCallback(() => {
if (multiple) {
setSelected(new Set(items.map(item => item[idField])))
}
}, [items, idField, multiple])
const deselectAll = useCallback(() => {
setSelected(new Set())
}, [])
const isSelected = useCallback((id: string | number) => {
return selected.has(id)
}, [selected])
const getSelected = useCallback(() => {
return items.filter(item => selected.has(item[idField]))
}, [items, selected, idField])
return {
selected,
toggle,
select,
deselect,
selectAll,
deselectAll,
isSelected,
getSelected,
count: selected.size,
hasSelection: selected.size > 0,
}
}
+32 -39
View File
@@ -1,61 +1,54 @@
import { useState, useMemo } from 'react'
import { useState, useMemo, useCallback } from 'react'
export type SortDirection = 'asc' | 'desc' | null
export type SortDirection = 'asc' | 'desc'
export interface UseSortOptions<T> {
export interface SortConfig<T> {
items: T[]
initialField?: keyof T
initialDirection?: SortDirection
defaultField?: keyof T
defaultDirection?: SortDirection
}
export function useSort<T>(options: UseSortOptions<T>) {
const { items, initialField, initialDirection = 'asc' } = options
const [field, setField] = useState<keyof T | null>(initialField || null)
const [direction, setDirection] = useState<SortDirection>(initialDirection)
export function useSort<T extends Record<string, any>>({
items,
defaultField,
defaultDirection = 'asc',
}: SortConfig<T>) {
const [sortField, setSortField] = useState<keyof T | undefined>(defaultField)
const [sortDirection, setSortDirection] = useState<SortDirection>(defaultDirection)
const sorted = useMemo(() => {
if (!field || !direction) return items
if (!sortField) return items
return [...items].sort((a, b) => {
const aVal = a[field]
const bVal = b[field]
const aVal = a[sortField]
const bVal = b[sortField]
if (aVal == null && bVal == null) return 0
if (aVal == null) return 1
if (bVal == null) return -1
if (aVal === bVal) return 0
if (aVal < bVal) return direction === 'asc' ? -1 : 1
if (aVal > bVal) return direction === 'asc' ? 1 : -1
return 0
const comparison = aVal < bVal ? -1 : 1
return sortDirection === 'asc' ? comparison : -comparison
})
}, [items, field, direction])
}, [items, sortField, sortDirection])
const toggleSort = (newField: keyof T) => {
if (field === newField) {
setDirection(prev =>
prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'
)
if (direction === 'desc') {
setField(null)
}
const toggleSort = useCallback((field: keyof T) => {
if (sortField === field) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
} else {
setField(newField)
setDirection('asc')
setSortField(field)
setSortDirection('asc')
}
}
}, [sortField])
const reset = () => {
setField(null)
setDirection(null)
}
const resetSort = useCallback(() => {
setSortField(defaultField)
setSortDirection(defaultDirection)
}, [defaultField, defaultDirection])
return {
sorted,
field,
direction,
sortField,
sortDirection,
toggleSort,
setField,
setDirection,
reset,
resetSort,
}
}