mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
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:
@@ -1,5 +1,6 @@
|
||||
import { useCRUD, useSearch, useFilter } from '@/hooks/data'
|
||||
import { useCRUD, useSearchFilter } from '@/hooks/data'
|
||||
import { useToggle, useDialog } from '@/hooks/ui'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
||||
import { SearchInput, DataCard, ActionBar } from '@/components/molecules'
|
||||
@@ -15,17 +16,15 @@ interface Task {
|
||||
}
|
||||
|
||||
export function AtomicComponentDemo() {
|
||||
const { items: tasks, create, remove } = useCRUD<Task>({
|
||||
key: 'demo-tasks',
|
||||
defaultValue: [
|
||||
{ id: 1, title: 'Build component library', status: 'active', priority: 'high' },
|
||||
{ id: 2, title: 'Write documentation', status: 'pending', priority: 'medium' },
|
||||
{ id: 3, title: 'Create examples', status: 'success', priority: 'low' },
|
||||
],
|
||||
persist: true,
|
||||
})
|
||||
const [tasks, setTasks] = useKV<Task[]>('demo-tasks', [
|
||||
{ id: 1, title: 'Build component library', status: 'active', priority: 'high' },
|
||||
{ id: 2, title: 'Write documentation', status: 'pending', priority: 'medium' },
|
||||
{ id: 3, title: 'Create examples', status: 'success', priority: 'low' },
|
||||
])
|
||||
|
||||
const { query, setQuery, filtered } = useSearch({
|
||||
const crud = useCRUD<Task>({ items: tasks, setItems: setTasks })
|
||||
|
||||
const { searchQuery: query, setSearchQuery: setQuery, filtered } = useSearchFilter({
|
||||
items: tasks,
|
||||
searchFields: ['title'],
|
||||
})
|
||||
|
||||
205
src/components/JSONPageRenderer.tsx
Normal file
205
src/components/JSONPageRenderer.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { StatCard } from '@/components/atoms'
|
||||
import * as Icons from '@phosphor-icons/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface PageComponentConfig {
|
||||
id: string
|
||||
type: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface PageLayoutConfig {
|
||||
type: string
|
||||
spacing?: string
|
||||
sections?: PageSectionConfig[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface PageSectionConfig {
|
||||
type: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface PageSchema {
|
||||
id: string
|
||||
layout: PageLayoutConfig
|
||||
dashboardCards?: any[]
|
||||
statCards?: any[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface ComponentRendererProps {
|
||||
schema: PageSchema
|
||||
data: Record<string, any>
|
||||
functions?: Record<string, (...args: any[]) => any>
|
||||
}
|
||||
|
||||
function resolveBinding(binding: string, data: Record<string, any>): any {
|
||||
try {
|
||||
const func = new Function(...Object.keys(data), `return ${binding}`)
|
||||
return func(...Object.values(data))
|
||||
} catch {
|
||||
return binding
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(iconName: string, props?: any) {
|
||||
const IconComponent = (Icons as any)[iconName]
|
||||
if (!IconComponent) return null
|
||||
return <IconComponent size={24} weight="duotone" {...props} />
|
||||
}
|
||||
|
||||
export function JSONPageRenderer({ schema, data, functions = {} }: ComponentRendererProps) {
|
||||
const renderSection = (section: PageSectionConfig, index: number): ReactNode => {
|
||||
switch (section.type) {
|
||||
case 'header':
|
||||
return (
|
||||
<div key={index}>
|
||||
<h1 className="text-3xl font-bold mb-2">{section.title}</h1>
|
||||
{section.description && (
|
||||
<p className="text-muted-foreground">{section.description}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'cards':
|
||||
const cards = schema[section.items as string] || []
|
||||
return (
|
||||
<div key={index} className={cn('space-y-' + (section.spacing || '4'))}>
|
||||
{cards.map((card: any) => renderCard(card))}
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'grid':
|
||||
const gridItems = schema[section.items as string] || []
|
||||
const { sm = 1, md = 2, lg = 3 } = section.columns || {}
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'grid gap-' + (section.gap || '4'),
|
||||
`grid-cols-${sm} md:grid-cols-${md} lg:grid-cols-${lg}`
|
||||
)}
|
||||
>
|
||||
{gridItems.map((item: any) => renderStatCard(item))}
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const renderCard = (card: any): ReactNode => {
|
||||
const icon = card.icon ? getIcon(card.icon) : null
|
||||
|
||||
if (card.type === 'gradient-card') {
|
||||
const computeFn = functions[card.dataSource?.compute]
|
||||
const computedData = computeFn ? computeFn(data) : {}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={card.id}
|
||||
className={cn(
|
||||
'bg-gradient-to-br border-primary/20',
|
||||
card.gradient
|
||||
)}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{icon && <span className="text-primary">{icon}</span>}
|
||||
{card.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{card.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{card.components?.map((comp: any, idx: number) =>
|
||||
renderSubComponent(comp, computedData, idx)
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card key={card.id}>
|
||||
<CardHeader>
|
||||
<CardTitle>{card.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{card.component && renderCustomComponent(card.component, card.props || {})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSubComponent = (comp: any, dataContext: any, key: number): ReactNode => {
|
||||
const value = dataContext[comp.binding]
|
||||
|
||||
switch (comp.type) {
|
||||
case 'metric':
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
<span className={cn(
|
||||
'font-bold',
|
||||
comp.size === 'large' ? 'text-4xl' : 'text-2xl'
|
||||
)}>
|
||||
{comp.format === 'percentage' ? `${value}%` : value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
case 'badge':
|
||||
const variant = value === 'ready' ? comp.variants?.ready : comp.variants?.inProgress
|
||||
return (
|
||||
<Badge key={key} variant={variant?.variant as any}>
|
||||
{variant?.label}
|
||||
</Badge>
|
||||
)
|
||||
|
||||
case 'progress':
|
||||
return <Progress key={key} value={value} className="h-3" />
|
||||
|
||||
case 'text':
|
||||
return (
|
||||
<p key={key} className={comp.className}>
|
||||
{value}
|
||||
</p>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const renderStatCard = (stat: any): ReactNode => {
|
||||
const icon = stat.icon ? getIcon(stat.icon) : undefined
|
||||
const value = resolveBinding(stat.dataBinding, data)
|
||||
const description = `${value} ${stat.description}`
|
||||
|
||||
return (
|
||||
<StatCard
|
||||
key={stat.id}
|
||||
icon={icon}
|
||||
title={stat.title}
|
||||
value={value}
|
||||
description={description}
|
||||
color={stat.color}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const renderCustomComponent = (componentName: string, props: any): ReactNode => {
|
||||
return <div>Custom component: {componentName}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6 space-y-6">
|
||||
{schema.layout.sections?.map((section, index) => renderSection(section, index))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
src/components/ProjectDashboard.new.tsx
Normal file
55
src/components/ProjectDashboard.new.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { JSONPageRenderer } from '@/components/JSONPageRenderer'
|
||||
import dashboardSchema from '@/config/pages/dashboard.json'
|
||||
import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest, FlaskConfig, NextJsConfig } from '@/types/project'
|
||||
|
||||
interface ProjectDashboardProps {
|
||||
files: ProjectFile[]
|
||||
models: PrismaModel[]
|
||||
components: ComponentNode[]
|
||||
theme: ThemeConfig
|
||||
playwrightTests: PlaywrightTest[]
|
||||
storybookStories: StorybookStory[]
|
||||
unitTests: UnitTest[]
|
||||
flaskConfig: FlaskConfig
|
||||
nextjsConfig: NextJsConfig
|
||||
}
|
||||
|
||||
function calculateCompletionScore(data: any) {
|
||||
const { files = [], models = [], components = [], playwrightTests = [], storybookStories = [], unitTests = [] } = data
|
||||
|
||||
const totalFiles = files.length
|
||||
const totalModels = models.length
|
||||
const totalComponents = components.length
|
||||
const totalTests = playwrightTests.length + storybookStories.length + unitTests.length
|
||||
|
||||
let score = 0
|
||||
if (totalFiles > 0) score += 30
|
||||
if (totalModels > 0) score += 20
|
||||
if (totalComponents > 0) score += 20
|
||||
if (totalTests > 0) score += 30
|
||||
|
||||
const completionScore = Math.min(score, 100)
|
||||
|
||||
return {
|
||||
completionScore,
|
||||
completionStatus: completionScore >= 70 ? 'ready' : 'inProgress',
|
||||
completionMessage: getCompletionMessage(completionScore)
|
||||
}
|
||||
}
|
||||
|
||||
function getCompletionMessage(score: number): string {
|
||||
if (score >= 90) return 'Excellent! Your project is production-ready.'
|
||||
if (score >= 70) return 'Great progress! Consider adding more tests.'
|
||||
if (score >= 50) return 'Good start! Keep building features.'
|
||||
return 'Just getting started. Add some components and models.'
|
||||
}
|
||||
|
||||
export function ProjectDashboard(props: ProjectDashboardProps) {
|
||||
return (
|
||||
<JSONPageRenderer
|
||||
schema={dashboardSchema as any}
|
||||
data={props}
|
||||
functions={{ calculateCompletionScore }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +1,51 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { forwardRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
interface ActionButtonProps {
|
||||
icon?: React.ReactNode
|
||||
label?: string
|
||||
variant?: 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive'
|
||||
export interface ActionButtonProps {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
onClick: () => void
|
||||
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
onClick?: () => void
|
||||
tooltip?: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
|
||||
({ icon, label, variant = 'default', size = 'default', onClick, disabled, loading, className }, ref) => {
|
||||
export function ActionButton({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
tooltip,
|
||||
disabled,
|
||||
className,
|
||||
}: ActionButtonProps) {
|
||||
const button = (
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{icon && <span className="mr-2">{icon}</span>}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
className={cn(className)}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : icon}
|
||||
{label && <span className={icon ? 'ml-2' : ''}>{label}</span>}
|
||||
</Button>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ActionButton.displayName = 'ActionButton'
|
||||
return button
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DataListProps<T> {
|
||||
items: T[]
|
||||
renderItem: (item: T, index: number) => React.ReactNode
|
||||
export interface DataListProps {
|
||||
items: any[]
|
||||
renderItem: (item: any, index: number) => ReactNode
|
||||
emptyMessage?: string
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
}
|
||||
|
||||
export function DataList<T>({
|
||||
items,
|
||||
renderItem,
|
||||
emptyMessage = 'No items to display',
|
||||
export function DataList({
|
||||
items,
|
||||
renderItem,
|
||||
emptyMessage = 'No items',
|
||||
className,
|
||||
itemClassName
|
||||
}: DataListProps<T>) {
|
||||
itemClassName,
|
||||
}: DataListProps) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
)
|
||||
@@ -26,7 +27,7 @@ export function DataList<T>({
|
||||
return (
|
||||
<div className={cn('space-y-2', className)}>
|
||||
{items.map((item, index) => (
|
||||
<div key={index} className={itemClassName}>
|
||||
<div key={index} className={cn('transition-colors', itemClassName)}>
|
||||
{renderItem(item, index)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
48
src/components/atoms/EmptyState.tsx
Normal file
48
src/components/atoms/EmptyState.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export interface EmptyStateProps {
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
action?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: EmptyStateProps) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col items-center justify-center gap-4 py-12 px-6 text-center',
|
||||
className
|
||||
)}>
|
||||
{icon && (
|
||||
<div className="text-muted-foreground text-4xl opacity-50">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{action && (
|
||||
<Button onClick={action.onClick} className="mt-2">
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
src/components/atoms/LoadingState.tsx
Normal file
31
src/components/atoms/LoadingState.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export interface LoadingStateProps {
|
||||
message?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LoadingState({
|
||||
message = 'Loading...',
|
||||
size = 'md',
|
||||
className
|
||||
}: LoadingStateProps) {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4 border-2',
|
||||
md: 'w-8 h-8 border-3',
|
||||
lg: 'w-12 h-12 border-4',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col items-center justify-center gap-3 py-8', className)}>
|
||||
<div className={cn(
|
||||
'border-primary border-t-transparent rounded-full animate-spin',
|
||||
sizeClasses[size]
|
||||
)} />
|
||||
{message && (
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,37 +1,50 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface StatCardProps {
|
||||
label: string
|
||||
export interface StatCardProps {
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
value: string | number
|
||||
icon?: React.ReactNode
|
||||
description?: string
|
||||
color?: string
|
||||
trend?: {
|
||||
value: number
|
||||
positive: boolean
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StatCard({ label, value, icon, trend, className }: StatCardProps) {
|
||||
export function StatCard({
|
||||
icon,
|
||||
title,
|
||||
value,
|
||||
description,
|
||||
color = 'text-primary',
|
||||
trend,
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<Card className={cn('bg-card/50 backdrop-blur', className)}>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">{label}</p>
|
||||
<p className="text-3xl font-bold">{value}</p>
|
||||
<Card className={cn('transition-all hover:shadow-lg', className)}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||
<p className="text-3xl font-bold mt-2">{value}</p>
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
{trend && (
|
||||
<p className={cn(
|
||||
'text-xs flex items-center gap-1',
|
||||
trend.positive ? 'text-green-600' : 'text-red-600'
|
||||
<div className={cn(
|
||||
'text-sm font-medium mt-2',
|
||||
trend.direction === 'up' ? 'text-green-600' : 'text-red-600'
|
||||
)}>
|
||||
<span>{trend.positive ? '↑' : '↓'}</span>
|
||||
<span>{Math.abs(trend.value)}%</span>
|
||||
</p>
|
||||
{trend.direction === 'up' ? '↑' : '↓'} {Math.abs(trend.value)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{icon && (
|
||||
<div className="text-muted-foreground">
|
||||
<div className={cn('text-2xl', color)}>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -19,4 +19,8 @@ export { List } from './List'
|
||||
export { Grid } from './Grid'
|
||||
export { DataSourceBadge } from './DataSourceBadge'
|
||||
export { BindingIndicator } from './BindingIndicator'
|
||||
export { StatCard } from './StatCard'
|
||||
export { LoadingState } from './LoadingState'
|
||||
export { EmptyState } from './EmptyState'
|
||||
|
||||
|
||||
|
||||
@@ -1,49 +1,120 @@
|
||||
{
|
||||
"id": "dashboard-json",
|
||||
"name": "Dashboard (JSON)",
|
||||
"description": "JSON-driven dashboard page",
|
||||
"icon": "ChartBar",
|
||||
"layout": {
|
||||
"type": "single"
|
||||
},
|
||||
"dataSources": [
|
||||
"dashboardCards": [
|
||||
{
|
||||
"id": "files",
|
||||
"type": "kv",
|
||||
"key": "project-files",
|
||||
"defaultValue": []
|
||||
"id": "completion",
|
||||
"type": "gradient-card",
|
||||
"title": "Project Completeness",
|
||||
"icon": "CheckCircle",
|
||||
"gradient": "from-primary/10 to-accent/10",
|
||||
"dataSource": {
|
||||
"type": "computed",
|
||||
"compute": "calculateCompletionScore"
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"type": "metric",
|
||||
"binding": "completionScore",
|
||||
"format": "percentage",
|
||||
"size": "large"
|
||||
},
|
||||
{
|
||||
"type": "badge",
|
||||
"binding": "completionStatus",
|
||||
"variants": {
|
||||
"ready": { "label": "Ready to Export", "variant": "default" },
|
||||
"inProgress": { "label": "In Progress", "variant": "secondary" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "progress",
|
||||
"binding": "completionScore"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"binding": "completionMessage",
|
||||
"className": "text-sm text-muted-foreground"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "models",
|
||||
"type": "kv",
|
||||
"key": "project-models",
|
||||
"defaultValue": []
|
||||
"id": "build-status",
|
||||
"type": "card",
|
||||
"title": "GitHub Build Status",
|
||||
"icon": "GitBranch",
|
||||
"component": "GitHubBuildStatus",
|
||||
"props": {}
|
||||
}
|
||||
],
|
||||
"statCards": [
|
||||
{
|
||||
"id": "code-files",
|
||||
"icon": "Code",
|
||||
"title": "Code Files",
|
||||
"dataBinding": "files.length",
|
||||
"description": "files in your project",
|
||||
"color": "text-blue-500"
|
||||
},
|
||||
{
|
||||
"id": "database-models",
|
||||
"icon": "Database",
|
||||
"title": "Database Models",
|
||||
"dataBinding": "models.length",
|
||||
"description": "Prisma schemas",
|
||||
"color": "text-purple-500"
|
||||
},
|
||||
{
|
||||
"id": "components",
|
||||
"type": "kv",
|
||||
"key": "project-components",
|
||||
"defaultValue": []
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"id": "dashboard-root",
|
||||
"type": "ProjectDashboard",
|
||||
"props": {},
|
||||
"dataBinding": "files"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "navigate-to-code",
|
||||
"type": "navigate",
|
||||
"target": "code"
|
||||
"icon": "Cube",
|
||||
"title": "Components",
|
||||
"dataBinding": "components.length",
|
||||
"description": "React components",
|
||||
"color": "text-green-500"
|
||||
},
|
||||
{
|
||||
"id": "create-file",
|
||||
"type": "create",
|
||||
"target": "files"
|
||||
"id": "workflows",
|
||||
"icon": "GitBranch",
|
||||
"title": "Workflows",
|
||||
"dataBinding": "workflows.length",
|
||||
"description": "automation flows",
|
||||
"color": "text-orange-500"
|
||||
},
|
||||
{
|
||||
"id": "flask-endpoints",
|
||||
"icon": "Flask",
|
||||
"title": "API Endpoints",
|
||||
"dataBinding": "flaskConfig.blueprints.reduce((acc, bp) => acc + bp.endpoints.length, 0)",
|
||||
"description": "Flask routes",
|
||||
"color": "text-pink-500"
|
||||
},
|
||||
{
|
||||
"id": "test-suites",
|
||||
"icon": "TestTube",
|
||||
"title": "Test Suites",
|
||||
"dataBinding": "playwrightTests.length + storybookStories.length + unitTests.length",
|
||||
"description": "automated tests",
|
||||
"color": "text-cyan-500"
|
||||
}
|
||||
]
|
||||
],
|
||||
"layout": {
|
||||
"type": "vertical",
|
||||
"spacing": "6",
|
||||
"sections": [
|
||||
{
|
||||
"type": "header",
|
||||
"title": "Project Dashboard",
|
||||
"description": "Overview of your CodeForge project"
|
||||
},
|
||||
{
|
||||
"type": "cards",
|
||||
"items": "dashboardCards",
|
||||
"spacing": "6"
|
||||
},
|
||||
{
|
||||
"type": "grid",
|
||||
"items": "statCards",
|
||||
"columns": { "sm": 1, "md": 2, "lg": 3 },
|
||||
"gap": "4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
src/hooks/data/use-data-source.ts
Normal file
81
src/hooks/data/use-data-source.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
src/hooks/data/use-search-filter.ts
Normal file
56
src/hooks/data/use-search-filter.ts
Normal 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
src/hooks/data/use-selection.ts
Normal file
77
src/hooks/data/use-selection.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './use-form'
|
||||
export * from './use-form-field'
|
||||
export { useFormField, useForm } from './use-form-field'
|
||||
export type { ValidationRule, FieldConfig, FormConfig } from './use-form-field'
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export type ValidationRule<T> = {
|
||||
export interface ValidationRule<T = any> {
|
||||
validate: (value: T) => boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export function useFormField<T>(
|
||||
initialValue: T,
|
||||
rules: ValidationRule<T>[] = []
|
||||
) {
|
||||
const [value, setValue] = useState<T>(initialValue)
|
||||
export interface FieldConfig<T = any> {
|
||||
name: string
|
||||
defaultValue?: T
|
||||
rules?: ValidationRule<T>[]
|
||||
}
|
||||
|
||||
export function useFormField<T = any>(config: FieldConfig<T>) {
|
||||
const [value, setValue] = useState<T | undefined>(config.defaultValue)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [touched, setTouched] = useState(false)
|
||||
|
||||
const validate = useCallback(() => {
|
||||
for (const rule of rules) {
|
||||
if (!rule.validate(value)) {
|
||||
if (!config.rules || !touched) return true
|
||||
|
||||
for (const rule of config.rules) {
|
||||
if (!rule.validate(value as T)) {
|
||||
setError(rule.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
setError(null)
|
||||
return true
|
||||
}, [value, rules])
|
||||
}, [value, config.rules, touched])
|
||||
|
||||
const onChange = useCallback((newValue: T) => {
|
||||
setValue(newValue)
|
||||
setTouched(true)
|
||||
}, [])
|
||||
if (touched) {
|
||||
setError(null)
|
||||
}
|
||||
}, [touched])
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setTouched(true)
|
||||
@@ -35,20 +42,43 @@ export function useFormField<T>(
|
||||
}, [validate])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setValue(initialValue)
|
||||
setValue(config.defaultValue)
|
||||
setError(null)
|
||||
setTouched(false)
|
||||
}, [initialValue])
|
||||
}, [config.defaultValue])
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue,
|
||||
onChange,
|
||||
onBlur,
|
||||
error,
|
||||
touched,
|
||||
isValid: error === null && touched,
|
||||
validate,
|
||||
onChange,
|
||||
onBlur,
|
||||
reset,
|
||||
validate,
|
||||
isValid: error === null,
|
||||
isDirty: value !== config.defaultValue,
|
||||
}
|
||||
}
|
||||
|
||||
export interface FormConfig {
|
||||
fields: Record<string, FieldConfig>
|
||||
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
||||
}
|
||||
|
||||
export function useForm(config: FormConfig) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const submit = useCallback(async (values: Record<string, any>) => {
|
||||
setIsSubmitting(true)
|
||||
try {
|
||||
await config.onSubmit?.(values)
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}, [config])
|
||||
|
||||
return {
|
||||
submit,
|
||||
isSubmitting,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,13 @@ export * from './ai/use-ai-generation'
|
||||
|
||||
export * from './data/use-seed-data'
|
||||
export * from './data/use-seed-templates'
|
||||
export { useKVDataSource, useComputedDataSource, useStaticDataSource, useMultipleDataSources } from './data/use-data-source'
|
||||
export { useCRUD } from './data/use-crud'
|
||||
export { useSearchFilter } from './data/use-search-filter'
|
||||
export { useSort } from './data/use-sort'
|
||||
export { usePagination } from './data/use-pagination'
|
||||
export { useSelection as useDataSelection } from './data/use-selection'
|
||||
|
||||
export { useFormField, useForm } from './forms/use-form-field'
|
||||
|
||||
export * from './use-route-preload'
|
||||
|
||||
Reference in New Issue
Block a user