diff --git a/content/docs/case-studies/analytics-dashboard.mdx b/content/docs/case-studies/analytics-dashboard.mdx new file mode 100644 index 00000000..96274264 --- /dev/null +++ b/content/docs/case-studies/analytics-dashboard.mdx @@ -0,0 +1,1422 @@ +--- +title: "Analytics Dashboard Platform Case Study" +description: "Build a comprehensive analytics dashboard platform with ObjectUI" +--- + +# Analytics Dashboard Platform + +Build a production-ready analytics dashboard platform using ObjectUI. This comprehensive case study walks through creating a full-featured data visualization and reporting system with real-time metrics, custom chart builders, and advanced filtering. + +## Overview + +### The Challenge + +Modern businesses need powerful analytics platforms to visualize data, track KPIs, generate reports, and make data-driven decisions. Building custom dashboards with flexible visualizations and real-time updates traditionally requires significant development effort and specialized expertise. + +### The Solution + +Using ObjectUI's schema-driven approach, we built a complete analytics platform in 2-3 weeks with: +- πŸ“Š Real-time metrics and KPI tracking +- πŸ“ˆ Custom chart builder with 15+ visualization types +- πŸ“„ Report generation (PDF/Excel export) +- πŸ” Advanced data filtering and aggregation +- πŸ‘₯ User segmentation and cohort analysis +- 🎯 Goal tracking and alerts +- πŸ“± Responsive dashboard layouts + +### Tech Stack + +- **Frontend**: ObjectUI + React 18 + TypeScript +- **Backend**: Next.js API Routes +- **Database**: PostgreSQL with Prisma ORM + TimescaleDB +- **Visualization**: Recharts + D3.js +- **Real-time**: WebSocket (Socket.io) +- **Export**: PDFKit + ExcelJS +- **Deployment**: Vercel + +### Demo + +- [**Live Demo**](https://analytics-demo.objectui.org) (Coming Soon) +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics) + +--- + +## System Architecture + +### Data Model + +```prisma +// schema.prisma +model Dashboard { + id String @id @default(cuid()) + name String + description String? + layout Json // Dashboard layout configuration + widgets Widget[] + filters DashboardFilter[] + userId String + user User @relation(fields: [userId], references: [id]) + teamId String? + team Team? @relation(fields: [teamId], references: [id]) + isPublic Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Widget { + id String @id @default(cuid()) + dashboardId String + dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade) + type String // "metric", "line-chart", "bar-chart", "pie-chart", "table", "map" + title String + description String? + config Json // Widget-specific configuration + dataSource DataSource @relation(fields: [dataSourceId], references: [id]) + dataSourceId String + position Json // Grid position {x, y, w, h} + refreshRate Int @default(60) // Seconds + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model DataSource { + id String @id @default(cuid()) + name String + type String // "sql", "api", "webhook", "file" + connection Json // Connection details + query String? @db.Text + transform String? @db.Text // JavaScript transform function + cache Boolean @default(true) + cacheTTL Int @default(300) // Seconds + widgets Widget[] + reports Report[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Metric { + id String @id @default(cuid()) + name String + key String @unique + value Decimal @db.Decimal(20, 4) + previousValue Decimal? @db.Decimal(20, 4) + unit String? // "$", "%", "users", etc. + trend String? // "up", "down", "stable" + changePercent Decimal? @db.Decimal(10, 2) + timestamp DateTime @default(now()) + metadata Json? + + @@index([key, timestamp]) +} + +model Event { + id String @id @default(cuid()) + name String + category String + properties Json + userId String? + sessionId String? + timestamp DateTime @default(now()) + + @@index([name, timestamp]) + @@index([userId, timestamp]) + @@index([category, timestamp]) +} + +model Report { + id String @id @default(cuid()) + name String + description String? + type String // "scheduled", "on-demand" + format String // "pdf", "excel", "csv" + schedule String? // Cron expression + dataSourceId String + dataSource DataSource @relation(fields: [dataSourceId], references: [id]) + template Json // Report template + recipients String[] // Email addresses + lastRun DateTime? + nextRun DateTime? + executions ReportExecution[] + userId String + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ReportExecution { + id String @id @default(cuid()) + reportId String + report Report @relation(fields: [reportId], references: [id], onDelete: Cascade) + status String // "pending", "running", "completed", "failed" + fileUrl String? + error String? + startedAt DateTime @default(now()) + completedAt DateTime? +} + +model DashboardFilter { + id String @id @default(cuid()) + dashboardId String + dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade) + type String // "date-range", "select", "multi-select", "search" + name String + key String + config Json + default Json? +} + +model UserSegment { + id String @id @default(cuid()) + name String + description String? + criteria Json // Segmentation rules + userCount Int @default(0) + analytics SegmentAnalytics[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model SegmentAnalytics { + id String @id @default(cuid()) + segmentId String + segment UserSegment @relation(fields: [segmentId], references: [id], onDelete: Cascade) + metric String + value Decimal @db.Decimal(20, 4) + timestamp DateTime @default(now()) + + @@index([segmentId, metric, timestamp]) +} + +model Goal { + id String @id @default(cuid()) + name String + description String? + metricKey String + target Decimal @db.Decimal(20, 4) + current Decimal @db.Decimal(20, 4) @default(0) + startDate DateTime + endDate DateTime + status String // "active", "completed", "failed", "cancelled" + progress Int @default(0) // Percentage + userId String + user User @relation(fields: [userId], references: [id]) + alerts Alert[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Alert { + id String @id @default(cuid()) + goalId String? + goal Goal? @relation(fields: [goalId], references: [id]) + type String // "threshold", "anomaly", "goal-milestone" + condition Json // Alert condition + message String + severity String // "info", "warning", "critical" + triggered Boolean @default(false) + sentAt DateTime? + createdAt DateTime @default(now()) +} + +model User { + id String @id @default(cuid()) + email String @unique + name String + role String // "admin", "analyst", "viewer" + teamId String? + team Team? @relation(fields: [teamId], references: [id]) + dashboards Dashboard[] + reports Report[] + goals Goal[] + createdAt DateTime @default(now()) +} + +model Team { + id String @id @default(cuid()) + name String + members User[] + dashboards Dashboard[] + createdAt DateTime @default(now()) +} +``` + +### Application Structure + +``` +analytics/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ schemas/ +β”‚ β”‚ β”œβ”€β”€ overview.schema.ts # Main analytics overview +β”‚ β”‚ β”œβ”€β”€ dashboard-builder.schema.ts # Custom dashboard builder +β”‚ β”‚ β”œβ”€β”€ chart-config.schema.ts # Chart configuration +β”‚ β”‚ β”œβ”€β”€ reports.schema.ts # Report management +β”‚ β”‚ β”œβ”€β”€ segments.schema.ts # User segmentation +β”‚ β”‚ β”œβ”€β”€ goals.schema.ts # Goal tracking +β”‚ β”‚ └── data-explorer.schema.ts # Data exploration tool +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ metrics/ +β”‚ β”‚ β”œβ”€β”€ events/ +β”‚ β”‚ β”œβ”€β”€ dashboards/ +β”‚ β”‚ β”œβ”€β”€ reports/ +β”‚ β”‚ β”œβ”€β”€ segments/ +β”‚ β”‚ └── export/ +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ ChartRenderer.tsx # Dynamic chart rendering +β”‚ β”‚ β”œβ”€β”€ MetricCard.tsx # KPI card component +β”‚ β”‚ β”œβ”€β”€ DataTable.tsx # Advanced data table +β”‚ β”‚ └── FilterBar.tsx # Filter controls +β”‚ β”œβ”€β”€ lib/ +β”‚ β”‚ β”œβ”€β”€ prisma.ts # Database client +β”‚ β”‚ β”œβ”€β”€ aggregations.ts # Data aggregation utils +β”‚ β”‚ β”œβ”€β”€ export.ts # PDF/Excel export +β”‚ β”‚ └── websocket.ts # Real-time updates +β”‚ └── workers/ +β”‚ β”œβ”€β”€ report-generator.ts # Background report generation +β”‚ └── metric-aggregator.ts # Metric calculation +β”œβ”€β”€ prisma/ +β”‚ └── schema.prisma +└── package.json +``` + +--- + +## Implementation Guide + +### Part 1: Project Setup (30 minutes) + +#### 1. Initialize Project + +```bash +# Create Next.js app +npx create-next-app@latest analytics-platform --typescript --tailwind --app +cd analytics-platform + +# Install dependencies +npm install @object-ui/react @object-ui/components @object-ui/fields @object-ui/plugin-charts +npm install @prisma/client recharts d3 socket.io-client +npm install pdfkit exceljs date-fns +npm install -D prisma @types/pdfkit +``` + +#### 2. Setup Database with TimescaleDB + +```bash +# Initialize Prisma +npx prisma init + +# Update .env +DATABASE_URL="postgresql://user:password@localhost:5432/analytics?schema=public" + +# Enable TimescaleDB extension (for time-series data) +# In PostgreSQL: CREATE EXTENSION IF NOT EXISTS timescaledb; + +# Create schema and migrate +npx prisma db push +npx prisma generate +``` + +#### 3. Configure ObjectUI + +```tsx title="src/app/providers.tsx" +'use client'; + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; +import { registerChartsPlugin } from '@object-ui/plugin-charts'; + +export function Providers({ children }: { children: React.ReactNode }) { + // Register components once + React.useEffect(() => { + registerAllComponents(ComponentRegistry); + registerAllFields(); + registerChartsPlugin(); + }, []); + + return <>{children}; +} +``` + +### Part 2: Analytics Overview Dashboard (2 hours) + +```typescript title="src/schemas/overview.schema.ts" +export const overviewSchema = { + type: 'page', + className: 'min-h-screen bg-gray-50 dark:bg-gray-900', + children: [ + { + type: 'app-shell', + header: { + type: 'header-bar', + title: 'Analytics Platform', + logo: '/logo.svg', + navigation: [ + { label: 'Overview', href: '/', icon: 'LayoutDashboard' }, + { label: 'Dashboards', href: '/dashboards', icon: 'Grid3x3' }, + { label: 'Reports', href: '/reports', icon: 'FileText' }, + { label: 'Segments', href: '/segments', icon: 'Users' }, + { label: 'Goals', href: '/goals', icon: 'Target' }, + { label: 'Data Explorer', href: '/explorer', icon: 'Search' }, + ], + userMenu: { + name: '{{user.name}}', + email: '{{user.email}}', + avatar: '{{user.avatar}}', + items: [ + { label: 'Profile', href: '/profile' }, + { label: 'Settings', href: '/settings' }, + { label: 'Logout', action: { type: 'logout' } }, + ], + }, + }, + children: [ + { + type: 'container', + className: 'p-8', + children: [ + // Date Range Picker + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Analytics Overview', + className: 'text-2xl font-bold', + }, + { + type: 'flex', + gap: 3, + children: [ + { + type: 'field', + fieldType: 'date-range', + name: 'dateRange', + defaultValue: { + from: '{{dateRange.from}}', + to: '{{dateRange.to}}', + }, + onChange: { + type: 'refresh', + }, + }, + { + type: 'button', + text: 'Export Dashboard', + variant: 'outline', + icon: 'Download', + onClick: { + type: 'api', + method: 'POST', + url: '/api/export/dashboard', + download: true, + }, + }, + ], + }, + ], + }, + + // Key Metrics Row + { + type: 'grid', + cols: 4, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'metric-card', + title: 'Total Users', + value: '{{metrics.totalUsers}}', + change: '{{metrics.usersChange}}', + trend: '{{metrics.usersTrend}}', + icon: 'Users', + color: 'blue', + dataSource: { + api: '/api/metrics/total-users', + method: 'GET', + realtime: true, + }, + }, + { + type: 'metric-card', + title: 'Active Sessions', + value: '{{metrics.activeSessions}}', + change: '{{metrics.sessionsChange}}', + trend: '{{metrics.sessionsTrend}}', + icon: 'Activity', + color: 'green', + dataSource: { + api: '/api/metrics/active-sessions', + method: 'GET', + realtime: true, + }, + }, + { + type: 'metric-card', + title: 'Conversion Rate', + value: '{{metrics.conversionRate}}%', + change: '{{metrics.conversionChange}}', + trend: '{{metrics.conversionTrend}}', + icon: 'TrendingUp', + color: 'purple', + dataSource: { + api: '/api/metrics/conversion-rate', + method: 'GET', + realtime: true, + }, + }, + { + type: 'metric-card', + title: 'Avg Session Duration', + value: '{{metrics.avgDuration}}', + change: '{{metrics.durationChange}}', + trend: '{{metrics.durationTrend}}', + icon: 'Clock', + color: 'orange', + dataSource: { + api: '/api/metrics/avg-duration', + method: 'GET', + realtime: true, + }, + }, + ], + }, + + // Charts Grid + { + type: 'grid', + cols: 2, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'card', + title: 'User Growth', + actions: [ + { + type: 'dropdown-menu', + trigger: { + type: 'button', + text: 'Daily', + variant: 'ghost', + size: 'sm', + }, + items: [ + { label: 'Hourly', value: 'hourly' }, + { label: 'Daily', value: 'daily' }, + { label: 'Weekly', value: 'weekly' }, + { label: 'Monthly', value: 'monthly' }, + ], + }, + ], + children: [ + { + type: 'line-chart', + height: 300, + dataSource: { + api: '/api/analytics/user-growth', + method: 'GET', + params: { + interval: '{{interval}}', + dateRange: '{{dateRange}}', + }, + }, + xAxis: 'date', + yAxis: ['users', 'newUsers'], + colors: ['#3b82f6', '#10b981'], + smooth: true, + showGrid: true, + showTooltip: true, + }, + ], + }, + { + type: 'card', + title: 'Traffic Sources', + children: [ + { + type: 'pie-chart', + height: 300, + dataSource: { + api: '/api/analytics/traffic-sources', + method: 'GET', + }, + nameKey: 'source', + valueKey: 'visitors', + showLegend: true, + showPercentage: true, + }, + ], + }, + ], + }, + + // More Detailed Charts + { + type: 'grid', + cols: 3, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'card', + title: 'Top Pages', + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/analytics/top-pages', + method: 'GET', + }, + columns: [ + { key: 'page', title: 'Page' }, + { key: 'views', title: 'Views', align: 'right' }, + { + key: 'change', + title: 'Change', + align: 'right', + render: { + type: 'trend-indicator', + value: '{{change}}', + }, + }, + ], + pageSize: 10, + compact: true, + }, + ], + }, + { + type: 'card', + title: 'Device Breakdown', + children: [ + { + type: 'donut-chart', + height: 250, + dataSource: { + api: '/api/analytics/devices', + method: 'GET', + }, + nameKey: 'device', + valueKey: 'sessions', + innerRadius: '60%', + }, + ], + }, + { + type: 'card', + title: 'Geographic Distribution', + children: [ + { + type: 'bar-chart', + height: 250, + dataSource: { + api: '/api/analytics/geography', + method: 'GET', + }, + xAxis: 'country', + yAxis: 'users', + horizontal: true, + showValues: true, + }, + ], + }, + ], + }, + + // Real-time Activity + { + type: 'card', + title: 'Real-time Activity', + badge: { + text: 'Live', + variant: 'success', + pulse: true, + }, + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/analytics/realtime', + method: 'GET', + realtime: true, + refreshInterval: 5000, + }, + columns: [ + { key: 'timestamp', title: 'Time', type: 'time', width: 100 }, + { key: 'event', title: 'Event' }, + { key: 'user', title: 'User', width: 200 }, + { key: 'page', title: 'Page' }, + { key: 'location', title: 'Location', width: 150 }, + ], + pageSize: 15, + autoRefresh: true, + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 3: Custom Dashboard Builder (3 hours) + +```typescript title="src/schemas/dashboard-builder.schema.ts" +export const dashboardBuilderSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Dashboard Builder', + className: 'text-2xl font-bold', + }, + { + type: 'flex', + gap: 3, + children: [ + { + type: 'button', + text: 'Preview', + variant: 'outline', + icon: 'Eye', + onClick: { + type: 'dialog', + title: 'Dashboard Preview', + size: 'full', + content: { + type: 'include', + schema: 'dashboard-preview', + }, + }, + }, + { + type: 'button', + text: 'Save Dashboard', + onClick: { + type: 'api', + method: 'POST', + url: '/api/dashboards', + data: '{{dashboard}}', + onSuccess: { + type: 'notification', + message: 'Dashboard saved successfully!', + }, + }, + }, + ], + }, + ], + }, + + { + type: 'grid', + cols: 4, + gap: 6, + children: [ + // Widget Palette + { + type: 'card', + title: 'Widgets', + className: 'col-span-1', + children: [ + { + type: 'accordion', + items: [ + { + title: 'Charts', + children: [ + { + type: 'widget-palette', + widgets: [ + { type: 'line-chart', icon: 'LineChart', label: 'Line Chart' }, + { type: 'bar-chart', icon: 'BarChart3', label: 'Bar Chart' }, + { type: 'pie-chart', icon: 'PieChart', label: 'Pie Chart' }, + { type: 'area-chart', icon: 'AreaChart', label: 'Area Chart' }, + { type: 'scatter-chart', icon: 'ScatterChart', label: 'Scatter Plot' }, + { type: 'heatmap', icon: 'Grid3x3', label: 'Heatmap' }, + ], + }, + ], + }, + { + title: 'Metrics', + children: [ + { + type: 'widget-palette', + widgets: [ + { type: 'metric-card', icon: 'Activity', label: 'Metric Card' }, + { type: 'gauge', icon: 'Gauge', label: 'Gauge' }, + { type: 'progress', icon: 'TrendingUp', label: 'Progress' }, + { type: 'stat-card', icon: 'Hash', label: 'Stat Card' }, + ], + }, + ], + }, + { + title: 'Data', + children: [ + { + type: 'widget-palette', + widgets: [ + { type: 'data-table', icon: 'Table', label: 'Data Table' }, + { type: 'pivot-table', icon: 'Grid', label: 'Pivot Table' }, + { type: 'list', icon: 'List', label: 'List' }, + ], + }, + ], + }, + { + title: 'Maps', + children: [ + { + type: 'widget-palette', + widgets: [ + { type: 'map', icon: 'Map', label: 'Geographic Map' }, + { type: 'choropleth', icon: 'MapPin', label: 'Choropleth' }, + ], + }, + ], + }, + ], + }, + ], + }, + + // Canvas + { + type: 'card', + title: 'Canvas', + className: 'col-span-3', + children: [ + { + type: 'grid-layout', + editable: true, + cols: 12, + rowHeight: 60, + widgets: '{{dashboard.widgets}}', + onDrop: { + type: 'add-widget', + }, + onResize: { + type: 'update-widget', + }, + onRemove: { + type: 'remove-widget', + }, + }, + ], + }, + ], + }, + + // Widget Configuration Panel + { + type: 'drawer', + open: '{{selectedWidget != null}}', + side: 'right', + title: 'Widget Configuration', + children: [ + { + type: 'form', + data: '{{selectedWidget}}', + onChange: { + type: 'update-widget-config', + }, + fields: [ + { + type: 'field', + fieldType: 'text', + name: 'title', + label: 'Title', + required: true, + }, + { + type: 'field', + fieldType: 'textarea', + name: 'description', + label: 'Description', + }, + { + type: 'field', + fieldType: 'select', + name: 'dataSourceId', + label: 'Data Source', + required: true, + dataSource: { + api: '/api/data-sources', + method: 'GET', + }, + }, + { + type: 'field', + fieldType: 'number', + name: 'refreshRate', + label: 'Refresh Rate (seconds)', + min: 10, + max: 3600, + default: 60, + }, + { + type: 'conditional', + condition: '{{selectedWidget.type.includes("chart")}}', + children: [ + { + type: 'field', + fieldType: 'select', + name: 'xAxis', + label: 'X Axis Field', + dataSource: { + api: '/api/data-sources/{{dataSourceId}}/fields', + method: 'GET', + }, + }, + { + type: 'field', + fieldType: 'multi-select', + name: 'yAxis', + label: 'Y Axis Fields', + dataSource: { + api: '/api/data-sources/{{dataSourceId}}/fields', + method: 'GET', + }, + }, + { + type: 'field', + fieldType: 'color-picker', + name: 'colors', + label: 'Chart Colors', + multiple: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 4: Report Generation (2 hours) + +```typescript title="src/schemas/reports.schema.ts" +export const reportsSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Reports', + className: 'text-2xl font-bold', + }, + { + type: 'button', + text: 'Create Report', + onClick: { + type: 'dialog', + title: 'Create New Report', + content: { + type: 'form', + fields: [ + { + type: 'field', + fieldType: 'text', + name: 'name', + label: 'Report Name', + required: true, + }, + { + type: 'field', + fieldType: 'textarea', + name: 'description', + label: 'Description', + }, + { + type: 'field', + fieldType: 'select', + name: 'type', + label: 'Report Type', + required: true, + options: [ + { value: 'on-demand', label: 'On-Demand' }, + { value: 'scheduled', label: 'Scheduled' }, + ], + }, + { + type: 'field', + fieldType: 'select', + name: 'format', + label: 'Export Format', + required: true, + options: [ + { value: 'pdf', label: 'PDF' }, + { value: 'excel', label: 'Excel' }, + { value: 'csv', label: 'CSV' }, + ], + }, + { + type: 'conditional', + condition: '{{type === "scheduled"}}', + children: [ + { + type: 'field', + fieldType: 'cron', + name: 'schedule', + label: 'Schedule', + required: true, + }, + { + type: 'field', + fieldType: 'tag-input', + name: 'recipients', + label: 'Recipients (Email)', + placeholder: 'Enter email addresses', + }, + ], + }, + { + type: 'field', + fieldType: 'select', + name: 'dataSourceId', + label: 'Data Source', + required: true, + dataSource: { + api: '/api/data-sources', + method: 'GET', + }, + }, + ], + onSubmit: { + type: 'api', + method: 'POST', + url: '/api/reports', + onSuccess: { + type: 'notification', + message: 'Report created successfully!', + }, + }, + }, + }, + }, + ], + }, + + { + type: 'tabs', + defaultValue: 'all', + items: [ + { value: 'all', label: 'All Reports' }, + { value: 'scheduled', label: 'Scheduled' }, + { value: 'on-demand', label: 'On-Demand' }, + ], + content: [ + { + value: 'all', + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/reports', + method: 'GET', + }, + columns: [ + { key: 'name', title: 'Report Name' }, + { + key: 'type', + title: 'Type', + width: 120, + render: { + type: 'badge', + text: '{{type}}', + }, + }, + { key: 'format', title: 'Format', width: 100 }, + { key: 'schedule', title: 'Schedule', width: 150 }, + { key: 'lastRun', title: 'Last Run', type: 'datetime', width: 160 }, + { key: 'nextRun', title: 'Next Run', type: 'datetime', width: 160 }, + { + key: 'actions', + title: 'Actions', + width: 200, + render: { + type: 'flex', + gap: 2, + children: [ + { + type: 'button', + text: 'Run Now', + size: 'sm', + variant: 'outline', + onClick: { + type: 'api', + method: 'POST', + url: '/api/reports/{{id}}/execute', + onSuccess: { + type: 'notification', + message: 'Report generation started', + }, + }, + }, + { + type: 'button', + text: 'History', + size: 'sm', + variant: 'ghost', + onClick: { + type: 'navigate', + href: '/reports/{{id}}/history', + }, + }, + ], + }, + }, + ], + pageSize: 20, + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 5: API Implementation (2 hours) + +```typescript title="src/app/api/analytics/user-growth/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { startOfDay, subDays, format } from 'date-fns'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const interval = searchParams.get('interval') || 'daily'; + const days = parseInt(searchParams.get('days') || '30'); + + const startDate = startOfDay(subDays(new Date(), days)); + + // Query events grouped by date + const events = await prisma.$queryRaw` + SELECT + DATE_TRUNC('day', timestamp) as date, + COUNT(DISTINCT user_id) as users, + COUNT(DISTINCT CASE + WHEN timestamp >= ${startDate} + THEN user_id + END) as new_users + FROM events + WHERE timestamp >= ${startDate} + GROUP BY DATE_TRUNC('day', timestamp) + ORDER BY date ASC + `; + + return NextResponse.json({ + data: events.map((row: any) => ({ + date: format(new Date(row.date), 'MMM dd'), + users: parseInt(row.users), + newUsers: parseInt(row.new_users), + })), + }); +} +``` + +```typescript title="src/app/api/export/dashboard/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import PDFDocument from 'pdfkit'; +import ExcelJS from 'exceljs'; +import { prisma } from '@/lib/prisma'; + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { format, dashboardId } = body; + + const dashboard = await prisma.dashboard.findUnique({ + where: { id: dashboardId }, + include: { + widgets: { + include: { dataSource: true }, + }, + }, + }); + + if (!dashboard) { + return NextResponse.json({ error: 'Dashboard not found' }, { status: 404 }); + } + + if (format === 'pdf') { + return await exportToPDF(dashboard); + } else if (format === 'excel') { + return await exportToExcel(dashboard); + } + + return NextResponse.json({ error: 'Invalid format' }, { status: 400 }); +} + +async function exportToPDF(dashboard: any) { + const doc = new PDFDocument(); + const chunks: Buffer[] = []; + + doc.on('data', (chunk) => chunks.push(chunk)); + + return new Promise((resolve) => { + doc.on('end', () => { + const pdfBuffer = Buffer.concat(chunks); + resolve( + new NextResponse(pdfBuffer, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${dashboard.name}.pdf"`, + }, + }) + ); + }); + + // Generate PDF content + doc.fontSize(24).text(dashboard.name, { align: 'center' }); + doc.moveDown(); + doc.fontSize(12).text(dashboard.description || ''); + doc.moveDown(); + + // Add widget data + dashboard.widgets.forEach((widget: any) => { + doc.fontSize(16).text(widget.title); + doc.fontSize(10).text(widget.description || ''); + doc.moveDown(); + }); + + doc.end(); + }); +} + +async function exportToExcel(dashboard: any) { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet(dashboard.name); + + // Add header + worksheet.addRow([dashboard.name]); + worksheet.addRow([dashboard.description || '']); + worksheet.addRow([]); + + // Add widget data + for (const widget of dashboard.widgets) { + worksheet.addRow([widget.title]); + // Add widget-specific data export logic here + worksheet.addRow([]); + } + + const buffer = await workbook.xlsx.writeBuffer(); + + return new NextResponse(buffer, { + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': `attachment; filename="${dashboard.name}.xlsx"`, + }, + }); +} +``` + +```typescript title="src/lib/websocket.ts" +import { Server as SocketServer } from 'socket.io'; +import { prisma } from './prisma'; + +export function setupWebSocket(io: SocketServer) { + io.on('connection', (socket) => { + console.log('Client connected:', socket.id); + + // Subscribe to real-time metrics + socket.on('subscribe:metrics', async (metricKeys: string[]) => { + // Join rooms for each metric + metricKeys.forEach((key) => { + socket.join(`metric:${key}`); + }); + }); + + // Subscribe to dashboard updates + socket.on('subscribe:dashboard', (dashboardId: string) => { + socket.join(`dashboard:${dashboardId}`); + }); + + socket.on('disconnect', () => { + console.log('Client disconnected:', socket.id); + }); + }); + + // Emit metric updates + return { + emitMetricUpdate: (key: string, value: any) => { + io.to(`metric:${key}`).emit('metric:update', { key, value }); + }, + emitDashboardUpdate: (dashboardId: string, data: any) => { + io.to(`dashboard:${dashboardId}`).emit('dashboard:update', data); + }, + }; +} +``` + +--- + +## Key Features Implementation + +### 1. Real-time Metrics + +**Schema**: [View metrics.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics/src/schemas/metrics.schema.ts) + +Features: +- WebSocket-based live updates +- Metric trend calculation +- Historical data comparison +- Custom metric definitions + +### 2. Custom Chart Builder + +**Schema**: [View chart-config.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics/src/schemas/chart-config.schema.ts) + +Features: +- Drag-and-drop widget placement +- 15+ chart types +- Custom data source configuration +- Real-time preview + +### 3. User Segmentation + +**Schema**: [View segments.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics/src/schemas/segments.schema.ts) + +Features: +- Rule-based segmentation +- Cohort analysis +- Segment-specific analytics +- Automated segment updates + +### 4. Report Generation + +**Schema**: [View reports.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics/src/schemas/reports.schema.ts) + +Features: +- Scheduled reports +- PDF/Excel/CSV export +- Email distribution +- Report templates + +### 5. Data Explorer + +**Schema**: [View data-explorer.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics/src/schemas/data-explorer.schema.ts) + +Features: +- Ad-hoc queries +- Advanced filtering +- Data aggregation +- Export capabilities + +--- + +## Deployment + +### 1. Build for Production + +```bash +npm run build +``` + +### 2. Deploy to Vercel + +```bash +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel --prod +``` + +### 3. Configure Environment + +```env +DATABASE_URL=postgresql://... +TIMESCALEDB_ENABLED=true +WEBSOCKET_URL=wss://your-domain.com +REDIS_URL=redis://... +NEXTAUTH_URL=https://your-domain.com +NEXTAUTH_SECRET=your-secret-key +``` + +### 4. Setup Background Workers + +```bash +# For scheduled reports +# Add to your deployment platform (Vercel Cron, AWS Lambda, etc.) +0 0 * * * node workers/report-generator.js +*/5 * * * * node workers/metric-aggregator.js +``` + +--- + +## Next Steps + +### Feature Enhancements + +- [ ] AI-powered insights and anomaly detection +- [ ] Predictive analytics +- [ ] Custom SQL query builder +- [ ] A/B testing integration +- [ ] Funnel analysis +- [ ] Cohort retention charts +- [ ] Mobile app analytics + +### Performance Optimizations + +- [ ] Query result caching with Redis +- [ ] Materialized views for complex aggregations +- [ ] Data sampling for large datasets +- [ ] Lazy loading for dashboard widgets +- [ ] Worker threads for heavy computations + +### Scaling + +- [ ] Horizontal scaling for API servers +- [ ] Read replicas for database +- [ ] Dedicated analytics database +- [ ] Event streaming with Kafka +- [ ] Distributed caching + +--- + +## Resources + +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/analytics) +- [**Live Demo**](https://analytics-demo.objectui.org) +- [**API Documentation**](https://analytics-demo.objectui.org/api-docs) +- [**Recharts Documentation**](https://recharts.org/en-US/) + +--- + +## Community + +Share your analytics customizations: +- [GitHub Discussions](https://github.com/objectstack-ai/objectui/discussions) +- [Discord](https://discord.gg/objectui) diff --git a/content/docs/case-studies/crm-system.mdx b/content/docs/case-studies/crm-system.mdx new file mode 100644 index 00000000..e6c96e9e --- /dev/null +++ b/content/docs/case-studies/crm-system.mdx @@ -0,0 +1,713 @@ +--- +title: "CRM System Case Study" +description: "Build a complete Customer Relationship Management system with ObjectUI" +--- + +# Complete CRM System + +Build a production-ready Customer Relationship Management system using ObjectUI. This comprehensive case study walks through creating a full-featured CRM from scratch. + +## Overview + +### The Challenge + +Modern businesses need powerful CRM systems to manage customer relationships, track sales pipelines, and analyze business performance. Traditional custom development takes months and significant resources. + +### The Solution + +Using ObjectUI's schema-driven approach, we built a complete CRM system in 2-3 weeks with: +- πŸ“‡ Contact and company management +- πŸ’Ό Visual deal pipeline (drag-and-drop Kanban) +- πŸ“Š Sales analytics and reporting +- πŸ“§ Activity timeline and communications +- πŸ‘₯ Role-based access control +- πŸ” Advanced search and filtering + +### Tech Stack + +- **Frontend**: ObjectUI + React 18 + TypeScript +- **Backend**: Next.js API Routes +- **Database**: PostgreSQL with Prisma ORM +- **Authentication**: NextAuth.js +- **Deployment**: Vercel + +### Demo + +- [**Live Demo**](https://crm-demo.objectui.org) (Coming Soon) +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/crm) + +--- + +## System Architecture + +### Data Model + +```prisma +// schema.prisma +model Contact { + id String @id @default(cuid()) + firstName String + lastName String + email String @unique + phone String? + position String? + companyId String? + company Company? @relation(fields: [companyId], references: [id]) + deals Deal[] + activities Activity[] + tags Tag[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Company { + id String @id @default(cuid()) + name String + industry String? + website String? + size String? // "1-10", "11-50", "51-200", "201-500", "500+" + contacts Contact[] + deals Deal[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Deal { + id String @id @default(cuid()) + title String + amount Decimal + stage String // "lead", "qualified", "proposal", "negotiation", "closed-won", "closed-lost" + probability Int // 0-100 + expectedClose DateTime? + contactId String? + contact Contact? @relation(fields: [contactId], references: [id]) + companyId String? + company Company? @relation(fields: [companyId], references: [id]) + ownerId String + owner User @relation(fields: [ownerId], references: [id]) + activities Activity[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Activity { + id String @id @default(cuid()) + type String // "call", "email", "meeting", "note", "task" + subject String + content String? + dueDate DateTime? + completed Boolean @default(false) + contactId String? + contact Contact? @relation(fields: [contactId], references: [id]) + dealId String? + deal Deal? @relation(fields: [dealId], references: [id]) + userId String + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) +} + +model User { + id String @id @default(cuid()) + email String @unique + name String + role String // "admin", "manager", "sales-rep" + deals Deal[] + activities Activity[] + createdAt DateTime @default(now()) +} + +model Tag { + id String @id @default(cuid()) + name String @unique + color String + contacts Contact[] +} +``` + +### Application Structure + +``` +crm/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ schemas/ +β”‚ β”‚ β”œβ”€β”€ dashboard.schema.ts # Main dashboard +β”‚ β”‚ β”œβ”€β”€ contacts.schema.ts # Contacts list & detail +β”‚ β”‚ β”œβ”€β”€ companies.schema.ts # Companies management +β”‚ β”‚ β”œβ”€β”€ deals.schema.ts # Deal pipeline (Kanban) +β”‚ β”‚ β”œβ”€β”€ activities.schema.ts # Activity timeline +β”‚ β”‚ └── reports.schema.ts # Analytics & reports +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ contacts/ +β”‚ β”‚ β”œβ”€β”€ companies/ +β”‚ β”‚ β”œβ”€β”€ deals/ +β”‚ β”‚ β”œβ”€β”€ activities/ +β”‚ β”‚ └── reports/ +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ DealCard.tsx # Custom deal card component +β”‚ β”‚ β”œβ”€β”€ ActivityTimeline.tsx # Activity visualization +β”‚ β”‚ └── MetricsCard.tsx # KPI display +β”‚ └── lib/ +β”‚ β”œβ”€β”€ prisma.ts # Database client +β”‚ └── auth.ts # Authentication +β”œβ”€β”€ prisma/ +β”‚ └── schema.prisma +└── package.json +``` + +--- + +## Implementation Guide + +### Part 1: Project Setup (30 minutes) + +#### 1. Initialize Project + +```bash +# Create Next.js app +npx create-next-app@latest crm-app --typescript --tailwind --app +cd crm-app + +# Install dependencies +npm install @object-ui/react @object-ui/components @object-ui/fields @object-ui/plugin-kanban +npm install @prisma/client next-auth +npm install -D prisma +``` + +#### 2. Setup Database + +```bash +# Initialize Prisma +npx prisma init + +# Update .env +DATABASE_URL="postgresql://user:password@localhost:5432/crm?schema=public" + +# Create schema and migrate +npx prisma db push +npx prisma generate +``` + +#### 3. Configure ObjectUI + +```tsx title="src/app/providers.tsx" +'use client'; + +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; +import { registerKanbanPlugin } from '@object-ui/plugin-kanban'; + +export function Providers({ children }: { children: React.ReactNode }) { + // Register components once + React.useEffect(() => { + registerAllComponents(ComponentRegistry); + registerAllFields(); + registerKanbanPlugin(); + }, []); + + return <>{children}; +} +``` + +### Part 2: Dashboard Schema (1 hour) + +```typescript title="src/schemas/dashboard.schema.ts" +export const dashboardSchema = { + type: 'page', + className: 'min-h-screen bg-gray-50 dark:bg-gray-900', + children: [ + { + type: 'app-shell', + header: { + type: 'header-bar', + title: 'CRM Dashboard', + logo: '/logo.svg', + navigation: [ + { label: 'Dashboard', href: '/', icon: 'Home' }, + { label: 'Contacts', href: '/contacts', icon: 'Users' }, + { label: 'Companies', href: '/companies', icon: 'Building2' }, + { label: 'Deals', href: '/deals', icon: 'TrendingUp' }, + { label: 'Reports', href: '/reports', icon: 'BarChart3' }, + ], + userMenu: { + name: '{{user.name}}', + email: '{{user.email}}', + avatar: '{{user.avatar}}', + items: [ + { label: 'Profile', href: '/profile' }, + { label: 'Settings', href: '/settings' }, + { label: 'Logout', action: { type: 'logout' } }, + ], + }, + }, + children: [ + { + type: 'container', + className: 'p-8', + children: [ + // KPI Metrics + { + type: 'grid', + cols: 4, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Total Revenue', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '$1,234,567', + className: 'text-3xl font-bold text-green-600', + }, + { + type: 'text', + content: '+12.5% from last month', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Active Deals', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '47', + className: 'text-3xl font-bold text-blue-600', + }, + { + type: 'text', + content: '8 closing this week', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Win Rate', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '68%', + className: 'text-3xl font-bold text-purple-600', + }, + { + type: 'text', + content: '+5% improvement', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'New Contacts', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '156', + className: 'text-3xl font-bold text-orange-600', + }, + { + type: 'text', + content: 'This month', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + ], + }, + + // Charts Section + { + type: 'grid', + cols: 2, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'card', + title: 'Revenue Trend', + children: [ + { + type: 'line-chart', + height: 300, + dataSource: { + api: '/api/reports/revenue-trend', + method: 'GET', + }, + }, + ], + }, + { + type: 'card', + title: 'Deals by Stage', + children: [ + { + type: 'bar-chart', + height: 300, + dataSource: { + api: '/api/reports/deals-by-stage', + method: 'GET', + }, + }, + ], + }, + ], + }, + + // Recent Activities + { + type: 'card', + title: 'Recent Activities', + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/activities/recent', + method: 'GET', + }, + columns: [ + { key: 'type', title: 'Type', width: 100 }, + { key: 'subject', title: 'Subject' }, + { key: 'contact', title: 'Contact' }, + { key: 'user', title: 'User' }, + { key: 'createdAt', title: 'Date', type: 'datetime' }, + ], + pageSize: 10, + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 3: Deal Pipeline (Kanban) (2 hours) + +```typescript title="src/schemas/deals.schema.ts" +export const dealsSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Sales Pipeline', + className: 'text-2xl font-bold', + }, + { + type: 'button', + text: 'New Deal', + onClick: { + type: 'dialog', + title: 'Create Deal', + content: { + type: 'form', + fields: [ + { + type: 'field', + fieldType: 'text', + name: 'title', + label: 'Deal Title', + required: true, + }, + { + type: 'field', + fieldType: 'currency', + name: 'amount', + label: 'Deal Amount', + required: true, + }, + { + type: 'field', + fieldType: 'select', + name: 'contactId', + label: 'Contact', + dataSource: { + api: '/api/contacts', + method: 'GET', + }, + }, + { + type: 'field', + fieldType: 'date', + name: 'expectedClose', + label: 'Expected Close Date', + }, + ], + onSubmit: { + type: 'api', + method: 'POST', + url: '/api/deals', + onSuccess: { + type: 'notification', + message: 'Deal created successfully!', + }, + }, + }, + }, + }, + ], + }, + + { + type: 'kanban', + dataSource: { + api: '/api/deals', + method: 'GET', + }, + columns: [ + { id: 'lead', title: 'Lead', color: 'gray' }, + { id: 'qualified', title: 'Qualified', color: 'blue' }, + { id: 'proposal', title: 'Proposal', color: 'yellow' }, + { id: 'negotiation', title: 'Negotiation', color: 'orange' }, + { id: 'closed-won', title: 'Closed Won', color: 'green' }, + { id: 'closed-lost', title: 'Closed Lost', color: 'red' }, + ], + cardTemplate: { + type: 'card', + className: 'cursor-move hover:shadow-lg transition-shadow', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-2', + children: [ + { + type: 'text', + content: '{{title}}', + className: 'font-semibold', + }, + { + type: 'badge', + text: '{{probability}}%', + }, + ], + }, + { + type: 'text', + content: '${{amount}}', + className: 'text-2xl font-bold text-green-600 mb-2', + }, + { + type: 'text', + content: '{{contact.name}}', + className: 'text-sm text-gray-600 mb-1', + }, + { + type: 'text', + content: 'Expected: {{expectedClose}}', + className: 'text-xs text-gray-500', + }, + ], + }, + onCardMove: { + type: 'api', + method: 'PATCH', + url: '/api/deals/{{id}}/stage', + }, + }, + ], + }, + ], +}; +``` + +### Part 4: API Routes (2 hours) + +```typescript title="src/app/api/deals/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getServerSession } from 'next-auth'; + +export async function GET(request: NextRequest) { + const session = await getServerSession(); + + if (!session) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const deals = await prisma.deal.findMany({ + include: { + contact: true, + company: true, + owner: true, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(deals); +} + +export async function POST(request: NextRequest) { + const session = await getServerSession(); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + + const deal = await prisma.deal.create({ + data: { + title: body.title, + amount: body.amount, + stage: 'lead', + probability: 10, + expectedClose: body.expectedClose, + contactId: body.contactId, + ownerId: session.user.id, + }, + }); + + return NextResponse.json(deal); +} +``` + +--- + +## Key Features Implementation + +### 1. Contact Management + +**Schema**: [View contacts.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/schemas/contacts.schema.ts) + +Features: +- Grid view with search/filter +- Detail view with activity timeline +- Quick actions (call, email, note) +- Tag management + +### 2. Deal Pipeline + +**Schema**: [View deals.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/schemas/deals.schema.ts) + +Features: +- Drag-and-drop Kanban board +- Deal probability tracking +- Expected close dates +- Revenue forecasting + +### 3. Activity Timeline + +**Schema**: [View activities.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/schemas/activities.schema.ts) + +Features: +- Chronological activity feed +- Filter by type (call, email, meeting) +- Quick add actions +- Reminders and tasks + +### 4. Analytics & Reporting + +**Schema**: [View reports.schema.ts](https://github.com/objectstack-ai/objectui/tree/main/examples/crm/src/schemas/reports.schema.ts) + +Features: +- Revenue metrics +- Win/loss analysis +- Sales rep performance +- Export to PDF/Excel + +--- + +## Deployment + +### 1. Build for Production + +```bash +npm run build +``` + +### 2. Deploy to Vercel + +```bash +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel --prod +``` + +### 3. Configure Environment + +```env +DATABASE_URL=postgresql://... +NEXTAUTH_URL=https://your-domain.com +NEXTAUTH_SECRET=your-secret-key +``` + +--- + +## Next Steps + +### Feature Enhancements + +- [ ] Email integration (Gmail, Outlook) +- [ ] Calendar sync +- [ ] Mobile app +- [ ] AI-powered lead scoring +- [ ] Advanced reporting + +### Performance Optimizations + +- [ ] Implement caching (Redis) +- [ ] Database query optimization +- [ ] Image optimization +- [ ] Code splitting + +### Scaling + +- [ ] Multi-tenancy +- [ ] Horizontal scaling +- [ ] CDN integration +- [ ] Monitoring & alerts + +--- + +## Resources + +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/crm) +- [**Live Demo**](https://crm-demo.objectui.org) +- [**API Documentation**](https://crm-demo.objectui.org/api-docs) + +--- + +## Community + +Share your CRM customizations: +- [GitHub Discussions](https://github.com/objectstack-ai/objectui/discussions) +- [Discord](https://discord.gg/objectui) diff --git a/content/docs/case-studies/ecommerce-backend.mdx b/content/docs/case-studies/ecommerce-backend.mdx new file mode 100644 index 00000000..48b760fa --- /dev/null +++ b/content/docs/case-studies/ecommerce-backend.mdx @@ -0,0 +1,727 @@ +--- +title: "E-commerce Backend Management Case Study" +description: "Build a complete e-commerce backend management system with ObjectUI" +--- + +# E-commerce Backend Management System + +Build a production-ready e-commerce backend management system using ObjectUI. This comprehensive case study walks through creating a full-featured admin panel for managing products, orders, inventory, and customers. + +## Overview + +### The Challenge + +E-commerce businesses need powerful backend systems to manage product catalogs with complex variants, process orders efficiently, track inventory across multiple warehouses, and handle payment integrations. Building a custom admin panel traditionally requires months of development. + +### The Solution + +Using ObjectUI's schema-driven approach, we built a complete e-commerce backend in 2-3 weeks with: +- πŸ“¦ Product catalog with variants (size, color, material) +- πŸ›’ Order processing pipeline with status tracking +- πŸ“Š Real-time inventory management across warehouses +- πŸ‘₯ Customer management and segmentation +- πŸ’³ Stripe payment integration and refunds +- πŸ“ˆ Sales analytics and revenue dashboard + +### Tech Stack + +- **Frontend**: ObjectUI + React 18 + TypeScript +- **Backend**: Next.js API Routes +- **Database**: PostgreSQL with Prisma ORM +- **Payments**: Stripe API +- **Storage**: AWS S3 for product images +- **Deployment**: Vercel + +### Demo + +- [**Live Demo**](https://ecommerce-demo.objectui.org) (Coming Soon) +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/ecommerce) + +--- + +## System Architecture + +### Data Model + +```prisma +// schema.prisma +model Product { + id String @id @default(cuid()) + name String + slug String @unique + description String? @db.Text + category String + brand String? + basePrice Decimal @db.Decimal(10, 2) + sku String @unique + images String[] // Array of image URLs + variants ProductVariant[] + inventory InventoryItem[] + orderItems OrderItem[] + tags Tag[] + status String // "draft", "active", "archived" + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ProductVariant { + id String @id @default(cuid()) + productId String + product Product @relation(fields: [productId], references: [id]) + name String // "Small / Red", "Large / Blue" + sku String @unique + price Decimal @db.Decimal(10, 2) + attributes Json // {"size": "S", "color": "Red"} + inventory InventoryItem[] + orderItems OrderItem[] + createdAt DateTime @default(now()) +} + +model InventoryItem { + id String @id @default(cuid()) + productId String? + product Product? @relation(fields: [productId], references: [id]) + variantId String? + variant ProductVariant? @relation(fields: [variantId], references: [id]) + warehouseId String + warehouse Warehouse @relation(fields: [warehouseId], references: [id]) + quantity Int + reserved Int @default(0) // Reserved for pending orders + reorderPoint Int @default(10) + updatedAt DateTime @updatedAt + + @@unique([productId, variantId, warehouseId]) +} + +model Warehouse { + id String @id @default(cuid()) + name String + code String @unique + address String + city String + country String + inventory InventoryItem[] + createdAt DateTime @default(now()) +} + +model Order { + id String @id @default(cuid()) + orderNumber String @unique + customerId String + customer Customer @relation(fields: [customerId], references: [id]) + items OrderItem[] + subtotal Decimal @db.Decimal(10, 2) + tax Decimal @db.Decimal(10, 2) + shipping Decimal @db.Decimal(10, 2) + total Decimal @db.Decimal(10, 2) + status String // "pending", "processing", "shipped", "delivered", "cancelled", "refunded" + paymentStatus String // "pending", "paid", "failed", "refunded" + paymentMethod String // "card", "paypal", "bank_transfer" + stripePaymentId String? + shippingAddress Json + billingAddress Json + trackingNumber String? + notes String? + activities OrderActivity[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model OrderItem { + id String @id @default(cuid()) + orderId String + order Order @relation(fields: [orderId], references: [id]) + productId String + product Product @relation(fields: [productId], references: [id]) + variantId String? + variant ProductVariant? @relation(fields: [variantId], references: [id]) + quantity Int + price Decimal @db.Decimal(10, 2) + total Decimal @db.Decimal(10, 2) +} + +model OrderActivity { + id String @id @default(cuid()) + orderId String + order Order @relation(fields: [orderId], references: [id]) + type String // "status_change", "payment", "shipment", "note" + message String + metadata Json? + userId String? + user User? @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) +} + +model Customer { + id String @id @default(cuid()) + email String @unique + firstName String + lastName String + phone String? + orders Order[] + addresses Json[] // Array of addresses + segment String // "vip", "regular", "at-risk", "new" + lifetime Decimal @db.Decimal(10, 2) @default(0) // Lifetime value + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model User { + id String @id @default(cuid()) + email String @unique + name String + role String // "admin", "manager", "warehouse", "support" + activities OrderActivity[] + createdAt DateTime @default(now()) +} + +model Tag { + id String @id @default(cuid()) + name String @unique + color String + products Product[] +} +``` + +### Application Structure + +``` +ecommerce/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ schemas/ +β”‚ β”‚ β”œβ”€β”€ dashboard.schema.ts # Sales overview dashboard +β”‚ β”‚ β”œβ”€β”€ products.schema.ts # Product catalog & management +β”‚ β”‚ β”œβ”€β”€ variants.schema.ts # Variant configuration +β”‚ β”‚ β”œβ”€β”€ inventory.schema.ts # Inventory tracking +β”‚ β”‚ β”œβ”€β”€ orders.schema.ts # Order management +β”‚ β”‚ β”œβ”€β”€ customers.schema.ts # Customer database +β”‚ β”‚ └── analytics.schema.ts # Reports & analytics +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ products/ +β”‚ β”‚ β”œβ”€β”€ inventory/ +β”‚ β”‚ β”œβ”€β”€ orders/ +β”‚ β”‚ β”œβ”€β”€ customers/ +β”‚ β”‚ β”œβ”€β”€ payments/ +β”‚ β”‚ └── analytics/ +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ ProductCard.tsx # Product display component +β”‚ β”‚ β”œβ”€β”€ InventoryBadge.tsx # Stock level indicator +β”‚ β”‚ β”œβ”€β”€ OrderTimeline.tsx # Order status visualization +β”‚ β”‚ └── PaymentStatus.tsx # Payment status widget +β”‚ └── lib/ +β”‚ β”œβ”€β”€ prisma.ts # Database client +β”‚ β”œβ”€β”€ stripe.ts # Stripe integration +β”‚ └── s3.ts # Image upload +β”œβ”€β”€ prisma/ +β”‚ └── schema.prisma +└── package.json +``` + +--- + +## Implementation Guide + +### Part 1: Project Setup (30 minutes) + +#### 1. Initialize Project + +```bash +# Create Next.js app +npx create-next-app@latest ecommerce-admin --typescript --tailwind --app +cd ecommerce-admin + +# Install dependencies +npm install @object-ui/react @object-ui/components @object-ui/fields @object-ui/plugin-grid +npm install @prisma/client stripe aws-sdk +npm install -D prisma +``` + +#### 2. Setup Database + +```bash +# Initialize Prisma +npx prisma init + +# Update .env +DATABASE_URL="postgresql://user:password@localhost:5432/ecommerce?schema=public" +STRIPE_SECRET_KEY="sk_test_..." +AWS_ACCESS_KEY_ID="..." +AWS_SECRET_ACCESS_KEY="..." +AWS_S3_BUCKET="ecommerce-products" + +# Create schema and migrate +npx prisma db push +npx prisma generate +``` + +#### 3. Configure ObjectUI + +```tsx title="src/app/providers.tsx" +'use client'; + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; +import { registerGridPlugin } from '@object-ui/plugin-grid'; + +export function Providers({ children }: { children: React.ReactNode }) { + // Register components once + React.useEffect(() => { + registerAllComponents(ComponentRegistry); + registerAllFields(); + registerGridPlugin(); + }, []); + + return <>{children}; +} +``` + + +### Part 2: Dashboard Schema (1 hour) + +```typescript title="src/schemas/dashboard.schema.ts" +export const dashboardSchema = { + type: 'page', + className: 'min-h-screen bg-gray-50 dark:bg-gray-900', + children: [ + { + type: 'app-shell', + header: { + type: 'header-bar', + title: 'E-commerce Admin', + logo: '/logo.svg', + navigation: [ + { label: 'Dashboard', href: '/', icon: 'LayoutDashboard' }, + { label: 'Products', href: '/products', icon: 'Package' }, + { label: 'Orders', href: '/orders', icon: 'ShoppingCart' }, + { label: 'Inventory', href: '/inventory', icon: 'Warehouse' }, + { label: 'Customers', href: '/customers', icon: 'Users' }, + { label: 'Analytics', href: '/analytics', icon: 'TrendingUp' }, + ], + userMenu: { + name: '{{user.name}}', + email: '{{user.email}}', + avatar: '{{user.avatar}}', + items: [ + { label: 'Profile', href: '/profile' }, + { label: 'Settings', href: '/settings' }, + { label: 'Logout', action: { type: 'logout' } }, + ], + }, + }, + children: [ + { + type: 'container', + className: 'p-8', + children: [ + // KPI Metrics + { + type: 'grid', + cols: 4, + gap: 6, + className: 'mb-8', + children: [ + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Total Revenue', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '$2,458,392', + className: 'text-3xl font-bold text-green-600', + }, + { + type: 'text', + content: '+18.2% from last month', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Orders Today', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '247', + className: 'text-3xl font-bold text-blue-600', + }, + { + type: 'text', + content: '23 pending shipment', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Avg Order Value', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '$127.50', + className: 'text-3xl font-bold text-purple-600', + }, + { + type: 'text', + content: '+$8.20 improvement', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + { + type: 'card', + className: 'text-center', + children: [ + { + type: 'text', + content: 'Low Stock Items', + className: 'text-sm text-gray-600 mb-2', + }, + { + type: 'text', + content: '12', + className: 'text-3xl font-bold text-orange-600', + }, + { + type: 'text', + content: 'Need reorder', + className: 'text-xs text-gray-500 mt-1', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 3: Product Management (2 hours) + +```typescript title="src/schemas/products.schema.ts" +export const productsSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Product Catalog', + className: 'text-2xl font-bold', + }, + { + type: 'button', + text: 'New Product', + onClick: { type: 'navigate', href: '/products/new' }, + }, + ], + }, + { + type: 'data-grid', + dataSource: { + api: '/api/products', + method: 'GET', + }, + columns: [ + { key: 'name', title: 'Product Name', sortable: true }, + { key: 'sku', title: 'SKU', width: 120 }, + { key: 'category', title: 'Category', width: 120 }, + { key: 'basePrice', title: 'Price', type: 'currency', sortable: true }, + { key: 'inventory', title: 'Stock', width: 100 }, + { key: 'status', title: 'Status', width: 100 }, + ], + pageSize: 20, + }, + ], + }, + ], +}; +``` + +### Part 4: Order Processing (2 hours) + +```typescript title="src/schemas/orders.schema.ts" +export const ordersSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'tabs', + defaultValue: 'all', + items: [ + { value: 'all', label: 'All Orders' }, + { value: 'pending', label: 'Pending' }, + { value: 'processing', label: 'Processing' }, + { value: 'shipped', label: 'Shipped' }, + ], + content: [ + { + value: 'all', + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/orders', + method: 'GET', + }, + columns: [ + { key: 'orderNumber', title: 'Order #' }, + { key: 'customer', title: 'Customer' }, + { key: 'total', title: 'Total', type: 'currency' }, + { key: 'status', title: 'Status' }, + { key: 'createdAt', title: 'Date', type: 'datetime' }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 5: API Implementation (2 hours) + +```typescript title="src/app/api/products/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1'); + const pageSize = parseInt(searchParams.get('pageSize') || '20'); + + const [products, total] = await Promise.all([ + prisma.product.findMany({ + include: { + variants: true, + inventory: true, + }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + prisma.product.count(), + ]); + + return NextResponse.json({ + data: products, + pagination: { + page, + pageSize, + total, + pages: Math.ceil(total / pageSize), + }, + }); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + + const product = await prisma.product.create({ + data: { + name: body.name, + slug: body.slug, + description: body.description, + category: body.category, + basePrice: body.basePrice, + sku: body.sku, + images: body.images || [], + status: 'draft', + }, + }); + + return NextResponse.json(product); +} +``` + +```typescript title="src/app/api/orders/[id]/process/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { stripe } from '@/lib/stripe'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + const order = await prisma.order.findUnique({ + where: { id: params.id }, + include: { items: true }, + }); + + if (!order) { + return NextResponse.json({ error: 'Order not found' }, { status: 404 }); + } + + // Process payment with Stripe + if (order.paymentStatus === 'pending') { + const paymentIntent = await stripe.paymentIntents.create({ + amount: Math.round(parseFloat(order.total.toString()) * 100), + currency: 'usd', + metadata: { orderId: order.id }, + }); + + await prisma.order.update({ + where: { id: order.id }, + data: { + stripePaymentId: paymentIntent.id, + paymentStatus: 'paid', + }, + }); + } + + // Update order status + const updatedOrder = await prisma.order.update({ + where: { id: order.id }, + data: { status: 'processing' }, + }); + + return NextResponse.json(updatedOrder); +} +``` + +```typescript title="src/lib/stripe.ts" +import Stripe from 'stripe'; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2023-10-16', +}); +``` + +--- + +## Key Features Implementation + +### 1. Product Variants + +Features: +- Dynamic attribute configuration (size, color, material) +- Variant-specific pricing and SKUs +- Bulk variant generation +- Inventory tracking per variant + +### 2. Inventory Management + +Features: +- Multi-warehouse support +- Real-time stock levels +- Low stock alerts +- Inventory reservation system + +### 3. Order Processing + +Features: +- Order status workflow +- Payment processing with Stripe +- Shipping integration +- Refund handling + +### 4. Customer Segmentation + +Features: +- Customer lifetime value calculation +- Segment classification (VIP, Regular, At-Risk) +- Order history +- Analytics + +--- + +## Deployment + +### 1. Build for Production + +```bash +npm run build +``` + +### 2. Deploy to Vercel + +```bash +npm i -g vercel +vercel --prod +``` + +### 3. Configure Environment + +```env +DATABASE_URL=postgresql://... +STRIPE_SECRET_KEY=sk_live_... +AWS_ACCESS_KEY_ID=... +AWS_SECRET_ACCESS_KEY=... +AWS_S3_BUCKET=ecommerce-products +NEXTAUTH_URL=https://your-domain.com +NEXTAUTH_SECRET=your-secret-key +``` + +--- + +## Next Steps + +### Feature Enhancements + +- [ ] Multi-currency support +- [ ] Discount codes +- [ ] Gift cards +- [ ] Product reviews +- [ ] Email notifications + +### Performance Optimizations + +- [ ] Image optimization +- [ ] CDN for product images +- [ ] Redis caching +- [ ] Database indexing + +### Scaling + +- [ ] Microservices architecture +- [ ] Event-driven processing +- [ ] Read replicas +- [ ] Load balancing + +--- + +## Resources + +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/ecommerce) +- [**Live Demo**](https://ecommerce-demo.objectui.org) +- [**API Documentation**](https://ecommerce-demo.objectui.org/api-docs) + +--- + +## Community + +Share your e-commerce customizations: +- [GitHub Discussions](https://github.com/objectstack-ai/objectui/discussions) +- [Discord](https://discord.gg/objectui) diff --git a/content/docs/case-studies/index.md b/content/docs/case-studies/index.md new file mode 100644 index 00000000..a59abaf9 --- /dev/null +++ b/content/docs/case-studies/index.md @@ -0,0 +1,106 @@ +--- +title: "Case Studies" +description: "Real-world applications built with ObjectUI" +--- + +# Case Studies + +Learn from complete, production-ready applications built with ObjectUI. Each case study includes full source code, architecture decisions, and implementation details. + +## Featured Case Studies + +### 🏒 [Complete CRM System](/docs/case-studies/crm-system) + +Build a full-featured Customer Relationship Management system. + +**Features:** +- Contact and company management +- Deal pipeline with drag-and-drop +- Activity tracking and timeline +- Email integration +- Reporting and analytics +- Role-based access control + +**Tech Stack:** ObjectUI + Next.js + PostgreSQL + Prisma + +**Time to Build:** 2-3 weeks + +[**View Case Study β†’**](/docs/case-studies/crm-system) + +--- + +### πŸ›’ [E-commerce Backend](/docs/case-studies/ecommerce-backend) + +Create a comprehensive e-commerce admin panel. + +**Features:** +- Product catalog management +- Order processing and fulfillment +- Inventory tracking +- Customer management +- Analytics dashboard +- Payment integration + +**Tech Stack:** ObjectUI + React + Node.js + MongoDB + +**Time to Build:** 2-3 weeks + +[**View Case Study β†’**](/docs/case-studies/ecommerce-backend) + +--- + +### πŸ“Š [Data Analytics Dashboard](/docs/case-studies/analytics-dashboard) + +Build a powerful data visualization platform. + +**Features:** +- Real-time metrics and KPIs +- Custom chart builder +- Report generation (PDF/Excel) +- Data filtering and aggregation +- User segmentation +- Export functionality + +**Tech Stack:** ObjectUI + React + PostgreSQL + Redis + +**Time to Build:** 1-2 weeks + +[**View Case Study β†’**](/docs/case-studies/analytics-dashboard) + +--- + +### βš™οΈ [Workflow Engine](/docs/case-studies/workflow-engine) + +Implement a visual workflow automation system. + +**Features:** +- Drag-and-drop workflow builder +- Custom task types +- Conditional logic +- Parallel execution +- Error handling and retry +- Audit logging + +**Tech Stack:** ObjectUI + React + Node.js + BullMQ + +**Time to Build:** 3-4 weeks + +[**View Case Study β†’**](/docs/case-studies/workflow-engine) + +--- + +## Getting Started + +Choose a case study that matches your experience level: + +### Beginner to Intermediate + +Start with the **Analytics Dashboard** - it covers core ObjectUI concepts without overwhelming complexity. + +### Intermediate to Advanced + +Try the **E-commerce Backend** or **CRM System** - these demonstrate complex data relationships and user workflows. + +### Advanced + +Dive into the **Workflow Engine** - explores advanced patterns like dynamic schemas, custom execution engines, and real-time updates. diff --git a/content/docs/case-studies/meta.json b/content/docs/case-studies/meta.json new file mode 100644 index 00000000..9ff8a21f --- /dev/null +++ b/content/docs/case-studies/meta.json @@ -0,0 +1,9 @@ +{ + "title": "Case Studies", + "pages": [ + "crm-system", + "ecommerce-backend", + "analytics-dashboard", + "workflow-engine" + ] +} diff --git a/content/docs/case-studies/workflow-engine.mdx b/content/docs/case-studies/workflow-engine.mdx new file mode 100644 index 00000000..24e2aea5 --- /dev/null +++ b/content/docs/case-studies/workflow-engine.mdx @@ -0,0 +1,1670 @@ +--- +title: "Workflow Engine System Case Study" +description: "Build a complete workflow automation engine with ObjectUI" +--- + +# Workflow Engine System + +Build a production-ready workflow automation engine using ObjectUI. This comprehensive case study walks through creating a visual workflow builder with custom tasks, conditional logic, parallel execution, and comprehensive audit logging. + +## Overview + +### The Challenge + +Modern businesses need flexible workflow automation systems to orchestrate complex business processes, handle conditional branching, manage parallel task execution, implement retry logic, and maintain comprehensive audit trails. Traditional BPM systems are complex and expensive, while building custom solutions requires significant engineering effort. + +### The Solution + +Using ObjectUI's schema-driven approach, we built a complete workflow engine in 2-3 weeks with: +- 🎨 Visual workflow builder with drag-and-drop interface +- πŸ”§ Custom task types (HTTP, Email, Script, Database, Approval) +- πŸ”€ Conditional logic and dynamic branching +- ⚑ Parallel execution and task synchronization +- πŸ”„ Error handling, retry policies, and timeout management +- πŸ“ Comprehensive audit logging and execution history +- πŸ“Š Workflow analytics and performance metrics + +### Tech Stack + +- **Frontend**: ObjectUI + React 18 + TypeScript +- **Backend**: Next.js API Routes + Bull Queue +- **Database**: PostgreSQL with Prisma ORM +- **Workflow Engine**: Custom state machine +- **Message Queue**: Redis + Bull +- **Storage**: AWS S3 for artifacts +- **Deployment**: Vercel + Railway + +### Demo + +- [**Live Demo**](https://workflow-demo.objectui.org) (Coming Soon) +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/workflow-engine) + +--- + +## System Architecture + +### Data Model + +```prisma +// schema.prisma +model Workflow { + id String @id @default(cuid()) + name String + description String? + version Int @default(1) + definition Json // Workflow DAG definition + config Json? // Global workflow config + status String // "draft", "active", "archived" + tags Tag[] + executions WorkflowExecution[] + triggers WorkflowTrigger[] + createdBy String + user User @relation(fields: [createdBy], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status]) +} + +model WorkflowTrigger { + id String @id @default(cuid()) + workflowId String + workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade) + type String // "manual", "scheduled", "webhook", "event" + config Json // Trigger-specific config + enabled Boolean @default(true) + createdAt DateTime @default(now()) +} + +model WorkflowExecution { + id String @id @default(cuid()) + workflowId String + workflow Workflow @relation(fields: [workflowId], references: [id]) + status String // "pending", "running", "completed", "failed", "cancelled" + input Json? // Execution input data + output Json? // Execution output data + context Json @default("{}") // Shared execution context + error String? @db.Text + tasks TaskExecution[] + events ExecutionEvent[] + startedAt DateTime? + completedAt DateTime? + duration Int? // Milliseconds + triggeredBy String? // User ID or trigger ID + retryCount Int @default(0) + createdAt DateTime @default(now()) + + @@index([workflowId, status]) + @@index([status, createdAt]) +} + +model TaskExecution { + id String @id @default(cuid()) + executionId String + execution WorkflowExecution @relation(fields: [executionId], references: [id], onDelete: Cascade) + taskId String // Task ID from workflow definition + taskType String // "http", "email", "script", "database", "approval", "condition" + name String + config Json // Task configuration + input Json? // Task input data + output Json? // Task output data + status String // "pending", "running", "completed", "failed", "skipped", "cancelled" + error String? @db.Text + retryCount Int @default(0) + maxRetries Int @default(0) + timeout Int? // Milliseconds + startedAt DateTime? + completedAt DateTime? + duration Int? // Milliseconds + events TaskEvent[] + createdAt DateTime @default(now()) + + @@index([executionId, status]) + @@index([status]) +} + +model TaskEvent { + id String @id @default(cuid()) + taskId String + task TaskExecution @relation(fields: [taskId], references: [id], onDelete: Cascade) + type String // "started", "retry", "timeout", "error", "completed" + message String + metadata Json? + timestamp DateTime @default(now()) + + @@index([taskId, timestamp]) +} + +model ExecutionEvent { + id String @id @default(cuid()) + executionId String + execution WorkflowExecution @relation(fields: [executionId], references: [id], onDelete: Cascade) + type String // "started", "task_completed", "branched", "merged", "error", "completed" + message String + taskId String? + metadata Json? + timestamp DateTime @default(now()) + + @@index([executionId, timestamp]) +} + +model TaskTemplate { + id String @id @default(cuid()) + name String + description String? + type String // "http", "email", "script", "database", "approval" + category String // "integration", "notification", "transformation", "approval" + icon String? + schema Json // JSON Schema for task config + defaultConfig Json // Default configuration + examples Json? // Example configurations + version String @default("1.0.0") + isPublic Boolean @default(true) + createdBy String + user User @relation(fields: [createdBy], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ApprovalTask { + id String @id @default(cuid()) + taskId String @unique + task TaskExecution @relation(fields: [taskId], references: [id], onDelete: Cascade) + approvers String[] // User IDs + approved Boolean? + approvedBy String? // User ID + rejectedBy String? // User ID + comment String? + decidedAt DateTime? + expiresAt DateTime? + + @@index([approvedBy]) + @@index([decidedAt]) +} + +model Schedule { + id String @id @default(cuid()) + workflowId String + name String + cronExpression String + timezone String @default("UTC") + input Json? // Input data for execution + enabled Boolean @default(true) + lastRun DateTime? + nextRun DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([enabled, nextRun]) +} + +model User { + id String @id @default(cuid()) + email String @unique + name String + role String // "admin", "editor", "viewer", "approver" + workflows Workflow[] + templates TaskTemplate[] + createdAt DateTime @default(now()) +} + +model Tag { + id String @id @default(cuid()) + name String @unique + color String + workflows Workflow[] +} +``` + +### Application Structure + +``` +workflow-engine/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ schemas/ +β”‚ β”‚ β”œβ”€β”€ workflows.schema.ts # Workflow list & management +β”‚ β”‚ β”œβ”€β”€ builder.schema.ts # Visual workflow builder +β”‚ β”‚ β”œβ”€β”€ executions.schema.ts # Execution monitoring +β”‚ β”‚ β”œβ”€β”€ templates.schema.ts # Task template library +β”‚ β”‚ β”œβ”€β”€ approvals.schema.ts # Approval queue +β”‚ β”‚ └── analytics.schema.ts # Workflow analytics +β”‚ β”œβ”€β”€ api/ +β”‚ β”‚ β”œβ”€β”€ workflows/ +β”‚ β”‚ β”œβ”€β”€ executions/ +β”‚ β”‚ β”œβ”€β”€ tasks/ +β”‚ β”‚ β”œβ”€β”€ approvals/ +β”‚ β”‚ └── schedules/ +β”‚ β”œβ”€β”€ engine/ +β”‚ β”‚ β”œβ”€β”€ executor.ts # Workflow execution engine +β”‚ β”‚ β”œβ”€β”€ task-handlers/ # Task type implementations +β”‚ β”‚ β”‚ β”œβ”€β”€ http.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ email.ts +β”‚ β”‚ β”‚ β”œβ”€β”€ script.ts +β”‚ β”‚ β”‚ └── database.ts +β”‚ β”‚ β”œβ”€β”€ condition-evaluator.ts # Conditional logic +β”‚ β”‚ └── retry-policy.ts # Retry mechanism +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ FlowCanvas.tsx # Workflow canvas +β”‚ β”‚ β”œβ”€β”€ TaskNode.tsx # Task node component +β”‚ β”‚ β”œβ”€β”€ ExecutionTimeline.tsx # Execution visualization +β”‚ β”‚ └── ApprovalCard.tsx # Approval request card +β”‚ └── lib/ +β”‚ β”œβ”€β”€ prisma.ts # Database client +β”‚ β”œβ”€β”€ queue.ts # Bull queue setup +β”‚ └── s3.ts # Artifact storage +β”œβ”€β”€ workers/ +β”‚ β”œβ”€β”€ workflow-executor.ts # Background workflow executor +β”‚ └── scheduler.ts # Scheduled workflow trigger +β”œβ”€β”€ prisma/ +β”‚ └── schema.prisma +└── package.json +``` + +--- + +## Implementation Guide + +### Part 1: Project Setup (30 minutes) + +#### 1. Initialize Project + +```bash +# Create Next.js app +npx create-next-app@latest workflow-engine --typescript --tailwind --app +cd workflow-engine + +# Install dependencies +npm install @object-ui/react @object-ui/components @object-ui/fields +npm install @prisma/client bull ioredis +npm install cron-parser date-fns axios +npm install reactflow +npm install -D prisma @types/bull @types/cron +``` + +#### 2. Setup Database and Redis + +```bash +# Initialize Prisma +npx prisma init + +# Update .env +DATABASE_URL="postgresql://user:password@localhost:5432/workflow?schema=public" +REDIS_URL="redis://localhost:6379" + +# Create schema and migrate +npx prisma db push +npx prisma generate +``` + +#### 3. Configure ObjectUI + +```tsx title="src/app/providers.tsx" +'use client'; + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; + +export function Providers({ children }: { children: React.ReactNode }) { + React.useEffect(() => { + registerAllComponents(ComponentRegistry); + registerAllFields(); + }, []); + + return <>{children}; +} +``` + +### Part 2: Workflow List Schema (1 hour) + +```typescript title="src/schemas/workflows.schema.ts" +export const workflowsSchema = { + type: 'page', + className: 'min-h-screen bg-gray-50 dark:bg-gray-900', + children: [ + { + type: 'app-shell', + header: { + type: 'header-bar', + title: 'Workflow Engine', + logo: '/logo.svg', + navigation: [ + { label: 'Workflows', href: '/', icon: 'GitBranch' }, + { label: 'Executions', href: '/executions', icon: 'Play' }, + { label: 'Templates', href: '/templates', icon: 'Library' }, + { label: 'Approvals', href: '/approvals', icon: 'CheckSquare' }, + { label: 'Schedules', href: '/schedules', icon: 'Clock' }, + { label: 'Analytics', href: '/analytics', icon: 'BarChart3' }, + ], + userMenu: { + name: '{{user.name}}', + email: '{{user.email}}', + items: [ + { label: 'Profile', href: '/profile' }, + { label: 'Settings', href: '/settings' }, + { label: 'Logout', action: { type: 'logout' } }, + ], + }, + }, + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Workflows', + className: 'text-2xl font-bold', + }, + { + type: 'button', + text: 'Create Workflow', + icon: 'Plus', + onClick: { + type: 'navigate', + href: '/workflows/new', + }, + }, + ], + }, + + // Tabs for workflow status + { + type: 'tabs', + defaultValue: 'all', + className: 'mb-6', + items: [ + { value: 'all', label: 'All Workflows' }, + { value: 'active', label: 'Active' }, + { value: 'draft', label: 'Draft' }, + { value: 'archived', label: 'Archived' }, + ], + content: [ + { + value: 'all', + children: [ + { + type: 'data-grid', + dataSource: { + api: '/api/workflows', + method: 'GET', + }, + columns: [ + { + key: 'name', + title: 'Workflow Name', + sortable: true, + render: { + type: 'flex', + direction: 'column', + children: [ + { + type: 'text', + content: '{{name}}', + className: 'font-medium', + }, + { + type: 'text', + content: '{{description}}', + className: 'text-sm text-gray-500', + }, + ], + }, + }, + { + key: 'status', + title: 'Status', + width: 120, + render: { + type: 'badge', + text: '{{status}}', + variant: '{{statusVariant}}', + }, + }, + { + key: 'version', + title: 'Version', + width: 100, + render: { + type: 'text', + content: 'v{{version}}', + }, + }, + { + key: 'executions', + title: 'Executions', + width: 120, + render: { + type: 'text', + content: '{{executionCount}} runs', + }, + }, + { + key: 'successRate', + title: 'Success Rate', + width: 130, + render: { + type: 'progress', + value: '{{successRate}}', + showLabel: true, + }, + }, + { + key: 'updatedAt', + title: 'Last Modified', + type: 'datetime', + width: 160, + }, + { + key: 'actions', + title: 'Actions', + width: 200, + render: { + type: 'flex', + gap: 2, + children: [ + { + type: 'button', + text: 'Edit', + size: 'sm', + variant: 'outline', + onClick: { + type: 'navigate', + href: '/workflows/{{id}}/edit', + }, + }, + { + type: 'button', + text: 'Run', + size: 'sm', + onClick: { + type: 'dialog', + title: 'Execute Workflow', + content: { + type: 'form', + fields: [ + { + type: 'field', + fieldType: 'json', + name: 'input', + label: 'Input Data (JSON)', + placeholder: '{"key": "value"}', + }, + ], + onSubmit: { + type: 'api', + method: 'POST', + url: '/api/workflows/{{id}}/execute', + onSuccess: { + type: 'notification', + message: 'Workflow execution started', + }, + }, + }, + }, + }, + ], + }, + }, + ], + pageSize: 20, + sortable: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + + +### Part 3: Visual Workflow Builder (3 hours) + +```typescript title="src/schemas/builder.schema.ts" +export const builderSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'h-screen flex flex-col', + children: [ + // Top toolbar + { + type: 'flex', + justify: 'between', + className: 'p-4 border-b bg-white', + children: [ + { + type: 'flex', + gap: 4, + align: 'center', + children: [ + { + type: 'button', + text: 'Back', + variant: 'ghost', + icon: 'ArrowLeft', + onClick: { type: 'navigate', href: '/workflows' }, + }, + { + type: 'field', + fieldType: 'text', + name: 'name', + placeholder: 'Workflow Name', + className: 'text-lg font-semibold', + }, + ], + }, + { + type: 'flex', + gap: 3, + children: [ + { + type: 'button', + text: 'Validate', + variant: 'outline', + onClick: { + type: 'api', + method: 'POST', + url: '/api/workflows/validate', + data: '{{workflow}}', + }, + }, + { + type: 'button', + text: 'Save Draft', + variant: 'outline', + onClick: { + type: 'api', + method: 'POST', + url: '/api/workflows', + data: '{{workflow}}', + }, + }, + { + type: 'button', + text: 'Publish', + onClick: { + type: 'api', + method: 'POST', + url: '/api/workflows/publish', + data: '{{workflow}}', + }, + }, + ], + }, + ], + }, + + // Main canvas area + { + type: 'grid', + cols: 4, + className: 'flex-1 overflow-hidden', + children: [ + // Task palette + { + type: 'card', + className: 'col-span-1 overflow-y-auto border-r', + children: [ + { + type: 'accordion', + items: [ + { + title: 'Integrations', + children: [ + { + type: 'task-palette', + tasks: [ + { + type: 'http', + icon: 'Globe', + label: 'HTTP Request', + color: 'blue', + }, + { + type: 'database', + icon: 'Database', + label: 'Database Query', + color: 'green', + }, + { + type: 'email', + icon: 'Mail', + label: 'Send Email', + color: 'purple', + }, + ], + }, + ], + }, + { + title: 'Control Flow', + children: [ + { + type: 'task-palette', + tasks: [ + { + type: 'condition', + icon: 'GitBranch', + label: 'Condition', + color: 'yellow', + }, + { + type: 'loop', + icon: 'RotateCw', + label: 'Loop', + color: 'orange', + }, + { + type: 'parallel', + icon: 'Layers', + label: 'Parallel', + color: 'cyan', + }, + { + type: 'wait', + icon: 'Clock', + label: 'Wait', + color: 'gray', + }, + ], + }, + ], + }, + { + title: 'Human Tasks', + children: [ + { + type: 'task-palette', + tasks: [ + { + type: 'approval', + icon: 'CheckSquare', + label: 'Approval', + color: 'indigo', + }, + { + type: 'manual', + icon: 'Hand', + label: 'Manual Task', + color: 'pink', + }, + ], + }, + ], + }, + { + title: 'Data', + children: [ + { + type: 'task-palette', + tasks: [ + { + type: 'transform', + icon: 'Code', + label: 'Transform Data', + color: 'violet', + }, + { + type: 'script', + icon: 'FileCode', + label: 'Run Script', + color: 'amber', + }, + ], + }, + ], + }, + ], + }, + ], + }, + + // Canvas + { + type: 'flow-canvas', + className: 'col-span-3', + nodes: '{{workflow.nodes}}', + edges: '{{workflow.edges}}', + onNodeAdd: { + type: 'add-node', + }, + onNodeUpdate: { + type: 'update-node', + }, + onNodeDelete: { + type: 'delete-node', + }, + onEdgeAdd: { + type: 'add-edge', + }, + onEdgeDelete: { + type: 'delete-edge', + }, + }, + ], + }, + + // Task configuration panel + { + type: 'drawer', + open: '{{selectedNode != null}}', + side: 'right', + title: 'Task Configuration', + width: 400, + children: [ + { + type: 'form', + data: '{{selectedNode}}', + onChange: { + type: 'update-node-config', + }, + fields: [ + { + type: 'field', + fieldType: 'text', + name: 'name', + label: 'Task Name', + required: true, + }, + { + type: 'field', + fieldType: 'textarea', + name: 'description', + label: 'Description', + }, + { + type: 'field', + fieldType: 'number', + name: 'timeout', + label: 'Timeout (seconds)', + min: 1, + max: 3600, + }, + { + type: 'field', + fieldType: 'number', + name: 'maxRetries', + label: 'Max Retries', + min: 0, + max: 10, + default: 0, + }, + { + type: 'conditional', + condition: '{{selectedNode.type === "http"}}', + children: [ + { + type: 'field', + fieldType: 'select', + name: 'method', + label: 'HTTP Method', + required: true, + options: [ + { value: 'GET', label: 'GET' }, + { value: 'POST', label: 'POST' }, + { value: 'PUT', label: 'PUT' }, + { value: 'DELETE', label: 'DELETE' }, + { value: 'PATCH', label: 'PATCH' }, + ], + }, + { + type: 'field', + fieldType: 'text', + name: 'url', + label: 'URL', + required: true, + placeholder: 'https://api.example.com/endpoint', + }, + { + type: 'field', + fieldType: 'json', + name: 'headers', + label: 'Headers', + }, + { + type: 'field', + fieldType: 'json', + name: 'body', + label: 'Request Body', + }, + ], + }, + { + type: 'conditional', + condition: '{{selectedNode.type === "email"}}', + children: [ + { + type: 'field', + fieldType: 'text', + name: 'to', + label: 'To', + required: true, + }, + { + type: 'field', + fieldType: 'text', + name: 'subject', + label: 'Subject', + required: true, + }, + { + type: 'field', + fieldType: 'rich-text', + name: 'body', + label: 'Email Body', + required: true, + }, + ], + }, + { + type: 'conditional', + condition: '{{selectedNode.type === "condition"}}', + children: [ + { + type: 'field', + fieldType: 'code', + name: 'expression', + label: 'Condition Expression', + language: 'javascript', + required: true, + placeholder: 'return context.amount > 1000;', + }, + ], + }, + { + type: 'conditional', + condition: '{{selectedNode.type === "approval"}}', + children: [ + { + type: 'field', + fieldType: 'multi-select', + name: 'approvers', + label: 'Approvers', + required: true, + dataSource: { + api: '/api/users?role=approver', + method: 'GET', + }, + }, + { + type: 'field', + fieldType: 'number', + name: 'expiryHours', + label: 'Expiry (hours)', + default: 24, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +}; +``` + +### Part 4: Execution Monitoring (2 hours) + +```typescript title="src/schemas/executions.schema.ts" +export const executionsSchema = { + type: 'page', + children: [ + { + type: 'container', + className: 'p-8', + children: [ + { + type: 'flex', + justify: 'between', + className: 'mb-6', + children: [ + { + type: 'text', + content: 'Workflow Executions', + className: 'text-2xl font-bold', + }, + { + type: 'flex', + gap: 3, + children: [ + { + type: 'field', + fieldType: 'date-range', + name: 'dateRange', + placeholder: 'Filter by date', + }, + { + type: 'button', + text: 'Refresh', + variant: 'outline', + icon: 'RefreshCw', + onClick: { + type: 'refresh', + }, + }, + ], + }, + ], + }, + + // Status tabs + { + type: 'tabs', + defaultValue: 'all', + className: 'mb-6', + items: [ + { value: 'all', label: 'All', count: '{{counts.all}}' }, + { value: 'running', label: 'Running', count: '{{counts.running}}' }, + { value: 'completed', label: 'Completed', count: '{{counts.completed}}' }, + { value: 'failed', label: 'Failed', count: '{{counts.failed}}' }, + ], + content: [ + { + value: 'all', + children: [ + { + type: 'data-table', + dataSource: { + api: '/api/executions', + method: 'GET', + realtime: true, + refreshInterval: 5000, + }, + columns: [ + { + key: 'workflow', + title: 'Workflow', + render: { + type: 'text', + content: '{{workflow.name}}', + className: 'font-medium', + }, + }, + { + key: 'status', + title: 'Status', + width: 120, + render: { + type: 'badge', + text: '{{status}}', + variant: '{{statusVariant}}', + pulse: '{{status === "running"}}', + }, + }, + { + key: 'progress', + title: 'Progress', + width: 150, + render: { + type: 'progress', + value: '{{progress}}', + showLabel: true, + }, + }, + { + key: 'duration', + title: 'Duration', + width: 100, + render: { + type: 'text', + content: '{{formattedDuration}}', + }, + }, + { + key: 'startedAt', + title: 'Started', + type: 'datetime', + width: 160, + }, + { + key: 'actions', + title: 'Actions', + width: 150, + render: { + type: 'flex', + gap: 2, + children: [ + { + type: 'button', + text: 'View', + size: 'sm', + variant: 'outline', + onClick: { + type: 'dialog', + title: 'Execution Details', + size: 'xl', + content: { + type: 'include', + schema: 'execution-detail', + props: { executionId: '{{id}}' }, + }, + }, + }, + { + type: 'button', + text: 'Cancel', + size: 'sm', + variant: 'destructive', + visible: '{{status === "running"}}', + onClick: { + type: 'api', + method: 'POST', + url: '/api/executions/{{id}}/cancel', + confirm: 'Cancel this execution?', + }, + }, + ], + }, + }, + ], + pageSize: 25, + }, + ], + }, + ], + }, + ], + }, + ], +}; + +// Execution Detail Schema +export const executionDetailSchema = { + type: 'container', + children: [ + { + type: 'grid', + cols: 3, + gap: 6, + className: 'mb-6', + children: [ + { + type: 'card', + title: 'Execution Info', + children: [ + { + type: 'dl-list', + items: [ + { label: 'Workflow', value: '{{workflow.name}}' }, + { label: 'Status', value: '{{status}}' }, + { label: 'Started', value: '{{startedAt}}' }, + { label: 'Completed', value: '{{completedAt}}' }, + { label: 'Duration', value: '{{formattedDuration}}' }, + { label: 'Retry Count', value: '{{retryCount}}' }, + ], + }, + ], + }, + { + type: 'card', + title: 'Input Data', + children: [ + { + type: 'code-block', + language: 'json', + content: '{{JSON.stringify(input, null, 2)}}', + }, + ], + }, + { + type: 'card', + title: 'Output Data', + children: [ + { + type: 'code-block', + language: 'json', + content: '{{JSON.stringify(output, null, 2)}}', + }, + ], + }, + ], + }, + + { + type: 'card', + title: 'Task Timeline', + className: 'mb-6', + children: [ + { + type: 'execution-timeline', + tasks: '{{tasks}}', + }, + ], + }, + + { + type: 'card', + title: 'Execution Events', + children: [ + { + type: 'data-table', + data: '{{events}}', + columns: [ + { key: 'timestamp', title: 'Time', type: 'datetime', width: 180 }, + { + key: 'type', + title: 'Event', + render: { + type: 'badge', + text: '{{type}}', + }, + }, + { key: 'message', title: 'Message' }, + { key: 'taskId', title: 'Task', width: 150 }, + ], + pageSize: 15, + }, + ], + }, + ], +}; +``` + +### Part 5: Workflow Engine Implementation (3 hours) + +```typescript title="src/engine/executor.ts" +import { prisma } from '@/lib/prisma'; +import { workflowQueue } from '@/lib/queue'; +import { TaskHandler } from './task-handlers'; + +export class WorkflowExecutor { + async execute(executionId: string) { + const execution = await prisma.workflowExecution.findUnique({ + where: { id: executionId }, + include: { + workflow: true, + tasks: true, + }, + }); + + if (!execution) { + throw new Error('Execution not found'); + } + + try { + // Update status to running + await prisma.workflowExecution.update({ + where: { id: executionId }, + data: { + status: 'running', + startedAt: new Date(), + }, + }); + + // Execute workflow DAG + const definition = execution.workflow.definition as any; + const context = { ...execution.input, ...execution.context }; + + const result = await this.executeDAG( + definition, + context, + executionId + ); + + // Mark as completed + await prisma.workflowExecution.update({ + where: { id: executionId }, + data: { + status: 'completed', + output: result, + completedAt: new Date(), + duration: Date.now() - execution.startedAt!.getTime(), + }, + }); + } catch (error) { + // Mark as failed + await prisma.workflowExecution.update({ + where: { id: executionId }, + data: { + status: 'failed', + error: error.message, + completedAt: new Date(), + }, + }); + throw error; + } + } + + private async executeDAG( + definition: any, + context: any, + executionId: string + ) { + const { nodes, edges } = definition; + const completed = new Set(); + const results: Record = {}; + + // Find start node + const startNode = nodes.find((n: any) => n.type === 'start'); + if (!startNode) { + throw new Error('No start node found'); + } + + // Execute nodes in topological order + await this.executeNode( + startNode.id, + nodes, + edges, + context, + executionId, + completed, + results + ); + + return results; + } + + private async executeNode( + nodeId: string, + nodes: any[], + edges: any[], + context: any, + executionId: string, + completed: Set, + results: Record + ) { + if (completed.has(nodeId)) { + return; + } + + const node = nodes.find((n) => n.id === nodeId); + if (!node) { + throw new Error(`Node ${nodeId} not found`); + } + + // Create task execution record + const taskExecution = await prisma.taskExecution.create({ + data: { + executionId, + taskId: nodeId, + taskType: node.type, + name: node.data.name, + config: node.data.config, + status: 'pending', + }, + }); + + try { + // Execute task + const handler = TaskHandler.getHandler(node.type); + const result = await handler.execute( + node.data.config, + context, + taskExecution.id + ); + + // Update task as completed + await prisma.taskExecution.update({ + where: { id: taskExecution.id }, + data: { + status: 'completed', + output: result, + completedAt: new Date(), + }, + }); + + results[nodeId] = result; + completed.add(nodeId); + + // Execute next nodes + const outgoingEdges = edges.filter((e) => e.source === nodeId); + + for (const edge of outgoingEdges) { + // Check condition if edge has one + if (edge.condition) { + const shouldExecute = this.evaluateCondition( + edge.condition, + context, + results + ); + if (!shouldExecute) continue; + } + + await this.executeNode( + edge.target, + nodes, + edges, + { ...context, ...result }, + executionId, + completed, + results + ); + } + } catch (error) { + // Handle retry logic + if (taskExecution.retryCount < node.data.config.maxRetries) { + await this.retryTask(taskExecution.id, error); + } else { + await prisma.taskExecution.update({ + where: { id: taskExecution.id }, + data: { + status: 'failed', + error: error.message, + completedAt: new Date(), + }, + }); + throw error; + } + } + } + + private async retryTask(taskId: string, error: any) { + const task = await prisma.taskExecution.findUnique({ + where: { id: taskId }, + }); + + if (!task) return; + + await prisma.taskExecution.update({ + where: { id: taskId }, + data: { + retryCount: task.retryCount + 1, + status: 'pending', + }, + }); + + await prisma.taskEvent.create({ + data: { + taskId, + type: 'retry', + message: `Retrying after error: ${error.message}`, + }, + }); + + // Re-queue the task + // Implementation depends on your queue system + } + + private evaluateCondition( + condition: string, + context: any, + results: any + ): boolean { + try { + const func = new Function('context', 'results', condition); + return func(context, results); + } catch (error) { + console.error('Condition evaluation error:', error); + return false; + } + } +} +``` + +```typescript title="src/engine/task-handlers/http.ts" +import axios from 'axios'; + +export class HttpTaskHandler { + async execute(config: any, context: any, taskId: string) { + const { method, url, headers, body } = config; + + // Replace variables in URL and body + const processedUrl = this.replaceVariables(url, context); + const processedBody = body ? this.replaceVariables( + JSON.stringify(body), + context + ) : undefined; + + const response = await axios({ + method, + url: processedUrl, + headers, + data: processedBody ? JSON.parse(processedBody) : undefined, + timeout: config.timeout || 30000, + }); + + return { + status: response.status, + headers: response.headers, + data: response.data, + }; + } + + private replaceVariables(template: string, context: any): string { + return template.replace(/\{\{(.+?)\}\}/g, (_, key) => { + const value = key.split('.').reduce((obj: any, k: string) => obj?.[k], context); + return value !== undefined ? String(value) : ''; + }); + } +} +``` + +### Part 6: API Implementation (2 hours) + +```typescript title="src/app/api/workflows/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const status = searchParams.get('status'); + + const where = status && status !== 'all' ? { status } : {}; + + const workflows = await prisma.workflow.findMany({ + where, + include: { + tags: true, + _count: { + select: { executions: true }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); + + // Calculate success rate + const enrichedWorkflows = await Promise.all( + workflows.map(async (workflow) => { + const executions = await prisma.workflowExecution.groupBy({ + by: ['status'], + where: { workflowId: workflow.id }, + _count: true, + }); + + const total = executions.reduce((sum, e) => sum + e._count, 0); + const completed = executions.find((e) => e.status === 'completed')?._count || 0; + const successRate = total > 0 ? (completed / total) * 100 : 0; + + return { + ...workflow, + executionCount: total, + successRate: Math.round(successRate), + statusVariant: workflow.status === 'active' ? 'success' : 'secondary', + }; + }) + ); + + return NextResponse.json(enrichedWorkflows); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + + const workflow = await prisma.workflow.create({ + data: { + name: body.name, + description: body.description, + definition: body.definition, + config: body.config, + status: body.status || 'draft', + createdBy: body.userId, + }, + }); + + return NextResponse.json(workflow); +} +``` + +```typescript title="src/app/api/workflows/[id]/execute/route.ts" +import { NextRequest, NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { workflowQueue } from '@/lib/queue'; + +export async function POST( + request: NextRequest, + { params }: { params: { id: string } } +) { + const body = await request.json(); + + const workflow = await prisma.workflow.findUnique({ + where: { id: params.id }, + }); + + if (!workflow) { + return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }); + } + + // Create execution record + const execution = await prisma.workflowExecution.create({ + data: { + workflowId: workflow.id, + status: 'pending', + input: body.input || {}, + triggeredBy: body.userId, + }, + }); + + // Queue for execution + await workflowQueue.add('execute-workflow', { + executionId: execution.id, + }); + + return NextResponse.json(execution); +} +``` + +--- + +## Key Features Implementation + +### 1. Visual Workflow Builder + +Features: +- Drag-and-drop canvas +- Task palette with 10+ task types +- Real-time validation +- Auto-save functionality + +### 2. Task Types + +Available task types: +- HTTP Request +- Database Query +- Send Email +- Run Script +- Approval Task +- Conditional Branch +- Loop/Iteration +- Wait/Delay + +### 3. Error Handling + +Features: +- Automatic retry with backoff +- Custom error handlers +- Timeout management +- Error notifications + +### 4. Audit Logging + +Features: +- Complete execution history +- Task-level events +- Performance metrics +- Error tracking + +--- + +## Deployment + +### 1. Build for Production + +```bash +npm run build +``` + +### 2. Deploy to Vercel + +```bash +npm i -g vercel +vercel --prod +``` + +### 3. Configure Environment + +```env +DATABASE_URL=postgresql://... +REDIS_URL=redis://... +AWS_S3_BUCKET=workflow-artifacts +SMTP_HOST=smtp.sendgrid.net +SMTP_USER=apikey +SMTP_PASS=... +``` + +### 4. Start Workers + +```bash +# Run workflow executor worker +node workers/workflow-executor.js + +# Run scheduler worker +node workers/scheduler.js +``` + +--- + +## Next Steps + +### Feature Enhancements + +- [ ] Sub-workflows +- [ ] Workflow versioning +- [ ] A/B testing for workflows +- [ ] Webhook triggers +- [ ] SLA monitoring +- [ ] Custom task plugins + +### Performance Optimizations + +- [ ] Parallel task execution +- [ ] Workflow caching +- [ ] Database query optimization +- [ ] Queue prioritization + +### Scaling + +- [ ] Distributed execution +- [ ] Horizontal worker scaling +- [ ] Event sourcing +- [ ] Multi-tenancy + +--- + +## Resources + +- [**Source Code**](https://github.com/objectstack-ai/objectui/tree/main/examples/workflow-engine) +- [**Live Demo**](https://workflow-demo.objectui.org) +- [**API Documentation**](https://workflow-demo.objectui.org/api-docs) + +--- + +## Community + +Share your workflow implementations: +- [GitHub Discussions](https://github.com/objectstack-ai/objectui/discussions) +- [Discord](https://discord.gg/objectui) diff --git a/content/docs/guide/custom-components.mdx b/content/docs/guide/custom-components.mdx new file mode 100644 index 00000000..346fee14 --- /dev/null +++ b/content/docs/guide/custom-components.mdx @@ -0,0 +1,615 @@ +--- +title: "Custom Component Development" +description: "Learn how to create custom components for ObjectUI" +--- + +# Custom Component Development + +Extend ObjectUI with your own custom components. This guide covers everything from basic components to advanced patterns with full TypeScript support. + +## Overview + +Creating custom components in ObjectUI involves three main steps: + +1. **Define the component schema types** - TypeScript interfaces for configuration +2. **Build the React component** - The actual UI implementation +3. **Register the component** - Make it available to the schema renderer + +## Basic Custom Component + +Let's create a simple "Counter" component. + +### Step 1: Define the Schema Type + +```typescript title="src/components/counter/counter.schema.ts" +import { ComponentSchema } from '@object-ui/types'; + +export interface CounterSchema extends ComponentSchema { + type: 'counter'; + initialValue?: number; + step?: number; + min?: number; + max?: number; + label?: string; + onValueChange?: { + type: 'log' | 'notify' | 'api'; + message?: string; + }; +} +``` + +### Step 2: Build the Component + +```tsx title="src/components/counter/Counter.tsx" +import React, { useState } from 'react'; +import { Button } from '@object-ui/components'; +import { CounterSchema } from './counter.schema'; + +export interface CounterProps { + schema: CounterSchema; +} + +export function Counter({ schema }: CounterProps) { + const { + initialValue = 0, + step = 1, + min = -Infinity, + max = Infinity, + label = 'Counter', + className, + } = schema; + + const [value, setValue] = useState(initialValue); + + const increment = () => { + const newValue = Math.min(value + step, max); + setValue(newValue); + handleValueChange(newValue); + }; + + const decrement = () => { + const newValue = Math.max(value - step, min); + setValue(newValue); + handleValueChange(newValue); + }; + + const handleValueChange = (newValue: number) => { + if (schema.onValueChange) { + // Handle the action + if (schema.onValueChange.type === 'log') { + console.log(`Counter value: ${newValue}`); + } + } + }; + + return ( +
+ {label}: +
+ + {value} + +
+
+ ); +} +``` + +### Step 3: Register the Component + +```typescript title="src/components/counter/index.ts" +import { ComponentRegistry } from '@object-ui/core'; +import { Counter } from './Counter'; +import { CounterSchema } from './counter.schema'; + +export function registerCounter() { + ComponentRegistry.register('counter', Counter); +} + +export { Counter, type CounterSchema }; +``` + +### Step 4: Use the Component + +```tsx title="src/App.tsx" +import { SchemaRenderer } from '@object-ui/react'; +import { registerCounter } from './components/counter'; + +// Register your custom component +registerCounter(); + +const schema = { + type: 'page', + children: [ + { + type: 'counter', + label: 'Quantity', + initialValue: 5, + step: 1, + min: 0, + max: 100, + onValueChange: { + type: 'log', + }, + }, + ], +}; + +function App() { + return ; +} +``` + +## Advanced Component with Children + +Create a component that can contain other components: + +```typescript title="src/components/panel/panel.schema.ts" +import { ComponentSchema } from '@object-ui/types'; + +export interface PanelSchema extends ComponentSchema { + type: 'panel'; + title: string; + collapsible?: boolean; + defaultExpanded?: boolean; + icon?: string; + variant?: 'default' | 'bordered' | 'elevated'; + children?: ComponentSchema[]; +} +``` + +```tsx title="src/components/panel/Panel.tsx" +import React, { useState } from 'react'; +import { SchemaRenderer } from '@object-ui/react'; +import { Card, CardHeader, CardTitle, CardContent } from '@object-ui/components'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { PanelSchema } from './panel.schema'; + +export interface PanelProps { + schema: PanelSchema; +} + +export function Panel({ schema }: PanelProps) { + const { + title, + collapsible = false, + defaultExpanded = true, + icon, + variant = 'default', + children = [], + className, + } = schema; + + const [expanded, setExpanded] = useState(defaultExpanded); + + const variantClasses = { + default: '', + bordered: 'border-2 border-primary', + elevated: 'shadow-lg', + }; + + return ( + + collapsible && setExpanded(!expanded)} + > + +
+ {icon && {icon}} + {title} +
+ {collapsible && ( + {expanded ? : } + )} +
+
+ {expanded && ( + + {children.map((child, index) => ( + + ))} + + )} +
+ ); +} +``` + +## Component with Data Fetching + +Create a component that fetches and displays data: + +```tsx title="src/components/user-list/UserList.tsx" +import React, { useEffect, useState } from 'react'; +import { Card } from '@object-ui/components'; + +export interface UserListSchema extends ComponentSchema { + type: 'user-list'; + apiUrl: string; + pageSize?: number; +} + +interface User { + id: string; + name: string; + email: string; +} + +export function UserList({ schema }: { schema: UserListSchema }) { + const { apiUrl, pageSize = 10 } = schema; + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(apiUrl) + .then((res) => res.json()) + .then((data) => { + setUsers(data.slice(0, pageSize)); + setLoading(false); + }) + .catch((error) => { + console.error('Failed to fetch users:', error); + setLoading(false); + }); + }, [apiUrl, pageSize]); + + if (loading) { + return
Loading...
; + } + + return ( +
+ {users.map((user) => ( + +

{user.name}

+

{user.email}

+
+ ))} +
+ ); +} +``` + +## Component with Context + +Share state across multiple components: + +```tsx title="src/components/theme-provider/ThemeProvider.tsx" +import React, { createContext, useContext, useState } from 'react'; +import { ComponentSchema } from '@object-ui/types'; +import { SchemaRenderer } from '@object-ui/react'; + +interface ThemeContextType { + theme: 'light' | 'dark'; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within ThemeProvider'); + } + return context; +} + +export interface ThemeProviderSchema extends ComponentSchema { + type: 'theme-provider'; + defaultTheme?: 'light' | 'dark'; + children?: ComponentSchema[]; +} + +export function ThemeProvider({ schema }: { schema: ThemeProviderSchema }) { + const { defaultTheme = 'light', children = [] } = schema; + const [theme, setTheme] = useState<'light' | 'dark'>(defaultTheme); + + const toggleTheme = () => { + setTheme((prev) => (prev === 'light' ? 'dark' : 'light')); + }; + + return ( + +
+ {children.map((child, index) => ( + + ))} +
+
+ ); +} +``` + +## Best Practices + +### 1. Use TypeScript + +Always define proper types for your schema: + +```typescript +// βœ… Good +export interface MyComponentSchema extends ComponentSchema { + type: 'my-component'; + requiredProp: string; + optionalProp?: number; +} + +// ❌ Bad +export interface MyComponentSchema { + type: string; + [key: string]: any; +} +``` + +### 2. Handle Default Values + +```tsx +// βœ… Good +export function MyComponent({ schema }: MyComponentProps) { + const { + title = 'Default Title', + variant = 'default', + size = 'medium', + } = schema; + // ... +} + +// ❌ Bad +export function MyComponent({ schema }: MyComponentProps) { + // Accessing properties without defaults + return
{schema.title}
; +} +``` + +### 3. Validate Props + +```tsx +export function MyComponent({ schema }: MyComponentProps) { + // Validate required props + if (!schema.dataSource) { + console.error('MyComponent requires a dataSource'); + return null; + } + + // Validate prop values + if (schema.columns && schema.columns.length === 0) { + console.warn('MyComponent has no columns defined'); + } + + // Continue with rendering... +} +``` + +### 4. Use Composition + +Reuse existing components: + +```tsx +import { Card, Button, Badge } from '@object-ui/components'; + +export function ProductCard({ schema }: { schema: ProductCardSchema }) { + return ( + + + {schema.title} + {schema.category} + + +

{schema.description}

+ +
+
+ ); +} +``` + +### 5. Export Everything + +```typescript title="src/components/my-component/index.ts" +export { MyComponent } from './MyComponent'; +export { type MyComponentSchema } from './my-component.schema'; +export { registerMyComponent } from './register'; +``` + +## Component Naming Conventions + +Follow these naming conventions: + +- **Component File**: `MyComponent.tsx` (PascalCase) +- **Schema File**: `my-component.schema.ts` (kebab-case) +- **Schema Type**: `MyComponentSchema` (PascalCase + Schema suffix) +- **Schema Type Value**: `'my-component'` (kebab-case) +- **Register Function**: `registerMyComponent` (camelCase) + +## Testing Custom Components + +```tsx title="src/components/counter/Counter.test.tsx" +import { render, screen, fireEvent } from '@testing-library/react'; +import { Counter } from './Counter'; +import { CounterSchema } from './counter.schema'; + +describe('Counter', () => { + it('renders with initial value', () => { + const schema: CounterSchema = { + type: 'counter', + initialValue: 5, + label: 'Test Counter', + }; + + render(); + expect(screen.getByText('5')).toBeInTheDocument(); + }); + + it('increments value when + button clicked', () => { + const schema: CounterSchema = { + type: 'counter', + initialValue: 0, + step: 1, + }; + + render(); + const incrementButton = screen.getByText('+'); + fireEvent.click(incrementButton); + expect(screen.getByText('1')).toBeInTheDocument(); + }); + + it('respects max value', () => { + const schema: CounterSchema = { + type: 'counter', + initialValue: 9, + max: 10, + step: 1, + }; + + render(); + const incrementButton = screen.getByText('+'); + fireEvent.click(incrementButton); + expect(screen.getByText('10')).toBeInTheDocument(); + + // Should not increment beyond max + fireEvent.click(incrementButton); + expect(screen.getByText('10')).toBeInTheDocument(); + }); +}); +``` + +## Storybook Documentation + +Create stories for your component: + +```tsx title="src/components/counter/Counter.stories.tsx" +import type { Meta, StoryObj } from '@storybook/react'; +import { Counter } from './Counter'; +import { CounterSchema } from './counter.schema'; + +const meta: Meta = { + title: 'Components/Counter', + component: Counter, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + schema: { + type: 'counter', + label: 'Default Counter', + initialValue: 0, + }, + }, +}; + +export const WithMinMax: Story = { + args: { + schema: { + type: 'counter', + label: 'Limited Counter', + initialValue: 5, + min: 0, + max: 10, + step: 1, + }, + }, +}; + +export const LargeStep: Story = { + args: { + schema: { + type: 'counter', + label: 'Count by 5', + initialValue: 0, + step: 5, + }, + }, +}; +``` + +## Component Namespaces + +Avoid naming conflicts with namespaces: + +```typescript +import { ComponentRegistry } from '@object-ui/core'; + +// Register with namespace +ComponentRegistry.register('counter', Counter, { + namespace: 'myapp' +}); + +// Use in schema +const schema = { + type: 'myapp:counter', // Namespace prefix + initialValue: 0, +}; +``` + +## Distribution + +### 1. Create a Plugin Package + +```json title="package.json" +{ + "name": "@myorg/objectui-counter", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist"], + "peerDependencies": { + "@object-ui/core": "^0.4.0", + "@object-ui/react": "^0.4.0", + "react": "^18.0.0" + } +} +``` + +### 2. Build Configuration + +```typescript title="vite.config.ts" +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [react()], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'ObjectUICounter', + fileName: 'index', + }, + rollupOptions: { + external: ['react', 'react-dom', '@object-ui/core', '@object-ui/react'], + }, + }, +}); +``` + +### 3. Publish to NPM + +```bash +npm run build +npm publish --access public +``` + +## Next Steps + +- [Plugin Development](/docs/guide/plugin-development) - Package multiple components +- [Performance Optimization](/docs/guide/performance) - Optimize component rendering +- [Component Gallery](/docs/components) - Browse existing components +- [API Reference](/docs/api) - Component API documentation + +## Examples + +See complete examples: +- [Counter Component](https://github.com/objectstack-ai/objectui/tree/main/examples/custom-components/counter) +- [Panel Component](https://github.com/objectstack-ai/objectui/tree/main/examples/custom-components/panel) +- [User List Component](https://github.com/objectstack-ai/objectui/tree/main/examples/custom-components/user-list) diff --git a/content/docs/guide/installation.mdx b/content/docs/guide/installation.mdx new file mode 100644 index 00000000..64a92349 --- /dev/null +++ b/content/docs/guide/installation.mdx @@ -0,0 +1,413 @@ +--- +title: "Installation" +description: "Complete installation guide for ObjectUI" +--- + +# Installation + +This guide covers everything you need to install and configure ObjectUI in your project. + +## System Requirements + +- **Node.js**: 18.0 or later +- **Package Manager**: pnpm (recommended), npm, or yarn +- **React**: 18.0 or later +- **TypeScript**: 5.0 or later (optional but recommended) + +## Installation Methods + +### Method 1: Fresh React Project with Vite + +Create a new React + TypeScript project with Vite: + +```bash +# Create new Vite project +npm create vite@latest my-objectui-app -- --template react-ts +cd my-objectui-app + +# Install ObjectUI packages +npm install @object-ui/react @object-ui/components @object-ui/fields @object-ui/core + +# Install Tailwind CSS +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +### Method 2: Add to Existing Next.js App + +```bash +# Install ObjectUI packages +npm install @object-ui/react @object-ui/components @object-ui/fields @object-ui/core + +# Tailwind CSS (if not already installed) +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +### Method 3: Clone and Use Development Build + +For testing or contributing: + +```bash +git clone https://github.com/objectstack-ai/objectui.git +cd objectui +pnpm install +pnpm build +``` + +## Core Packages + +ObjectUI is split into focused packages for optimal tree-shaking: + +### Required Packages + +| Package | Purpose | Size | +|---------|---------|------| +| `@object-ui/core` | Schema engine and registry | ~15KB | +| `@object-ui/react` | React bindings and renderer | ~8KB | +| `@object-ui/components` | UI component library | ~120KB | +| `@object-ui/fields` | Form field widgets | ~80KB | + +### Optional Packages + +| Package | Purpose | When to Use | +|---------|---------|-------------| +| `@object-ui/layout` | Layout components | Building full applications | +| `@object-ui/plugin-form` | Advanced forms | Multi-step forms, validation | +| `@object-ui/plugin-grid` | Data grids | Tables with sorting/filtering | +| `@object-ui/plugin-kanban` | Kanban boards | Project management UIs | +| `@object-ui/plugin-charts` | Charts & graphs | Data visualization | +| `@object-ui/plugin-dashboard` | Dashboards | Analytics interfaces | +| `@object-ui/plugin-calendar` | Calendar views | Event scheduling | +| `@object-ui/plugin-gantt` | Gantt charts | Project timelines | +| `@object-ui/plugin-map` | Maps | Location-based features | +| `@object-ui/plugin-editor` | Rich text editor | Content editing | +| `@object-ui/plugin-markdown` | Markdown renderer | Documentation | +| `@object-ui/plugin-chatbot` | Chat interface | Conversational UIs | +| `@object-ui/data-objectstack` | ObjectStack integration | Using ObjectStack backend | + +## Tailwind CSS Configuration + +ObjectUI requires Tailwind CSS. Here's the complete setup: + +### 1. Install Tailwind + +```bash +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +### 2. Configure Tailwind + +Update `tailwind.config.js`: + +```js title="tailwind.config.js" +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: ['class'], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + // Include ObjectUI packages + './node_modules/@object-ui/**/*.{js,ts,jsx,tsx}', + ], + theme: { + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [require('tailwindcss-animate')], +} +``` + +### 3. Add CSS Variables + +Create or update your global CSS file: + +```css title="src/index.css" +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} +``` + +### 4. Install Required Plugin + +```bash +npm install -D tailwindcss-animate +``` + +## Component Registration + +ObjectUI uses a registry pattern. Components must be registered before use: + +### Option 1: Register All Components (Easiest) + +```tsx +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; + +// Register everything (larger bundle) +registerAllComponents(ComponentRegistry); +registerAllFields(); +``` + +### Option 2: Lazy Registration (Recommended) + +For smaller bundles, register only what you need: + +```tsx +import { ComponentRegistry } from '@object-ui/core'; +import { registerField } from '@object-ui/fields'; + +// Register specific components +ComponentRegistry.register('button', ButtonComponent); +ComponentRegistry.register('card', CardComponent); + +// Register specific fields +registerField('text'); +registerField('number'); +registerField('email'); +``` + +**Bundle size impact**: Lazy registration can reduce bundle size by 30-50%! + +## TypeScript Configuration + +For the best developer experience, configure TypeScript: + +```json title="tsconfig.json" +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Path aliases (optional) */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +## Verification + +Test your installation: + +```tsx title="src/App.tsx" +import { SchemaRenderer } from '@object-ui/react'; +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; + +registerAllComponents(ComponentRegistry); + +const schema = { + type: 'card', + title: 'βœ… ObjectUI is installed!', + children: [ + { + type: 'text', + content: 'If you can see this card, everything is working correctly.', + }, + ], +}; + +function App() { + return ; +} + +export default App; +``` + +## Next Steps + +- [Quick Start](/docs/guide/quick-start) - Build your first component +- [Zero to Deployment](/docs/guide/zero-to-deployment) - Complete tutorial +- [Schema Overview](/docs/guide/schema-overview) - Learn the schema structure +- [Component Registry](/docs/guide/component-registry) - Advanced registration + +## Troubleshooting + +### Issue: Styles not applying + +**Solution**: Ensure Tailwind content includes ObjectUI: + +```js +content: [ + "./node_modules/@object-ui/**/*.{js,ts,jsx,tsx}", +] +``` + +### Issue: Module not found + +**Solution**: Check package.json includes all required dependencies: + +```json +{ + "dependencies": { + "@object-ui/react": "latest", + "@object-ui/components": "latest", + "@object-ui/fields": "latest", + "@object-ui/core": "latest" + } +} +``` + +### Issue: TypeScript errors + +**Solution**: Ensure `skipLibCheck: true` in tsconfig.json + +### Issue: Dark mode not working + +**Solution**: Add dark mode toggle: + +```tsx +// Add to your root component +
{/* or light */} + +
+``` + +## Support + +- [GitHub Issues](https://github.com/objectstack-ai/objectui/issues) +- [Discussions](https://github.com/objectstack-ai/objectui/discussions) +- [Documentation](/docs) diff --git a/content/docs/guide/meta.json b/content/docs/guide/meta.json index 7e6fc6cf..14e01aac 100644 --- a/content/docs/guide/meta.json +++ b/content/docs/guide/meta.json @@ -1,6 +1,10 @@ { "title": "Guide", "pages": [ + "quick-start", + "installation", + "zero-to-deployment", + "video-tutorials", "architecture", "schema-rendering", "layout", @@ -9,6 +13,10 @@ "fields", "component-registry", "plugins", - "schema-overview" + "schema-overview", + "custom-components", + "plugin-development", + "performance", + "security" ] } diff --git a/content/docs/guide/performance.mdx b/content/docs/guide/performance.mdx new file mode 100644 index 00000000..c29b5ec3 --- /dev/null +++ b/content/docs/guide/performance.mdx @@ -0,0 +1,562 @@ +--- +title: "Performance Optimization" +description: "Optimize your ObjectUI applications for maximum performance" +--- + +# Performance Optimization + +Learn how to build lightning-fast ObjectUI applications with optimal bundle sizes and rendering performance. + +## Bundle Size Optimization + +### 1. Lazy Field Registration + +Register only the fields you need: + +```tsx +import { registerField } from '@object-ui/fields'; + +// ❌ Bad - Registers all 37 fields (~80KB) +import { registerAllFields } from '@object-ui/fields'; +registerAllFields(); + +// βœ… Good - Register only what you use (~20KB) +registerField('text'); +registerField('number'); +registerField('email'); +registerField('select'); +``` + +**Impact**: 60-70% reduction in field bundle size! + +### 2. Selective Component Registration + +```tsx +import { ComponentRegistry } from '@object-ui/core'; +import { Button, Card, Input } from '@object-ui/components'; + +// ❌ Bad - Registers all 47 components +import { registerAllComponents } from '@object-ui/components'; +registerAllComponents(ComponentRegistry); + +// βœ… Good - Register only what you use +ComponentRegistry.register('button', Button); +ComponentRegistry.register('card', Card); +ComponentRegistry.register('input', Input); +``` + +**Impact**: 30-50% reduction in component bundle size! + +### 3. Plugin Code Splitting + +Load plugins only when needed: + +```tsx +import { lazy, Suspense } from 'react'; + +// Lazy load heavy plugins +const KanbanBoard = lazy(() => import('@object-ui/plugin-kanban')); +const DataGrid = lazy(() => import('@object-ui/plugin-grid')); +const ChartDashboard = lazy(() => import('@object-ui/plugin-charts')); + +function App() { + return ( + Loading...}> + + + ); +} +``` + +### 4. Analyze Bundle Size + +Use bundle analyzers to identify bloat: + +```bash +# Install analyzer +npm install -D rollup-plugin-visualizer + +# Add to vite.config.ts +import { visualizer } from 'rollup-plugin-visualizer'; + +export default defineConfig({ + plugins: [ + react(), + visualizer({ + open: true, + gzipSize: true, + brotliSize: true, + }), + ], +}); + +# Build and analyze +npm run build +``` + +## Rendering Performance + +### 1. Memoize Components + +Prevent unnecessary re-renders: + +```tsx +import { memo } from 'react'; + +export const ExpensiveComponent = memo(({ schema }: ExpensiveComponentProps) => { + // Component implementation +}, (prevProps, nextProps) => { + // Custom comparison + return prevProps.schema === nextProps.schema; +}); +``` + +### 2. Use React.memo for Schema Renderer + +```tsx +import { memo } from 'react'; +import { SchemaRenderer } from '@object-ui/react'; + +const MemoizedSchemaRenderer = memo(SchemaRenderer, (prevProps, nextProps) => { + return JSON.stringify(prevProps.schema) === JSON.stringify(nextProps.schema); +}); + +function App() { + return ; +} +``` + +### 3. Virtual Scrolling for Large Lists + +Use virtualization for long lists: + +```tsx +import { FixedSizeList } from 'react-window'; + +export function VirtualList({ schema }: { schema: ListSchema }) { + const { items, height = 400, itemHeight = 50 } = schema; + + return ( + + {({ index, style }) => ( +
+ +
+ )} +
+ ); +} +``` + +### 4. Debounce Expensive Operations + +```tsx +import { useMemo, useState } from 'react'; +import { debounce } from 'lodash-es'; + +export function SearchComponent({ schema }: SearchComponentProps) { + const [query, setQuery] = useState(''); + + const debouncedSearch = useMemo( + () => + debounce((value: string) => { + // Expensive search operation + performSearch(value); + }, 300), + [] + ); + + return ( + { + setQuery(e.target.value); + debouncedSearch(e.target.value); + }} + value={query} + /> + ); +} +``` + +### 5. Optimize Re-renders with Keys + +Always use stable keys: + +```tsx +// ❌ Bad - Index as key causes unnecessary re-renders +{items.map((item, index) => ( + +))} + +// βœ… Good - Stable unique key +{items.map((item) => ( + +))} +``` + +## Data Fetching Optimization + +### 1. Implement Caching + +```tsx +import { useQuery } from '@tanstack/react-query'; + +export function DataComponent({ schema }: DataComponentProps) { + const { data, isLoading } = useQuery({ + queryKey: ['data', schema.dataSource.api], + queryFn: () => fetch(schema.dataSource.api).then(res => res.json()), + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + cacheTime: 10 * 60 * 1000, + }); + + if (isLoading) return
Loading...
; + return
{/* Render data */}
; +} +``` + +### 2. Pagination + +Implement server-side pagination: + +```tsx +const paginatedSchema = { + type: 'data-table', + dataSource: { + api: '/api/users', + method: 'GET', + params: { + page: 1, + pageSize: 20, // Load only 20 items at a time + }, + }, + pagination: { + enabled: true, + pageSize: 20, + showSizeChanger: true, + }, +}; +``` + +### 3. Incremental Loading + +Load data as needed: + +```tsx +import { useInfiniteQuery } from '@tanstack/react-query'; + +export function InfiniteList({ schema }: InfiniteListProps) { + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ + queryKey: ['items'], + queryFn: ({ pageParam = 0 }) => + fetch(`/api/items?page=${pageParam}`).then(res => res.json()), + getNextPageParam: (lastPage, pages) => lastPage.nextPage, + }); + + return ( +
+ {data?.pages.map((page) => + page.items.map((item) => ( + + )) + )} + {hasNextPage && ( + + )} +
+ ); +} +``` + +## Image Optimization + +### 1. Lazy Load Images + +```tsx +export function ImageComponent({ schema }: ImageSchema) { + return ( + {schema.alt} + ); +} +``` + +### 2. Use Modern Formats + +```tsx + + + + {alt} + +``` + +### 3. Responsive Images + +```tsx +export function ResponsiveImage({ schema }: ImageSchema) { + return ( + {schema.alt} + ); +} +``` + +## CSS Optimization + +### 1. Purge Unused Tailwind CSS + +Configure Tailwind to remove unused styles: + +```js title="tailwind.config.js" +export default { + content: [ + './src/**/*.{js,jsx,ts,tsx}', + './node_modules/@object-ui/**/*.{js,ts,jsx,tsx}', + ], + // Tailwind will purge unused classes in production +} +``` + +### 2. Use CSS Containment + +```tsx +export function CardComponent({ schema }: CardSchema) { + return ( +
+ {/* Card content */} +
+ ); +} +``` + +## Runtime Performance + +### 1. Use Production Build + +Always use production builds: + +```bash +# Development (larger, slower) +npm run dev + +# Production (optimized) +npm run build +npm run preview +``` + +### 2. Enable Compression + +Configure your server for gzip/brotli: + +```js title="vite.config.ts" +import { defineConfig } from 'vite'; +import viteCompression from 'vite-plugin-compression'; + +export default defineConfig({ + plugins: [ + viteCompression({ + algorithm: 'brotliCompress', + ext: '.br', + }), + ], +}); +``` + +### 3. Tree Shaking + +Ensure tree shaking is enabled: + +```json title="package.json" +{ + "sideEffects": false +} +``` + +## Monitoring Performance + +### 1. Use React DevTools Profiler + +```tsx +import { Profiler } from 'react'; + +function App() { + const onRenderCallback = ( + id, + phase, + actualDuration, + baseDuration, + startTime, + commitTime + ) => { + console.log(`${id} (${phase}) took ${actualDuration}ms`); + }; + + return ( + + + + ); +} +``` + +### 2. Web Vitals + +Monitor Core Web Vitals: + +```tsx +import { onCLS, onFID, onFCP, onLCP, onTTFB } from 'web-vitals'; + +onCLS(console.log); +onFID(console.log); +onFCP(console.log); +onLCP(console.log); +onTTFB(console.log); +``` + +### 3. Lighthouse Audits + +Run regular Lighthouse audits: + +```bash +# Install Lighthouse +npm install -g @lhci/cli + +# Run audit +lhci autorun --collect.url=http://localhost:3000 +``` + +## Performance Checklist + +βœ… **Bundle Size** +- [ ] Use lazy field registration +- [ ] Register only needed components +- [ ] Code split heavy plugins +- [ ] Analyze bundle with visualizer +- [ ] Keep total bundle < 500KB gzipped + +βœ… **Rendering** +- [ ] Memoize expensive components +- [ ] Use stable keys for lists +- [ ] Implement virtual scrolling for long lists +- [ ] Debounce expensive operations +- [ ] Avoid inline function creation in render + +βœ… **Data Fetching** +- [ ] Implement caching +- [ ] Use pagination for large datasets +- [ ] Implement incremental loading +- [ ] Optimize API responses + +βœ… **Images** +- [ ] Use lazy loading +- [ ] Serve modern formats (WebP, AVIF) +- [ ] Implement responsive images +- [ ] Optimize image sizes + +βœ… **CSS** +- [ ] Purge unused Tailwind classes +- [ ] Use CSS containment +- [ ] Minimize custom CSS + +βœ… **Build** +- [ ] Use production builds +- [ ] Enable compression +- [ ] Configure tree shaking + +βœ… **Monitoring** +- [ ] Set up performance monitoring +- [ ] Track Core Web Vitals +- [ ] Run regular Lighthouse audits + +## Performance Targets + +Aim for these metrics: + +| Metric | Target | Good | Needs Improvement | +|--------|--------|------|-------------------| +| **First Contentful Paint (FCP)** | < 1.8s | < 3.0s | > 3.0s | +| **Largest Contentful Paint (LCP)** | < 2.5s | < 4.0s | > 4.0s | +| **Total Blocking Time (TBT)** | < 200ms | < 600ms | > 600ms | +| **Cumulative Layout Shift (CLS)** | < 0.1 | < 0.25 | > 0.25 | +| **Speed Index** | < 3.4s | < 5.8s | > 5.8s | +| **Bundle Size (gzipped)** | < 300KB | < 500KB | > 500KB | + +## Common Performance Issues + +### Issue: Large Bundle Size + +**Diagnosis**: Run `npm run build` and check output size. + +**Solution**: +```tsx +// Before: 500KB +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; + +// After: 200KB +import { registerField } from '@object-ui/fields'; +registerField('text'); +registerField('number'); +``` + +### Issue: Slow Initial Load + +**Diagnosis**: Check Network tab in DevTools. + +**Solution**: Implement code splitting: +```tsx +const Dashboard = lazy(() => import('./pages/Dashboard')); +const Reports = lazy(() => import('./pages/Reports')); +``` + +### Issue: Janky Scrolling + +**Diagnosis**: Use DevTools Performance tab. + +**Solution**: Implement virtual scrolling: +```tsx +import { FixedSizeList } from 'react-window'; +``` + +## Next Steps + +- [Security Best Practices](/docs/guide/security) - Secure your app +- [Custom Components](/docs/guide/custom-components) - Build optimized components +- [Plugin Development](/docs/guide/plugin-development) - Create performant plugins + +## Resources + +- [Web.dev Performance](https://web.dev/performance/) +- [React Performance Optimization](https://react.dev/learn/render-and-commit) +- [Vite Performance](https://vitejs.dev/guide/performance.html) +- [Tailwind CSS Optimization](https://tailwindcss.com/docs/optimizing-for-production) diff --git a/content/docs/guide/plugin-development.mdx b/content/docs/guide/plugin-development.mdx new file mode 100644 index 00000000..247cd7d1 --- /dev/null +++ b/content/docs/guide/plugin-development.mdx @@ -0,0 +1,654 @@ +--- +title: "Plugin Development" +description: "Learn how to create and publish ObjectUI plugins" +--- + +# Plugin Development + +Create reusable plugins to extend ObjectUI with new components, fields, and functionality. This guide covers the complete plugin development workflow. + +## What is a Plugin? + +An ObjectUI plugin is a collection of: +- **Components** - UI building blocks +- **Fields** - Form input widgets +- **Utilities** - Helper functions +- **Styles** - Tailwind CSS configurations +- **Documentation** - Usage guides and examples + +Popular plugins include `plugin-kanban`, `plugin-charts`, `plugin-grid`, and more. + +## Quick Start with CLI + +The fastest way to create a plugin: + +```bash +npx @object-ui/create-plugin my-awesome-plugin +cd my-awesome-plugin +pnpm install +pnpm dev +``` + +This scaffolds a complete plugin structure with: +- TypeScript configuration +- Vite build setup +- Storybook integration +- Testing framework +- Example components + +## Plugin Structure + +``` +my-plugin/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ components/ +β”‚ β”‚ β”œβ”€β”€ MyComponent.tsx +β”‚ β”‚ β”œβ”€β”€ MyComponent.stories.tsx +β”‚ β”‚ └── my-component.schema.ts +β”‚ β”œβ”€β”€ fields/ +β”‚ β”‚ β”œβ”€β”€ MyField.tsx +β”‚ β”‚ └── my-field.schema.ts +β”‚ β”œβ”€β”€ utils/ +β”‚ β”‚ └── helpers.ts +β”‚ β”œβ”€β”€ index.ts # Main entry point +β”‚ └── register.ts # Component registration +β”œβ”€β”€ package.json +β”œβ”€β”€ tsconfig.json +β”œβ”€β”€ vite.config.ts +β”œβ”€β”€ tailwind.config.js +└── README.md +``` + +## Step-by-Step: Creating a Chart Plugin + +Let's create a plugin for data visualization charts. + +### Step 1: Initialize Plugin + +```bash +npx @object-ui/create-plugin plugin-charts +cd plugin-charts +``` + +### Step 2: Define Component Schema + +```typescript title="src/components/line-chart/line-chart.schema.ts" +import { ComponentSchema } from '@object-ui/types'; + +export interface LineChartSchema extends ComponentSchema { + type: 'line-chart'; + data: Array<{ + label: string; + value: number; + }>; + title?: string; + xAxisLabel?: string; + yAxisLabel?: string; + color?: string; + showGrid?: boolean; + showLegend?: boolean; + height?: number; + dataSource?: { + api: string; + method?: 'GET' | 'POST'; + transform?: string; // Expression to transform data + }; +} +``` + +### Step 3: Build the Component + +```tsx title="src/components/line-chart/LineChart.tsx" +import React, { useEffect, useState } from 'react'; +import { + LineChart as RechartsLineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from 'recharts'; +import { Card, CardHeader, CardTitle, CardContent } from '@object-ui/components'; +import { LineChartSchema } from './line-chart.schema'; + +export interface LineChartProps { + schema: LineChartSchema; +} + +export function LineChart({ schema }: LineChartProps) { + const { + data: initialData = [], + title, + xAxisLabel = 'X Axis', + yAxisLabel = 'Y Axis', + color = '#8884d8', + showGrid = true, + showLegend = true, + height = 300, + dataSource, + className, + } = schema; + + const [data, setData] = useState(initialData); + const [loading, setLoading] = useState(!!dataSource); + + useEffect(() => { + if (dataSource) { + fetchData(); + } + }, [dataSource]); + + const fetchData = async () => { + try { + const response = await fetch(dataSource!.api, { + method: dataSource!.method || 'GET', + }); + const result = await response.json(); + + // Transform data if transform expression provided + const transformedData = dataSource!.transform + ? transformDataWithExpression(result, dataSource!.transform) + : result; + + setData(transformedData); + } catch (error) { + console.error('Failed to fetch chart data:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Loading chart...
; + } + + const content = ( + + + {showGrid && } + + + + {showLegend && } + + + + ); + + return title ? ( + + + {title} + + {content} + + ) : ( +
{content}
+ ); +} + +function transformDataWithExpression(data: any, expression: string): any { + // Simple expression evaluation (in production, use a safe evaluator) + // Example: "items.map(item => ({ label: item.name, value: item.count }))" + try { + const fn = new Function('data', `return ${expression}`); + return fn(data); + } catch (error) { + console.error('Failed to transform data:', error); + return data; + } +} +``` + +### Step 4: Add Dependencies + +```json title="package.json" +{ + "name": "@object-ui/plugin-charts", + "version": "1.0.0", + "description": "Chart components for ObjectUI", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": ["dist", "README.md"], + "scripts": { + "dev": "vite build --watch", + "build": "vite build && tsc --emitDeclarationOnly", + "test": "vitest", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "recharts": "^2.10.0" + }, + "peerDependencies": { + "@object-ui/core": "^0.4.0", + "@object-ui/react": "^0.4.0", + "@object-ui/components": "^0.4.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@object-ui/types": "^0.4.0", + "@storybook/react": "^8.0.0", + "@storybook/react-vite": "^8.0.0", + "@types/react": "^18.0.0", + "@vitejs/plugin-react": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^5.0.0", + "vitest": "^1.0.0" + }, + "keywords": [ + "objectui", + "plugin", + "charts", + "visualization", + "react" + ] +} +``` + +### Step 5: Create Registration Function + +```typescript title="src/register.ts" +import { ComponentRegistry } from '@object-ui/core'; +import { LineChart } from './components/line-chart/LineChart'; +import { BarChart } from './components/bar-chart/BarChart'; +import { PieChart } from './components/pie-chart/PieChart'; + +export function registerChartsPlugin() { + ComponentRegistry.register('line-chart', LineChart, { + namespace: 'charts', + }); + + ComponentRegistry.register('bar-chart', BarChart, { + namespace: 'charts', + }); + + ComponentRegistry.register('pie-chart', PieChart, { + namespace: 'charts', + }); +} +``` + +### Step 6: Export Everything + +```typescript title="src/index.ts" +// Components +export { LineChart } from './components/line-chart/LineChart'; +export { BarChart } from './components/bar-chart/BarChart'; +export { PieChart } from './components/pie-chart/PieChart'; + +// Schemas +export type { LineChartSchema } from './components/line-chart/line-chart.schema'; +export type { BarChartSchema } from './components/bar-chart/bar-chart.schema'; +export type { PieChartSchema } from './components/pie-chart/pie-chart.schema'; + +// Registration +export { registerChartsPlugin } from './register'; +``` + +### Step 7: Configure Build + +```typescript title="vite.config.ts" +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + }), + ], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'ObjectUIPluginCharts', + formats: ['es', 'umd'], + fileName: (format) => `index.${format}.js`, + }, + rollupOptions: { + external: [ + 'react', + 'react-dom', + '@object-ui/core', + '@object-ui/react', + '@object-ui/components', + ], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + '@object-ui/core': 'ObjectUICore', + '@object-ui/react': 'ObjectUIReact', + '@object-ui/components': 'ObjectUIComponents', + }, + }, + }, + }, +}); +``` + +## Testing Your Plugin + +### Unit Tests + +```tsx title="src/components/line-chart/LineChart.test.tsx" +import { render, screen } from '@testing-library/react'; +import { LineChart } from './LineChart'; +import { LineChartSchema } from './line-chart.schema'; + +describe('LineChart', () => { + it('renders with data', () => { + const schema: LineChartSchema = { + type: 'line-chart', + title: 'Sales Data', + data: [ + { label: 'Jan', value: 100 }, + { label: 'Feb', value: 150 }, + { label: 'Mar', value: 200 }, + ], + }; + + render(); + expect(screen.getByText('Sales Data')).toBeInTheDocument(); + }); + + it('fetches data from API', async () => { + const schema: LineChartSchema = { + type: 'line-chart', + dataSource: { + api: '/api/chart-data', + method: 'GET', + }, + }; + + // Mock fetch + global.fetch = vi.fn(() => + Promise.resolve({ + json: () => Promise.resolve([ + { label: 'A', value: 10 }, + { label: 'B', value: 20 }, + ]), + }) + ) as any; + + render(); + + await screen.findByText('A'); + expect(screen.getByText('A')).toBeInTheDocument(); + }); +}); +``` + +### Storybook Stories + +```tsx title="src/components/line-chart/LineChart.stories.tsx" +import type { Meta, StoryObj } from '@storybook/react'; +import { LineChart } from './LineChart'; + +const meta: Meta = { + title: 'Charts/LineChart', + component: LineChart, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + schema: { + type: 'line-chart', + title: 'Monthly Revenue', + data: [ + { label: 'Jan', value: 4000 }, + { label: 'Feb', value: 3000 }, + { label: 'Mar', value: 2000 }, + { label: 'Apr', value: 2780 }, + { label: 'May', value: 1890 }, + { label: 'Jun', value: 2390 }, + ], + xAxisLabel: 'Month', + yAxisLabel: 'Revenue ($)', + color: '#8884d8', + }, + }, +}; + +export const WithoutGrid: Story = { + args: { + schema: { + ...Default.args.schema, + showGrid: false, + }, + }, +}; + +export const CustomColor: Story = { + args: { + schema: { + ...Default.args.schema, + color: '#ff6b6b', + }, + }, +}; +``` + +## Documentation + +Create comprehensive README: + +```markdown title="README.md" +# @object-ui/plugin-charts + +Chart components for ObjectUI - Line, Bar, Pie, and more. + +## Installation + +\`\`\`bash +npm install @object-ui/plugin-charts recharts +\`\`\` + +## Usage + +\`\`\`tsx +import { registerChartsPlugin } from '@object-ui/plugin-charts'; +import { SchemaRenderer } from '@object-ui/react'; + +// Register the plugin +registerChartsPlugin(); + +const schema = { + type: 'line-chart', + title: 'Sales Trend', + data: [ + { label: 'Jan', value: 100 }, + { label: 'Feb', value: 150 }, + { label: 'Mar', value: 200 }, + ], +}; + +function App() { + return ; +} +\`\`\` + +## Components + +### LineChart + +Display data as a line chart. + +**Schema Properties:** +- `data` - Array of {label, value} objects +- `title` - Chart title +- `xAxisLabel` - X-axis label +- `yAxisLabel` - Y-axis label +- `color` - Line color (hex) +- `showGrid` - Show grid lines (default: true) +- `height` - Chart height in pixels (default: 300) + +[More documentation...] + +## License + +MIT +``` + +## Publishing to NPM + +### Step 1: Build + +```bash +pnpm build +``` + +### Step 2: Test Locally + +```bash +# In your plugin directory +npm link + +# In your test project +npm link @object-ui/plugin-charts +``` + +### Step 3: Publish + +```bash +# Login to npm +npm login + +# Publish (with public access for scoped packages) +npm publish --access public +``` + +### Step 4: Version Management + +Use semantic versioning: + +```bash +# Patch release (bug fixes) +npm version patch + +# Minor release (new features) +npm version minor + +# Major release (breaking changes) +npm version major +``` + +## Plugin Best Practices + +### 1. Tree-Shaking Support + +Export individual components: + +```typescript +// βœ… Good - Allows tree-shaking +export { LineChart } from './components/line-chart/LineChart'; +export { BarChart } from './components/bar-chart/BarChart'; + +// ❌ Bad - Bundles everything +export * from './components'; +``` + +### 2. Peer Dependencies + +Don't bundle React or ObjectUI core: + +```json +{ + "peerDependencies": { + "react": "^18.0.0", + "@object-ui/core": "^0.4.0" + } +} +``` + +### 3. Size Optimization + +- Use dynamic imports for large dependencies +- Minimize bundle size +- Provide both ESM and UMD builds + +```typescript +// Lazy load heavy dependencies +const HeavyChart = lazy(() => import('./components/HeavyChart')); +``` + +### 4. TypeScript Support + +Always export types: + +```typescript +export type { LineChartSchema } from './components/line-chart/line-chart.schema'; +``` + +### 5. Documentation + +- Include README with examples +- Add Storybook stories +- Document all schema properties +- Provide migration guides + +## Plugin Examples + +Study these official plugins: + +- **[@object-ui/plugin-kanban](https://github.com/objectstack-ai/objectui/tree/main/packages/plugin-kanban)** - Kanban boards with drag-and-drop +- **[@object-ui/plugin-grid](https://github.com/objectstack-ai/objectui/tree/main/packages/plugin-grid)** - Advanced data grids +- **[@object-ui/plugin-charts](https://github.com/objectstack-ai/objectui/tree/main/packages/plugin-charts)** - Data visualization +- **[@object-ui/plugin-form](https://github.com/objectstack-ai/objectui/tree/main/packages/plugin-form)** - Advanced forms + +## Troubleshooting + +### Peer dependency warnings + +Use `--legacy-peer-deps` when installing: + +```bash +npm install --legacy-peer-deps +``` + +### Build errors + +Clear cache and rebuild: + +```bash +rm -rf dist node_modules +npm install +npm run build +``` + +### TypeScript errors in consumers + +Ensure `types` field points to correct declaration file: + +```json +{ + "types": "dist/index.d.ts" +} +``` + +## Next Steps + +- [Custom Component Development](/docs/guide/custom-components) - Build components +- [Performance Optimization](/docs/guide/performance) - Optimize plugins +- [API Reference](/docs/api) - Component API +- [Publishing Guide](https://docs.npmjs.com/packages-and-modules) - NPM documentation + +## Community Plugins + +Share your plugin: +- [Submit to Plugin Marketplace](https://github.com/objectstack-ai/objectui/discussions) +- [Plugin Directory](/plugins) +- [Awesome ObjectUI](https://github.com/objectstack-ai/awesome-objectui) diff --git a/content/docs/guide/quick-start.mdx b/content/docs/guide/quick-start.mdx new file mode 100644 index 00000000..e0ecdede --- /dev/null +++ b/content/docs/guide/quick-start.mdx @@ -0,0 +1,205 @@ +--- +title: "Quick Start" +description: "Get up and running with ObjectUI in 5 minutes" +--- + +# Quick Start + +Get your first ObjectUI application running in just 5 minutes! This guide will walk you through the fastest path to see ObjectUI in action. + +## Prerequisites + +Before you begin, ensure you have: + +- **Node.js 18+** installed +- **pnpm** package manager (recommended) or npm +- Basic knowledge of React and TypeScript + +## Option 1: Try the Live Demo (0 minutes) + +The fastest way to see ObjectUI in action: + +```bash +# Clone the repository +git clone https://github.com/objectstack-ai/objectui.git +cd objectui + +# Install dependencies +pnpm install + +# Start the development console +pnpm dev +``` + +Open [http://localhost:5173](http://localhost:5173) to see the ObjectUI console with live examples! + +## Option 2: Add to Existing React App (5 minutes) + +### Step 1: Install Core Packages + +```bash +npm install @object-ui/react @object-ui/components @object-ui/fields +``` + +### Step 2: Setup Tailwind CSS + +ObjectUI requires Tailwind CSS. Add it to your project: + +```bash +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +Update your `tailwind.config.js`: + +```js title="tailwind.config.js" +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + // Include ObjectUI components + "./node_modules/@object-ui/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +Add Tailwind directives to your CSS: + +```css title="src/index.css" +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +### Step 3: Render Your First Schema + +Create a simple form schema: + +```tsx title="src/App.tsx" +import { SchemaRenderer } from '@object-ui/react'; +import { ComponentRegistry } from '@object-ui/core'; +import { registerAllComponents } from '@object-ui/components'; +import { registerAllFields } from '@object-ui/fields'; + +// Register components +registerAllComponents(ComponentRegistry); +registerAllFields(); + +const schema = { + type: 'card', + title: 'Contact Form', + children: [ + { + type: 'field', + fieldType: 'text', + name: 'name', + label: 'Full Name', + placeholder: 'John Doe', + required: true, + }, + { + type: 'field', + fieldType: 'email', + name: 'email', + label: 'Email', + placeholder: 'john@example.com', + required: true, + }, + { + type: 'field', + fieldType: 'textarea', + name: 'message', + label: 'Message', + placeholder: 'Your message here...', + rows: 4, + }, + { + type: 'button', + text: 'Submit', + variant: 'default', + onClick: { + type: 'log', + message: 'Form submitted!', + }, + }, + ], +}; + +function App() { + return ( +
+
+ +
+
+ ); +} + +export default App; +``` + +### Step 4: Run Your App + +```bash +npm run dev +``` + +πŸŽ‰ **Congratulations!** You've just rendered your first schema-driven UI component! + +## What Just Happened? + +1. **Schema Definition**: You defined a form using plain JSON/JavaScript objects +2. **Component Registration**: You registered ObjectUI's component library +3. **Schema Rendering**: The `` transformed your schema into React components + +## Next Steps + +Now that you have ObjectUI running, explore these guides: + +- [**Installation Guide**](/docs/guide/installation) - Complete setup with all features +- [**Zero to Deployment**](/docs/guide/zero-to-deployment) - Build and deploy a complete app +- [**Schema Overview**](/docs/guide/schema-overview) - Understand the schema structure +- [**Component Gallery**](/docs/components) - Browse all 35+ available components + +## Common Issues + +### Tailwind styles not loading? + +Make sure you've added ObjectUI to your `content` array in `tailwind.config.js`: + +```js +content: [ + "./node_modules/@object-ui/**/*.{js,ts,jsx,tsx}", +] +``` + +### Components not rendering? + +Ensure you've registered the components before rendering: + +```tsx +import { registerAllComponents } from '@object-ui/components'; +import { ComponentRegistry } from '@object-ui/core'; + +registerAllComponents(ComponentRegistry); +``` + +### TypeScript errors? + +Install type definitions: + +```bash +npm install -D @types/react @types/react-dom +``` + +## Resources + +- [GitHub Repository](https://github.com/objectstack-ai/objectui) +- [Component Documentation](/docs/components) +- [API Reference](/docs/core) +- [Examples](https://github.com/objectstack-ai/objectui/tree/main/examples) diff --git a/content/docs/guide/security.mdx b/content/docs/guide/security.mdx new file mode 100644 index 00000000..f4f9982a --- /dev/null +++ b/content/docs/guide/security.mdx @@ -0,0 +1,726 @@ +--- +title: "Security Best Practices" +description: "Build secure ObjectUI applications with industry best practices" +--- + +# Security Best Practices + +Protect your ObjectUI applications from common security vulnerabilities. This guide covers input validation, XSS prevention, authentication, and more. + +## Input Validation and Sanitization + +### 1. Validate User Input + +Always validate data before processing: + +```tsx +import { z } from 'zod'; + +// Define validation schema +const userSchema = z.object({ + name: z.string().min(1).max(100), + email: z.string().email(), + age: z.number().min(18).max(120), + role: z.enum(['user', 'admin', 'moderator']), +}); + +export function UserForm({ schema }: FormSchema) { + const handleSubmit = (data: unknown) => { + try { + // Validate input + const validated = userSchema.parse(data); + + // Safe to use validated data + processUser(validated); + } catch (error) { + if (error instanceof z.ZodError) { + console.error('Validation failed:', error.errors); + showErrors(error.errors); + } + } + }; + + return
{/* Form fields */}
; +} +``` + +### 2. Sanitize HTML Content + +Prevent XSS attacks by sanitizing HTML: + +```tsx +import DOMPurify from 'dompurify'; + +export function RichTextDisplay({ schema }: RichTextSchema) { + const { content } = schema; + + // ❌ Dangerous - Direct HTML injection + // return
; + + // βœ… Safe - Sanitized HTML + const sanitized = DOMPurify.sanitize(content, { + ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a'], + ALLOWED_ATTR: ['href', 'title'], + }); + + return
; +} +``` + +### 3. Validate Schema Structure + +```tsx +import { ComponentSchema } from '@object-ui/types'; + +export function validateSchema(schema: unknown): schema is ComponentSchema { + if (!schema || typeof schema !== 'object') { + throw new Error('Schema must be an object'); + } + + const s = schema as Record; + + if (!s.type || typeof s.type !== 'string') { + throw new Error('Schema must have a valid type'); + } + + // Prevent script injection + if (s.type.includes('