diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml
new file mode 100644
index 0000000..7b0bd06
--- /dev/null
+++ b/.github/workflows/pr-review.yml
@@ -0,0 +1,96 @@
+name: PR Review
+
+on:
+ pull_request:
+ types: [opened, synchronize, ready_for_review, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ review:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout PR branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Generate Marty token
+ id: marty-token
+ uses: actions/create-github-app-token@v2
+ with:
+ app-id: ${{ secrets.MARTY_APP_ID }}
+ private-key: ${{ secrets.MARTY_APP_PRIVATE_KEY }}
+
+ - name: Run Marty PR Review
+ uses: nesalia-inc/marty-action@1.0.0
+ with:
+ github_token: ${{ steps.marty-token.outputs.token }}
+ prompt: |
+ REPO: ${{ github.repository }}
+ PR NUMBER: ${{ github.event.pull_request.number }}
+ AUTHOR: ${{ github.event.pull_request.user.login }}
+ TITLE: ${{ github.event.pull_request.title }}
+
+ Please review this pull request comprehensively:
+
+ ## Review Focus Areas
+
+ ### Code Quality
+ - Code follows best practices and design patterns
+ - Proper error handling and edge cases
+ - No code duplication (DRY principle)
+ - Clear and maintainable code structure
+
+ ### Security
+ - No hardcoded secrets or credentials
+ - Proper input validation and sanitization
+ - SQL injection and XSS vulnerabilities
+ - Authentication and authorization checks
+ - Sensitive data handling
+
+ ### Performance
+ - Potential performance bottlenecks
+ - Efficient database queries
+ - Proper caching strategies
+ - Resource cleanup and memory leaks
+
+ ### Testing
+ - Adequate test coverage for changes
+ - Edge cases covered
+ - Test quality and assertions
+
+ ### Documentation
+ - README updated if needed
+ - Inline comments for complex logic
+ - API documentation updated
+ - Breaking changes documented
+
+ ## Review Output Format
+
+ Use inline comments for specific code issues (highlight exact lines).
+ Use top-level PR comments for general observations and summary.
+
+ Structure your top-level comment as:
+ - **Summary**: Brief overview
+ - **Critical Issues**: Must-fix items
+ - **Recommendations**: Suggestions for improvement
+ - **Positive Notes**: Good practices observed
+
+ Note: The PR branch is already checked out in the current working directory.
+
+ Only post GitHub comments - don't submit review text as messages.
+
+ claude_args: |
+ --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
+ --max-turns 100
+
+ env:
+ ANTHROPIC_BASE_URL: https://api.minimax.io/anthropic
+ ANTHROPIC_AUTH_TOKEN: ${{ secrets.MINIMAX_API_KEY }}
+ ANTHROPIC_DEFAULT_SONNET_MODEL: MiniMax-M2.5
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: MiniMax-M2.5
+ ANTHROPIC_DEFAULT_OPUS_MODEL: MiniMax-M2.5
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cbad707
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Dependencies
+node_modules/
+.pnpm-store/
+
+# Build outputs
+dist/
+out/
+build/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# Testing
+coverage/
+
+# Turbo
+.turbo/
+
+# Misc
+*.tsbuildinfo
diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts
index ac341ad..384aee0 100644
--- a/apps/desktop/electron.vite.config.ts
+++ b/apps/desktop/electron.vite.config.ts
@@ -1,5 +1,7 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
+import tailwindcss from '@tailwindcss/vite'
+import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
main: {
@@ -23,13 +25,21 @@ export default defineConfig({
}
},
renderer: {
- root: resolve(__dirname, '../web/dist'),
+ root: resolve(__dirname, '../web'),
+ plugins: [
+ tailwindcss(),
+ tsconfigPaths({ projects: ['./tsconfig.json'] })
+ ],
build: {
rollupOptions: {
input: {
- index: resolve(__dirname, '../web/dist/index.html')
+ index: resolve(__dirname, '../web/index.html')
}
}
+ },
+ server: {
+ host: '127.0.0.1',
+ port: 3000
}
}
})
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 7ed2b1b..ff8559f 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -23,11 +23,14 @@
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
+ "@tailwindcss/vite": "^4.1.18",
"@types/node": "^22.10.2",
"electron": "^33.3.1",
"electron-builder": "^25.1.8",
- "electron-vite": "^2.3.0",
+ "electron-vite": "^5.0.0",
+ "tailwindcss": "^4.1.18",
"typescript": "^5.7.2",
- "vite": "^7.1.7"
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
}
}
diff --git a/apps/web/index.html b/apps/web/index.html
index 0788391..613bd99 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -1,10 +1,15 @@
-
- web
+
+ Wareflow
+
diff --git a/apps/web/package.json b/apps/web/package.json
index 1bacf56..23d7033 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -19,6 +19,7 @@
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
+ "@wareflow/db": "workspace:*",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -27,6 +28,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.545.0",
"next-themes": "^0.4.6",
+ "papaparse": "^5.5.3",
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-day-picker": "^9.13.2",
@@ -46,6 +48,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
+ "@types/papaparse": "^5.5.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
diff --git a/apps/web/src/components/setup/column-mapping.tsx b/apps/web/src/components/setup/column-mapping.tsx
new file mode 100644
index 0000000..5966f76
--- /dev/null
+++ b/apps/web/src/components/setup/column-mapping.tsx
@@ -0,0 +1,165 @@
+import { useMemo } from 'react'
+import { ArrowLeft, ArrowRight, RefreshCw, Columns2 } from 'lucide-react'
+import { Button } from '../ui/button'
+import { IMPORT_FIELDS, type ColumnMapping, type ImportField, type ParsedData } from '../../types/setup'
+
+interface ColumnMappingProps {
+ data: ParsedData[]
+ headers: string[]
+ columnMapping: ColumnMapping
+ onBack: () => void
+ onNext: () => void
+ onMappingChange: (mapping: ColumnMapping) => void
+}
+
+export function ColumnMappingComponent({
+ data,
+ headers,
+ columnMapping,
+ onBack,
+ onNext,
+ onMappingChange,
+}: ColumnMappingProps) {
+ const sampleData = useMemo(() => data.slice(0, 3), [data])
+
+ const autoDetectMapping = useMemo(() => {
+ const mapping: ColumnMapping = {}
+
+ IMPORT_FIELDS.forEach(field => {
+ // Try exact match first
+ const exactMatch = headers.find(
+ h => h.toLowerCase() === field.label.toLowerCase() ||
+ h.toLowerCase() === field.key.toLowerCase()
+ )
+
+ if (exactMatch) {
+ mapping[field.key] = exactMatch
+ return
+ }
+
+ // Try partial match
+ const partialMatch = headers.find(
+ h => h.toLowerCase().includes(field.key.toLowerCase()) ||
+ field.label.toLowerCase().includes(h.toLowerCase())
+ )
+
+ if (partialMatch) {
+ mapping[field.key] = partialMatch
+ }
+ })
+
+ return mapping
+ }, [headers])
+
+ const handleReset = () => {
+ onMappingChange(autoDetectMapping)
+ }
+
+ const handleHeaderChange = (fieldKey: ImportField['key'], header: string) => {
+ onMappingChange({ ...columnMapping, [fieldKey]: header })
+ }
+
+ const mappedCount = Object.keys(columnMapping).filter(k => columnMapping[k as ImportField['key']]).length
+ const requiredMapped = IMPORT_FIELDS
+ .filter(f => f.required)
+ .every(f => columnMapping[f.key])
+
+ return (
+
+
+
+ Map Your Columns
+
+
+ Match your file columns to the system fields
+
+
+
+
+
+
+
+ {mappedCount} / {IMPORT_FIELDS.length} fields mapped
+
+
+
+
+ Auto-detect
+
+
+
+
+
+
+ File Columns
+
+
+ System Fields
+
+
+
+
+ {IMPORT_FIELDS.map((field) => (
+
+
+
+ {field.label}
+ {field.required && * }
+
+ {field.defaultValue && (
+
+ Default: {field.defaultValue}
+
+ )}
+
+
handleHeaderChange(field.key, e.target.value)}
+ className="px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-sm text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
+ >
+ -- Select column --
+ {headers.map((header) => (
+
+ {header}
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* Sample data preview */}
+
+
+ Sample Data Preview
+
+
+ {IMPORT_FIELDS.slice(0, 3).map((field) => {
+ const header = columnMapping[field.key]
+ const sample = header ? sampleData[0]?.[header] : null
+ return (
+
+
{field.label}
+
+ {sample || '-'}
+
+
+ )
+ })}
+
+
+
+
+
+
+ Back
+
+
+ Continue
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/setup/complete.tsx b/apps/web/src/components/setup/complete.tsx
new file mode 100644
index 0000000..e23a20e
--- /dev/null
+++ b/apps/web/src/components/setup/complete.tsx
@@ -0,0 +1,54 @@
+import { CheckCircle, Package, Upload, ArrowRight } from 'lucide-react'
+import { Button } from '../ui/button'
+
+interface CompleteProps {
+ importedCount: number
+ onViewProducts: () => void
+ onImportAnother: () => void
+}
+
+export function Complete({ importedCount, onViewProducts, onImportAnother }: CompleteProps) {
+ return (
+
+
+
+
+
+
+
+ Import Complete!
+
+
+ Your products have been imported successfully
+
+
+
+
+
+
+
+
+ {importedCount}
+
+
+ Products Imported
+
+
+
+
+
+
+
+
+ View Products
+
+
+
+
+
+ Import Another File
+
+
+
+ )
+}
diff --git a/apps/web/src/components/setup/file-selection.tsx b/apps/web/src/components/setup/file-selection.tsx
new file mode 100644
index 0000000..9344e47
--- /dev/null
+++ b/apps/web/src/components/setup/file-selection.tsx
@@ -0,0 +1,181 @@
+import { useCallback, useState } from 'react'
+import { Upload, FileSpreadsheet, AlertCircle, FileText } from 'lucide-react'
+import Papa from 'papaparse'
+
+const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
+const ACCEPTED_TYPES = ['.csv']
+
+interface FileSelectionProps {
+ onFileSelect: (file: File, data: string[][], headers: string[]) => void
+}
+
+export function FileSelection({ onFileSelect }: FileSelectionProps) {
+ const [isDragging, setIsDragging] = useState(false)
+ const [error, setError] = useState(null)
+
+ const parseFile = useCallback(async (file: File) => {
+ setError(null)
+
+ if (file.size > MAX_FILE_SIZE) {
+ setError(`File is too large. Maximum size is 10 MB.`)
+ return
+ }
+
+ const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
+ if (!ACCEPTED_TYPES.includes(ext)) {
+ setError(`Invalid file type. Accepted: .csv`)
+ return
+ }
+
+ Papa.parse(file, {
+ header: true,
+ skipEmptyLines: true,
+ complete: (results) => {
+ if (results.errors.length > 0) {
+ setError(`Parsing error: ${results.errors[0].message}`)
+ return
+ }
+
+ const headers = results.meta.fields || []
+ if (headers.length === 0) {
+ setError('File has no headers')
+ return
+ }
+
+ const data = results.data as Record[]
+ if (data.length === 0) {
+ setError('File is empty')
+ return
+ }
+
+ // Convert to array of arrays for compatibility
+ const dataArray = data.map(row => headers.map(h => row[h] || ''))
+ onFileSelect(file, dataArray, headers)
+ },
+ error: (err) => {
+ setError(`Failed to parse file: ${err.message}`)
+ }
+ })
+ }, [onFileSelect])
+
+ const handleDrop = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(false)
+
+ const file = e.dataTransfer.files[0]
+ if (file) {
+ parseFile(file)
+ }
+ }, [parseFile])
+
+ const handleDragOver = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(true)
+ }, [])
+
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(false)
+ }, [])
+
+ const handleFileInput = useCallback((e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ if (file) {
+ parseFile(file)
+ }
+ }, [parseFile])
+
+ return (
+
+
+
+
+
+
+ Import Your Products
+
+
+ Upload a CSV file with your product inventory
+
+
+
+
{
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ document.getElementById('file-input')?.click()
+ }
+ }}
+ className={`
+ relative border-2 border-dashed rounded-2xl p-16 text-center cursor-pointer
+ transition-all duration-300 outline-none
+ ${isDragging
+ ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 scale-[1.02]'
+ : 'border-slate-300 dark:border-slate-600 hover:border-slate-400 dark:hover:border-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800/50 focus-visible:ring-4 focus-visible:ring-blue-500/20'
+ }
+ `}
+ >
+
+ Select a CSV file to import
+
+
+
+
+
+ {isDragging ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {isDragging ? 'Drop your file here' : 'Drag & drop your CSV file'}
+
+
+ or click to browse
+
+
+
+
+ .csv
+ Max 10 MB
+
+
+
+
+ {error && (
+
+ )}
+
+ {/* Help text */}
+
+
+ Your CSV should include columns like: SKU , Name , Quantity
+
+
+
+ )
+}
diff --git a/apps/web/src/components/setup/index.ts b/apps/web/src/components/setup/index.ts
new file mode 100644
index 0000000..4a52ebb
--- /dev/null
+++ b/apps/web/src/components/setup/index.ts
@@ -0,0 +1,5 @@
+export { FileSelection } from './file-selection'
+export { Preview } from './preview'
+export { ColumnMappingComponent } from './column-mapping'
+export { Validation } from './validation'
+export { Complete } from './complete'
diff --git a/apps/web/src/components/setup/preview.tsx b/apps/web/src/components/setup/preview.tsx
new file mode 100644
index 0000000..3dc02e9
--- /dev/null
+++ b/apps/web/src/components/setup/preview.tsx
@@ -0,0 +1,94 @@
+import { FileSpreadsheet, ArrowLeft, ArrowRight, Rows3 } from 'lucide-react'
+import { Button } from '../ui/button'
+import type { ParsedData } from '../../types/setup'
+
+interface PreviewProps {
+ data: ParsedData[]
+ headers: string[]
+ rowCount: number
+ fileName: string
+ onBack: () => void
+ onNext: () => void
+}
+
+export function Preview({ data, headers, rowCount, fileName, onBack, onNext }: PreviewProps) {
+ const previewData = data.slice(0, 10)
+
+ return (
+
+
+
+ Preview Your Data
+
+
+ Showing first 10 rows from {rowCount} total rows
+
+
+
+
+
+
+
+ {fileName}
+
+
+
+ {rowCount} rows
+
+
+
+
+
+
+
+
+ #
+
+ {headers.map((header) => (
+
+ {header}
+
+ ))}
+
+
+
+ {previewData.map((row, idx) => (
+
+ {idx + 1}
+ {headers.map((header) => (
+
+ {row[header] || '-'}
+
+ ))}
+
+ ))}
+
+
+
+
+ {rowCount > 10 && (
+
+ ... and {rowCount - 10} more rows
+
+ )}
+
+
+
+
+
+ Back
+
+
+ Continue
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/setup/validation.tsx b/apps/web/src/components/setup/validation.tsx
new file mode 100644
index 0000000..5bbb320
--- /dev/null
+++ b/apps/web/src/components/setup/validation.tsx
@@ -0,0 +1,222 @@
+import { useMemo } from 'react'
+import { AlertTriangle, CheckCircle, XCircle, ArrowLeft, ArrowRight, AlertCircle } from 'lucide-react'
+import { Button } from '../ui/button'
+import { IMPORT_FIELDS, type ColumnMapping, type ParsedData, type ValidationResult, type ValidationError, type ValidationWarning } from '../../types/setup'
+
+interface ValidationProps {
+ data: ParsedData[]
+ columnMapping: ColumnMapping
+ onBack: () => void
+ onImport: () => void
+ isImporting?: boolean
+ error?: string | null
+}
+
+export function Validation({ data, columnMapping, onBack, onImport, isImporting = false, error }: ValidationProps) {
+ const validation = useMemo((): ValidationResult => {
+ const errors: ValidationError[] = []
+ const warnings: ValidationWarning[] = []
+
+ // Check required field mappings first
+ const requiredFields = IMPORT_FIELDS.filter(f => f.required)
+ for (const field of requiredFields) {
+ if (!columnMapping[field.key]) {
+ errors.push({ row: 0, field: field.key, message: `Required field "${field.label}" is not mapped` })
+ }
+ }
+
+ // If required fields are missing, don't validate data
+ if (errors.length > 0) {
+ return { isValid: false, errors, warnings }
+ }
+
+ const skuField = columnMapping.sku
+ const nameField = columnMapping.name
+ const quantityField = columnMapping.quantity
+
+ // Track seen SKUs for duplicate detection
+ const seenSkus = new Set()
+
+ data.forEach((row, idx) => {
+ const rowNum = idx + 1
+
+ // Required fields validation
+ if (skuField) {
+ const sku = row[skuField]
+ if (!sku || sku.trim() === '') {
+ errors.push({ row: rowNum, field: 'sku', message: 'SKU is required' })
+ } else if (seenSkus.has(sku)) {
+ warnings.push({ row: rowNum, field: 'sku', message: `Duplicate SKU: ${sku}` })
+ } else {
+ seenSkus.add(sku)
+ }
+ }
+
+ if (nameField) {
+ const name = row[nameField]
+ if (!name || name.trim() === '') {
+ errors.push({ row: rowNum, field: 'name', message: 'Name is required' })
+ }
+ }
+
+ if (quantityField) {
+ const quantity = row[quantityField]
+ const numQuantity = parseFloat(quantity)
+
+ if (!quantity || quantity.trim() === '') {
+ // Quantity is optional in IMPORT_FIELDS but let's warn if missing
+ } else if (isNaN(numQuantity)) {
+ errors.push({ row: rowNum, field: 'quantity', message: 'Invalid quantity: must be a number' })
+ } else if (numQuantity < 0) {
+ warnings.push({ row: rowNum, field: 'quantity', message: `Negative quantity: ${numQuantity}` })
+ }
+ }
+ })
+
+ return {
+ isValid: errors.length === 0,
+ errors,
+ warnings,
+ }
+ }, [data, columnMapping])
+
+ const errorCount = validation.errors.length
+ const warningCount = validation.warnings.length
+
+ return (
+
+
+
+ Validate Your Data
+
+
+ Review errors and warnings before importing
+
+
+
+ {/* Summary cards */}
+
+
+
+ {validation.isValid ? (
+
+ ) : (
+
+ )}
+
+
+ {validation.isValid ? 'Valid' : 'Invalid'}
+
+
+ {data.length} rows
+
+
+
+
+
+
+
+
+
+
+ {errorCount}
+
+
+ Errors
+
+
+
+
+
+
+
+
+
+
+ {warningCount}
+
+
+ Warnings
+
+
+
+
+
+
+ {/* Errors section */}
+ {errorCount > 0 && (
+
+
+
+ Errors ({errorCount})
+
+
+ {validation.errors.map((error, idx) => (
+
+
+
+
+ Row {error.row}
+
+ - {error.message}
+
+
+ ))}
+
+
+ )}
+
+ {/* Warnings section */}
+ {warningCount > 0 && (
+
+
+
+ Warnings ({warningCount})
+
+
+ {validation.warnings.map((warning, idx) => (
+
+
+
+
+ Row {warning.row}
+
+ - {warning.message}
+
+
+ ))}
+
+
+ )}
+
+ {/* No issues */}
+ {errorCount === 0 && warningCount === 0 && !error && (
+
+
+
+ All data looks good! No issues found.
+
+
+ )}
+
+ {/* Import error */}
+ {error && (
+
+ )}
+
+
+
+
+ Back
+
+
0 || isImporting}>
+ {isImporting ? 'Importing...' : `Import ${data.length} Products`}
+
+
+
+
+ )
+}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index f9425ee..647fc9b 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client'
import { RouterProvider, createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
+import './styles.css'
+
const router = createRouter({
routeTree,
defaultPreload: 'intent',
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index 3178b44..8078496 100644
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -1,118 +1,9 @@
-import { createFileRoute } from '@tanstack/react-router'
-import {
- Zap,
- Server,
- Route as RouteIcon,
- Shield,
- Waves,
- Sparkles,
-} from 'lucide-react'
-
-export const Route = createFileRoute('/')({ component: App })
-
-function App() {
- const features = [
- {
- icon: ,
- title: 'Powerful Server Functions',
- description:
- 'Write server-side code that seamlessly integrates with your client components. Type-safe, secure, and simple.',
- },
- {
- icon: ,
- title: 'Flexible Server Side Rendering',
- description:
- 'Full-document SSR, streaming, and progressive enhancement out of the box. Control exactly what renders where.',
- },
- {
- icon: ,
- title: 'API Routes',
- description:
- 'Build type-safe API endpoints alongside your application. No separate backend needed.',
- },
- {
- icon: ,
- title: 'Strongly Typed Everything',
- description:
- 'End-to-end type safety from server to client. Catch errors before they reach production.',
- },
- {
- icon: ,
- title: 'Full Streaming Support',
- description:
- 'Stream data from server to client progressively. Perfect for AI applications and real-time updates.',
- },
- {
- icon: ,
- title: 'Next Generation Ready',
- description:
- 'Built from the ground up for modern web applications. Deploy anywhere JavaScript runs.',
- },
- ]
-
- return (
-
-
-
-
-
-
-
- TANSTACK {' '}
-
- START
-
-
-
-
- The framework for next generation AI applications
-
-
- Full-stack framework powered by TanStack Router for React and Solid.
- Build modern applications with server functions, streaming, and type
- safety.
-
-
-
- Documentation
-
-
- Begin your TanStack Start journey by editing{' '}
-
- /src/routes/index.tsx
-
-
-
-
-
-
-
-
- {features.map((feature, index) => (
-
-
{feature.icon}
-
- {feature.title}
-
-
- {feature.description}
-
-
- ))}
-
-
-
- )
-}
+import { createFileRoute, redirect } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/')({
+ beforeLoad: () => {
+ throw redirect({
+ to: '/setup',
+ })
+ },
+})
diff --git a/apps/web/src/routes/setup.tsx b/apps/web/src/routes/setup.tsx
new file mode 100644
index 0000000..636246a
--- /dev/null
+++ b/apps/web/src/routes/setup.tsx
@@ -0,0 +1,196 @@
+import { useState, useCallback } from 'react'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+import { FileSelection, Preview, ColumnMappingComponent, Validation, Complete } from '../components/setup'
+import { type SetupStep, type ColumnMapping, type ParsedData } from '../types/setup'
+import { importProducts } from '@wareflow/db'
+
+export const Route = createFileRoute('/setup')({
+ component: SetupPage,
+})
+
+const STEPS = ['file-selection', 'preview', 'column-mapping', 'validation'] as const
+const STEP_LABELS = ['File', 'Preview', 'Mapping', 'Validate']
+
+function SetupPage() {
+ const navigate = useNavigate()
+
+ const [step, setStep] = useState('file-selection')
+ const [file, setFile] = useState(null)
+ const [parsedData, setParsedData] = useState([])
+ const [headers, setHeaders] = useState([])
+ const [rowCount, setRowCount] = useState(0)
+ const [columnMapping, setColumnMapping] = useState({})
+ const [importedCount, setImportedCount] = useState(0)
+ const [isImporting, setIsImporting] = useState(false)
+ const [importError, setImportError] = useState(null)
+
+ const handleFileSelect = useCallback((selectedFile: File, data: ParsedData[], fileHeaders: string[]) => {
+ setFile(selectedFile)
+ setParsedData(data)
+ setHeaders(fileHeaders)
+ setRowCount(data.length)
+ setStep('preview')
+ }, [])
+
+ const handleBack = useCallback(() => {
+ switch (step) {
+ case 'preview':
+ setStep('file-selection')
+ setFile(null)
+ setParsedData([])
+ setHeaders([])
+ setRowCount(0)
+ break
+ case 'column-mapping':
+ setStep('preview')
+ break
+ case 'validation':
+ setStep('column-mapping')
+ break
+ }
+ }, [step])
+
+ const handleNext = useCallback(() => {
+ switch (step) {
+ case 'preview':
+ setStep('column-mapping')
+ break
+ case 'column-mapping':
+ setStep('validation')
+ break
+ }
+ }, [step])
+
+ const handleImport = useCallback(async () => {
+ setIsImporting(true)
+ setImportError(null)
+ try {
+ const result = await importProducts(parsedData, columnMapping)
+ setImportedCount(result.imported)
+ setStep('complete')
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Import failed'
+ setImportError(message)
+ console.error('Import failed:', error)
+ } finally {
+ setIsImporting(false)
+ }
+ }, [parsedData, columnMapping])
+
+ const handleViewProducts = useCallback(() => {
+ navigate({ to: '/' })
+ }, [navigate])
+
+ const handleImportAnother = useCallback(() => {
+ setStep('file-selection')
+ setFile(null)
+ setParsedData([])
+ setHeaders([])
+ setRowCount(0)
+ setColumnMapping({})
+ setImportedCount(0)
+ }, [])
+
+ const currentStepIndex = STEPS.indexOf(step)
+
+ return (
+
+ {/* Progress indicator */}
+ {step !== 'complete' && (
+
+
+
+ {STEPS.map((s, idx) => {
+ const isActive = s === step
+ const isCompleted = idx < currentStepIndex
+
+ return (
+
+
+
+ {isCompleted ? (
+
+
+
+ ) : (
+ idx + 1
+ )}
+
+
+ {STEP_LABELS[idx]}
+
+
+ {idx < STEPS.length - 1 && (
+
+ )}
+
+ )
+ })}
+
+
+
+ )}
+
+ {/* Step content */}
+
+
+ {step === 'file-selection' && (
+
+ )}
+
+ {step === 'preview' && file && (
+
+ )}
+
+ {step === 'column-mapping' && (
+
+ )}
+
+ {step === 'validation' && (
+
+ )}
+
+ {step === 'complete' && (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/web/src/types/setup.ts b/apps/web/src/types/setup.ts
new file mode 100644
index 0000000..20b1ca1
--- /dev/null
+++ b/apps/web/src/types/setup.ts
@@ -0,0 +1,56 @@
+export type ImportField = {
+ key: 'sku' | 'name' | 'quantity' | 'sector' | 'zone' | 'floor' | 'description' | 'category' | 'unit'
+ label: string
+ required: boolean
+ defaultValue?: string
+}
+
+export const IMPORT_FIELDS: ImportField[] = [
+ { key: 'sku', label: 'SKU', required: true },
+ { key: 'name', label: 'Name', required: true },
+ { key: 'quantity', label: 'Quantity', required: true },
+ { key: 'sector', label: 'Sector', required: false },
+ { key: 'zone', label: 'Zone', required: false },
+ { key: 'floor', label: 'Floor', required: false },
+ { key: 'description', label: 'Description', required: false },
+ { key: 'category', label: 'Category', required: false },
+ { key: 'unit', label: 'Unit', required: false, defaultValue: 'pcs' },
+]
+
+export type ParsedData = Record
+
+export type ColumnMapping = Partial>
+
+export type ValidationError = {
+ row: number
+ field: string
+ message: string
+}
+
+export type ValidationWarning = {
+ row: number
+ field: string
+ message: string
+}
+
+export type ValidationResult = {
+ isValid: boolean
+ errors: ValidationError[]
+ warnings: ValidationWarning[]
+}
+
+export type ImportResult = {
+ imported: number
+}
+
+export type SetupStep = 'file-selection' | 'preview' | 'column-mapping' | 'validation' | 'complete'
+
+export interface SetupState {
+ step: SetupStep
+ file: File | null
+ parsedData: ParsedData[]
+ headers: string[]
+ rowCount: number
+ columnMapping: ColumnMapping
+ validation: ValidationResult
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 8250877..df4eb56 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -15,6 +15,10 @@ const config = defineConfig({
tanstackRouter({ target: 'react', autoCodeSplitting: true }),
viteReact(),
],
+ server: {
+ host: '127.0.0.1',
+ port: 3000,
+ },
})
export default config
diff --git a/package.json b/package.json
index c1b03c7..d7106ed 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,9 @@
"type": "module",
"scripts": {
"dev": "turbo dev",
+ "dev:web": "turbo dev --filter=web",
+ "dev:desktop": "turbo dev --filter=desktop",
+ "dev:all": "turbo run dev --filter=web --filter=desktop",
"build": "turbo build",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
diff --git a/packages/db/package.json b/packages/db/package.json
new file mode 100644
index 0000000..5f72c06
--- /dev/null
+++ b/packages/db/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@wareflow/db",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "dependencies": {
+ "dexie": "^4.0.11"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/packages/db/src/db.ts b/packages/db/src/db.ts
new file mode 100644
index 0000000..e6d1305
--- /dev/null
+++ b/packages/db/src/db.ts
@@ -0,0 +1,67 @@
+import Dexie, { type Table } from 'dexie'
+
+export type Warehouse = {
+ id?: number
+ name: string
+ floors: number
+ createdAt: Date
+ updatedAt: Date
+}
+
+export type Sector = {
+ id?: number
+ warehouseId: number
+ name: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+export type Zone = {
+ id?: number
+ sectorId: number
+ name: string
+ floor: number
+ positionX: number
+ positionY: number
+ width: number
+ height: number
+ color?: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+export type Product = {
+ id?: number
+ sku: string
+ name: string
+ quantity: number
+ sectorId?: number
+ zoneId?: number
+ floor: number
+ positionX?: number
+ positionY?: number
+ description?: string
+ category?: string
+ unit: string
+ createdAt: Date
+ updatedAt: Date
+}
+
+export class WarehouseDB extends Dexie {
+ warehouses!: Table
+ sectors!: Table
+ zones!: Table
+ products!: Table
+
+ constructor() {
+ super('wareflow')
+ this.version(1).stores({
+ warehouses: '++id, name',
+ sectors: '++id, warehouseId, name',
+ zones: '++id, sectorId, name, floor',
+ products: '++id, sku, sectorId, zoneId, floor, category'
+ })
+ }
+}
+
+export const db = new WarehouseDB()
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
new file mode 100644
index 0000000..de83ffa
--- /dev/null
+++ b/packages/db/src/index.ts
@@ -0,0 +1,2 @@
+export * from './db'
+export * from './utils'
diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts
new file mode 100644
index 0000000..a052a60
--- /dev/null
+++ b/packages/db/src/utils.ts
@@ -0,0 +1,85 @@
+import Dexie from 'dexie'
+import { db, type Warehouse, type Sector, type Product } from './db'
+
+export type ParsedData = Record
+export type ColumnMapping = Record
+
+export type ImportResult = {
+ imported: number
+ errors?: string[]
+}
+
+export const isSetupRequired = async (): Promise => {
+ const productCount = await db.products.count()
+ return productCount === 0
+}
+
+const parseNumber = (value: string | undefined): number | undefined => {
+ if (!value) return undefined
+ const num = parseFloat(value)
+ return isNaN(num) ? undefined : num
+}
+
+export const importProducts = async (
+ data: ParsedData[],
+ mapping: ColumnMapping
+): Promise => {
+ const errors: string[] = []
+
+ // Get or create default warehouse
+ let warehouse = await db.warehouses.toCollection().first()
+ if (!warehouse) {
+ const warehouseId = await db.warehouses.add({
+ name: 'Main Warehouse',
+ floors: 6,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
+ warehouse = { id: warehouseId, name: 'Main Warehouse', floors: 6, createdAt: new Date(), updatedAt: new Date() }
+ }
+
+ // Get or create sector
+ const sectorName = data[0]?.[mapping.sector] || 'Default'
+ let sector = await db.sectors.where('name').equals(sectorName).first()
+ if (!sector) {
+ const sectorId = await db.sectors.add({
+ warehouseId: warehouse.id!,
+ name: sectorName,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ })
+ sector = { id: sectorId, warehouseId: warehouse.id!, name: sectorName, createdAt: new Date(), updatedAt: new Date() }
+ }
+
+ const products: Omit[] = data.map(row => ({
+ sku: row[mapping.sku] || '',
+ name: row[mapping.name] || '',
+ quantity: parseNumber(row[mapping.quantity]) || 0,
+ sectorId: sector.id,
+ zoneId: undefined,
+ floor: parseNumber(row[mapping.floor]) || 0,
+ positionX: undefined,
+ positionY: undefined,
+ description: row[mapping.description] || undefined,
+ category: row[mapping.category] || undefined,
+ unit: row[mapping.unit] || 'pcs',
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }))
+
+ try {
+ // Use bulkPut to handle duplicates (update existing or add new)
+ await db.products.bulkPut(products)
+ return { imported: products.length, errors: errors.length > 0 ? errors : undefined }
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unknown error during import'
+ throw new Error(`Import failed: ${message}`)
+ }
+}
+
+export const resetApp = async (): Promise => {
+ await db.products.clear()
+ await db.zones.clear()
+ await db.sectors.clear()
+ await db.warehouses.clear()
+}
diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json
new file mode 100644
index 0000000..5cd4894
--- /dev/null
+++ b/packages/db/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "declaration": true
+ },
+ "include": ["src"]
+}