mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-27 15:14:55 +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'
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user