diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6e09b4f9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run lint + run: bun run lint + + - name: Run tests + run: bun run test + + - name: Run build + run: bun run build diff --git a/DEVOPS.md b/DEVOPS.md new file mode 100644 index 00000000..2da20bc1 --- /dev/null +++ b/DEVOPS.md @@ -0,0 +1,102 @@ +# DevOps Strategy for Form Engine Project + +## 1. Overview +This document outlines the DevOps strategy for the Form Engine project, focusing on the frontend component and its integration points. The strategy prioritizes automation, quality assurance, and seamless deployment using modern CI/CD practices. + +## 2. Architecture & DevOps Diagram + +```mermaid +graph TD + subgraph "Development Environment" + Dev[Developer] + Git[Local Git] + end + + subgraph "Source Control (GitHub)" + Repo[form-engine-frontend Repo] + PR[Pull Request] + Main[Main Branch] + end + + subgraph "CI/CD Pipeline (GitHub Actions)" + Lint[Lint & Format] + Test[Unit Tests (Vitest)] + Build[Build Check] + Deploy[Deploy to Vercel] + end + + subgraph "Production Environment" + Vercel[Vercel Edge Network] + Browser[User Browser] + end + + subgraph "External Dependencies" + API[Backend API (http://localhost:8000 / Production URL)] + DB[Database] + end + + Dev -->|Commit Code| Git + Git -->|Push| Repo + Repo -->|Create PR| PR + PR -->|Trigger| Lint + Lint --> Test + Test --> Build + Build -->|Merge| Main + Main -->|Trigger| Deploy + Deploy --> Vercel + Vercel -->|Serve App| Browser + Browser -->|Fetch Data| API + API -->|Query| DB +``` + +## 3. Components & Deployment Strategy + +### 3.1 Frontend Component +- **Source Code Repository**: `https://github.com/Nandgopal-R/form-engine-frontend` +- **Deployment Location**: + - **Provider**: [Vercel](https://vercel.com) (Recommended for Vite/React apps) or AWS S3 + CloudFront. + - **URL**: Production URL (e.g., `https://form-engine.vercel.app`) +- **Configuration**: + - Environment variables must be used for dynamic configuration (e.g., `VITE_API_URL` instead of hardcoded `http://localhost:8000`). +- **Tests & Checks Strategy**: + 1. **Static Analysis**: + - **Linting**: `bun run lint` (ESLint) to ensure code quality and catch errors early. + - **Formatting**: `bun run format` (Prettier) to enforce code style. + - **Type Checking**: `tsc` to verify TypeScript types. + 2. **Unit & Integration Tests**: + - **Command**: `bun run test` (Vitest). + - **Scope**: Components, Hooks, and Utility functions. + 3. **Build Verification**: + - **Command**: `bun run build`. + - Ensures the application builds successfully without errors before deployment. + +### 3.2 Backend Component (External Integration) +*Note: This component is referenced as a dependency.* +- **Source Code Repository**: `form-engine` (Assumed) +- **Tests & Checks**: + - API Contract Tests to ensure changes don't break the frontend. + +## 4. Tools, Platforms, and Libraries + +The following tools and platforms are selected for the DevOps lifecycle: + +| Category | Tool/Platform | Purpose | +| :--- | :--- | :--- | +| **Source Control** | **GitHub** | Version control and collaboration. | +| **CI/CD** | **GitHub Actions** | Automated pipelines for testing and deployment. | +| **Build Tool** | **Vite** | Fast frontend build tool. | +| **Hosting** | **Vercel** | Optimized hosting for frontend assets. | +| **Testing** | **Vitest** | Fast unit testing framework. | +| **Linting** | **ESLint** | Javascript/TypeScript linting. | +| **Formatting** | **Prettier** | Code formatting. | +| **Containerization** | **Docker** (Optional) | For consistent local dev or containerized deployment if Vercel is not used. | + +## 5. CI/CD Pipeline Workflow + +The pipeline is triggered on push to `main` or pull requests. + +1. **Install Dependencies**: `bun install` +2. **Lint & Format Check**: `bun run check` (Runs Prettier & ESLint) +3. **Run Tests**: `bun run test` +4. **Build Application**: `bun run build` +5. **Deploy** (Only on `main` branch): Deploy artifacts to Vercel. diff --git a/src/components/Header.test.tsx b/src/components/Header.test.tsx index 676df3bc..ae8d9a4d 100644 --- a/src/components/Header.test.tsx +++ b/src/components/Header.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import Header from './Header'; // Mock TanStack Router Link diff --git a/src/components/app-sidebar.test.tsx b/src/components/app-sidebar.test.tsx index 512cbb22..49b7baac 100644 --- a/src/components/app-sidebar.test.tsx +++ b/src/components/app-sidebar.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { RouterProvider, createMemoryHistory, createRootRoute, createRouter } from '@tanstack/react-router'; import { AppSidebar } from './app-sidebar'; import { SidebarProvider } from '@/components/ui/sidebar'; -import { createRouter, createMemoryHistory, RouterProvider, createRootRoute } from '@tanstack/react-router'; // Mock useIsMobile hook vi.mock('@/hooks/use-mobile', () => ({ diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 3ed0c32f..dc226630 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -73,7 +73,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) { const navigate = useNavigate() const { data: session } = authClient.useSession() - const userName = session?.user?.name || session?.user?.email || 'User' + const userName = session?.user.name || session?.user.email || 'User' const userInitial = userName.charAt(0).toUpperCase() const handleLogout = async () => { @@ -114,7 +114,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
{userName} - {session?.user?.email && session?.user?.name && ( + {session?.user.email && session.user.name && ( {session.user.email} )}
diff --git a/src/components/editor-canvas.test.tsx b/src/components/editor-canvas.test.tsx index 8cccf729..c6ec997e 100644 --- a/src/components/editor-canvas.test.tsx +++ b/src/components/editor-canvas.test.tsx @@ -1,10 +1,10 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { EditorCanvas } from './editor-canvas'; import type { CanvasField } from './fields/field-preview'; -const mockFields: CanvasField[] = [ +const mockFields: Array = [ { id: '1', type: 'text', label: 'Field 1' }, { id: '2', type: 'number', label: 'Field 2' }, ]; diff --git a/src/components/editor-sidebar-tabs.test.tsx b/src/components/editor-sidebar-tabs.test.tsx index 4c40004e..a9d5a584 100644 --- a/src/components/editor-sidebar-tabs.test.tsx +++ b/src/components/editor-sidebar-tabs.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { TabsLine } from './editor-sidebar-tabs'; describe('TabsLine', () => { diff --git a/src/components/field-properties.test.tsx b/src/components/field-properties.test.tsx index e58a923b..51ef361e 100644 --- a/src/components/field-properties.test.tsx +++ b/src/components/field-properties.test.tsx @@ -1,6 +1,6 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FieldProperties } from './field-properties'; import type { CanvasField } from './fields/field-preview'; diff --git a/src/components/field-properties.tsx b/src/components/field-properties.tsx index 552f1390..38d19e9e 100644 --- a/src/components/field-properties.tsx +++ b/src/components/field-properties.tsx @@ -14,7 +14,10 @@ */ import { useEffect, useState } from 'react' +import { ChevronDown } from 'lucide-react' +import { ValidationRuleBuilder } from './validation-rule-builder' import type { CanvasField } from './fields/field-preview' +import type { ValidationConfig } from '@/lib/validation-engine' import { Button } from '@/components/ui/button' import { Dialog, @@ -38,9 +41,6 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' -import { ChevronDown } from 'lucide-react' -import { ValidationRuleBuilder } from './validation-rule-builder' -import type { ValidationConfig } from '@/lib/validation-engine' interface FieldPropertiesProps { field: CanvasField | null // Field being edited (null when no field selected) diff --git a/src/components/fields/field-items.test.tsx b/src/components/fields/field-items.test.tsx index 593af319..f5b3487a 100644 --- a/src/components/fields/field-items.test.tsx +++ b/src/components/fields/field-items.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { FieldItems } from './field-items'; describe('FieldItems', () => { diff --git a/src/components/fields/field-preview.test.tsx b/src/components/fields/field-preview.test.tsx index 95a3daaa..d86b650d 100644 --- a/src/components/fields/field-preview.test.tsx +++ b/src/components/fields/field-preview.test.tsx @@ -1,7 +1,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; -import { FieldPreview, type CanvasField } from './field-preview'; +import { describe, expect, it, vi } from 'vitest'; +import { FieldPreview } from './field-preview'; +import type {CanvasField} from './field-preview'; const mockField: CanvasField = { id: 'test-id', diff --git a/src/components/fields/field-preview.tsx b/src/components/fields/field-preview.tsx index 4709b9ec..311a5ff3 100644 --- a/src/components/fields/field-preview.tsx +++ b/src/components/fields/field-preview.tsx @@ -1,4 +1,5 @@ import { Settings, Star, Trash2 } from 'lucide-react' +import type { ValidationConfig } from '@/lib/validation-engine' import { Card } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Checkbox } from '@/components/ui/checkbox' @@ -6,7 +7,6 @@ import { Label } from '@/components/ui/label' import { Button } from '@/components/ui/button' import { Field, FieldContent, FieldLabel } from '@/components/ui/field' import { Slider } from '@/components/ui/slider' -import type { ValidationConfig } from '@/lib/validation-engine' export interface CanvasField { id: string diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx index 9a91c2e7..5d0ccf11 100644 --- a/src/components/nav-main.tsx +++ b/src/components/nav-main.tsx @@ -1,8 +1,8 @@ 'use client' import { ChevronRight } from 'lucide-react' -import type { LucideIcon } from 'lucide-react' import { Link } from '@tanstack/react-router' +import type { LucideIcon } from 'lucide-react' import { Collapsible, diff --git a/src/components/ui/badge.test.tsx b/src/components/ui/badge.test.tsx index 5cccd8ae..d37e7cb0 100644 --- a/src/components/ui/badge.test.tsx +++ b/src/components/ui/badge.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { Badge } from './badge'; describe('Badge', () => { diff --git a/src/components/ui/button.test.tsx b/src/components/ui/button.test.tsx index fdd740db..68c93613 100644 --- a/src/components/ui/button.test.tsx +++ b/src/components/ui/button.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Button } from './button'; describe('Button', () => { diff --git a/src/components/ui/complex-primitives.test.tsx b/src/components/ui/complex-primitives.test.tsx index 58a140ef..236a82fb 100644 --- a/src/components/ui/complex-primitives.test.tsx +++ b/src/components/ui/complex-primitives.test.tsx @@ -1,14 +1,14 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; import { Switch } from './switch'; import { Slider } from './slider'; import { Sheet, - SheetTrigger, SheetContent, + SheetDescription, SheetHeader, SheetTitle, - SheetDescription + SheetTrigger } from './sheet'; describe('Interactive UI Components', () => { diff --git a/src/components/ui/field.test.tsx b/src/components/ui/field.test.tsx index bbd3f88f..1bf8561b 100644 --- a/src/components/ui/field.test.tsx +++ b/src/components/ui/field.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { Field, - FieldLabel, FieldContent, FieldDescription, FieldError, FieldGroup, + FieldLabel, FieldLegend, FieldSet } from './field'; diff --git a/src/components/ui/field.tsx b/src/components/ui/field.tsx index 53aac96c..c4c1e3cf 100644 --- a/src/components/ui/field.tsx +++ b/src/components/ui/field.tsx @@ -203,7 +203,7 @@ function FieldError({ ...new Map(errors.map((error) => [error?.message, error])).values(), ] - if (uniqueErrors?.length == 1) { + if (uniqueErrors.length == 1) { return uniqueErrors[0]?.message } diff --git a/src/components/ui/input.test.tsx b/src/components/ui/input.test.tsx index 794be45e..ef8f7b99 100644 --- a/src/components/ui/input.test.tsx +++ b/src/components/ui/input.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { Input } from './input'; describe('Input', () => { diff --git a/src/components/ui/primitives.test.tsx b/src/components/ui/primitives.test.tsx index 4ee9d980..2cc5f6b5 100644 --- a/src/components/ui/primitives.test.tsx +++ b/src/components/ui/primitives.test.tsx @@ -1,5 +1,5 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, it, expect, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; import { Checkbox } from './checkbox'; import { Separator } from './separator'; import { Skeleton } from './skeleton'; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index fdfb99fe..d7e071ad 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,7 +1,8 @@ import * as React from "react" import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" import { X } from "lucide-react" +import type {VariantProps} from "class-variance-authority"; import { cn } from "@/lib/utils" diff --git a/src/components/validation-rule-builder.test.tsx b/src/components/validation-rule-builder.test.tsx index 19114aa9..423c247f 100644 --- a/src/components/validation-rule-builder.test.tsx +++ b/src/components/validation-rule-builder.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect, vi, beforeAll } from 'vitest'; -import { ValidationRuleBuilder } from './validation-rule-builder'; +import { beforeAll, describe, expect, it, vi } from 'vitest'; import userEvent from '@testing-library/user-event'; +import { ValidationRuleBuilder } from './validation-rule-builder'; // Mock pointer capture for Radix UI beforeAll(() => { diff --git a/src/components/validation-rule-builder.tsx b/src/components/validation-rule-builder.tsx index ff786e1d..39a15698 100644 --- a/src/components/validation-rule-builder.tsx +++ b/src/components/validation-rule-builder.tsx @@ -1,3 +1,8 @@ + +import { useMemo, useState } from 'react' +import { AlertTriangle, CheckCircle2, Info, Plus, X } from 'lucide-react' +import type { RuleTemplate, ValidationConfig } from '@/lib/validation-engine'; + /** * Validation Rule Builder Component * @@ -11,9 +16,6 @@ * The component uses a template-based system where common validation patterns * are pre-configured and users just need to set parameters. */ - -import { useState, useMemo } from 'react' -import { Plus, X, Info, AlertTriangle, CheckCircle2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -32,11 +34,11 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { - RULE_TEMPLATES, - getRulesForFieldType, PREDEFINED_PATTERNS, - type ValidationConfig, - type RuleTemplate, + RULE_TEMPLATES, + + + getRulesForFieldType } from '@/lib/validation-engine' interface ValidationRuleBuilderProps { @@ -85,6 +87,7 @@ export function ValidationRuleBuilder({ currentValidation = {}, onChange, }: ValidationRuleBuilderProps) { + // State for currently active validation rules on this field const [activeRules, setActiveRules] = useState(() => { // Initialize active rules from existing validation config @@ -135,9 +138,9 @@ export function ValidationRuleBuilder({ // Group rules by category const rulesByCategory = useMemo(() => { - const grouped: Record = {} + const grouped: Record> = {} for (const rule of applicableRules) { - if (!grouped[rule.category]) { + if (!Object.prototype.hasOwnProperty.call(grouped, rule.category)) { grouped[rule.category] = [] } grouped[rule.category].push(rule) @@ -146,6 +149,7 @@ export function ValidationRuleBuilder({ }, [applicableRules]) // Build validation config from active rules + const buildConfig = ( rules: ActiveRule[], required?: boolean, diff --git a/src/hooks/use-mobile.test.ts b/src/hooks/use-mobile.test.ts index 9104d5dd..34e1276c 100644 --- a/src/hooks/use-mobile.test.ts +++ b/src/hooks/use-mobile.test.ts @@ -1,5 +1,5 @@ -import { renderHook, act } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useIsMobile } from './use-mobile'; describe('useIsMobile', () => { diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts index d4d79a4c..7fa2b569 100644 --- a/src/hooks/use-toast.ts +++ b/src/hooks/use-toast.ts @@ -68,7 +68,7 @@ type Action = } interface State { - toasts: ToasterToast[] + toasts: Array } // Store timeout references for proper cleanup diff --git a/src/lib/auth-client.test.ts b/src/lib/auth-client.test.ts index 652e307b..f9409ffe 100644 --- a/src/lib/auth-client.test.ts +++ b/src/lib/auth-client.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { authClient } from './auth-client'; describe('authClient', () => { diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index c8a99d71..eed162b7 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; import { cn } from './utils'; describe('cn utility', () => { @@ -7,12 +7,12 @@ describe('cn utility', () => { }); it('handles conditional classes (truthy)', () => { - const isActive = true; + const isActive = true as boolean; expect(cn('base', isActive && 'active')).toBe('base active'); }); it('handles conditional classes (falsy)', () => { - const isActive = false; + const isActive = false as boolean; expect(cn('base', isActive && 'active')).toBe('base'); }); diff --git a/src/lib/validation-engine.test.ts b/src/lib/validation-engine.test.ts index 8a401309..ae21e92e 100644 --- a/src/lib/validation-engine.test.ts +++ b/src/lib/validation-engine.test.ts @@ -1,15 +1,16 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { - validateField, - validateForm, + PREDEFINED_PATTERNS, + RULE_TEMPLATES, + buildValidationConfig, - getRulesForFieldType, combinePatterns, getPatternDescription, - PREDEFINED_PATTERNS, - RULE_TEMPLATES, - type ValidationConfig, + getRulesForFieldType, + validateField, + validateForm } from './validation-engine'; +import type {ValidationConfig} from './validation-engine'; describe('Validation Engine', () => { describe('getPatternDescription', () => { diff --git a/src/lib/validation-engine.ts b/src/lib/validation-engine.ts index 01c33eef..9f98e261 100644 --- a/src/lib/validation-engine.ts +++ b/src/lib/validation-engine.ts @@ -632,7 +632,7 @@ export function buildValidationConfig( * Combine multiple regex patterns with AND logic * This creates a pattern that matches if ALL patterns match */ -export function combinePatterns(patterns: string[]): string { +export function combinePatterns(patterns: Array): string { if (patterns.length === 0) return '' if (patterns.length === 1) return patterns[0] @@ -644,7 +644,7 @@ export function combinePatterns(patterns: string[]): string { /** * Get rules applicable to a field type */ -export function getRulesForFieldType(fieldType: string): RuleTemplate[] { +export function getRulesForFieldType(fieldType: string): Array { const textTypes = ['text', 'textarea', 'Input', 'input'] const numberTypes = ['number', 'slider', 'cgpa'] diff --git a/src/main.tsx b/src/main.tsx index c950f24c..ddb56cda 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -23,7 +23,6 @@ declare module '@tanstack/react-router' { const rootElement = document.getElementById('app') if (rootElement && !rootElement.innerHTML) { - const queryClient = new QueryClient() const root = ReactDOM.createRoot(rootElement) root.render( diff --git a/src/routes/_layout.analytics.index.tsx b/src/routes/_layout.analytics.index.tsx index 5032909e..9291a3a8 100644 --- a/src/routes/_layout.analytics.index.tsx +++ b/src/routes/_layout.analytics.index.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { Loader2, AlertCircle, FileText, Users, ClipboardList } from 'lucide-react' +import { AlertCircle, ClipboardList, FileText, Loader2, Users } from 'lucide-react' import { formsApi } from '@/api/forms' import { Card, diff --git a/src/routes/_layout.analytics.responses.tsx b/src/routes/_layout.analytics.responses.tsx index 6a7c7e1b..167cab12 100644 --- a/src/routes/_layout.analytics.responses.tsx +++ b/src/routes/_layout.analytics.responses.tsx @@ -1,9 +1,10 @@ import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { Loader2, AlertCircle, FileText } from 'lucide-react' -import { responsesApi } from '@/api/responses' -import { formsApi, type Form } from '@/api/forms' +import { AlertCircle, FileText, Loader2 } from 'lucide-react' import type { FormResponseForOwner } from '@/api/responses' +import type { Form } from '@/api/forms'; +import { responsesApi } from '@/api/responses' +import { formsApi } from '@/api/forms' import { Card, CardContent, @@ -114,14 +115,14 @@ function ResponsesPage() { {/* Loading / Error handling */} - {(isFormsLoading || isAllResponsesLoading) && ( + {isAllResponsesLoading && (
Loading responses...
)} - {forms && forms.length === 0 && ( + {forms.length === 0 && (
No forms found. Create a form first to collect responses.
@@ -135,19 +136,19 @@ function ResponsesPage() { {group.formTitle || 'Untitled Form'} - {group.responses?.length + {group.responses.length ? `${group.responses.length} response${group.responses.length === 1 ? '' : 's'}` : 'No responses yet'} - {group.responses && group.responses.length > 0 ? ( + {group.responses.length > 0 ? (
{group.responses.map((response, idx) => (

Response #{idx + 1}

- {Object.entries(response.answers || {}).map(([k, v]) => ( + {Object.entries(response.answers).map(([k, v]) => (
{k}:
@@ -167,7 +168,7 @@ function ResponsesPage() { ))}
) : ( - !isFormsLoading && !isAllResponsesLoading && ( + !isAllResponsesLoading && (
No responses received yet.
) )} diff --git a/src/routes/_layout.analytics.tsx b/src/routes/_layout.analytics.tsx index 0e9724e2..1cb253d9 100644 --- a/src/routes/_layout.analytics.tsx +++ b/src/routes/_layout.analytics.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Outlet } from '@tanstack/react-router' +import { Outlet, createFileRoute } from '@tanstack/react-router' export const Route = createFileRoute('/_layout/analytics')({ component: AnalyticsLayout, diff --git a/src/routes/_layout.dashboard.tsx b/src/routes/_layout.dashboard.tsx index 61cfc48c..cc955084 100644 --- a/src/routes/_layout.dashboard.tsx +++ b/src/routes/_layout.dashboard.tsx @@ -1,14 +1,13 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { authClient } from '@/lib/auth-client' -import { useEffect, useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { AlertCircle, FileX, Plus, Search } from 'lucide-react' +import { authClient } from '@/lib/auth-client' import { FormCard } from '@/components/form-card' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { formsApi } from '@/api/forms' import { Input } from '@/components/ui/input' -import { useState } from 'react' import { useToast } from '@/hooks/use-toast' export const Route = createFileRoute('/_layout/dashboard')({ diff --git a/src/routes/_layout.editor.index.tsx b/src/routes/_layout.editor.index.tsx index e1db40d2..6f0915cc 100644 --- a/src/routes/_layout.editor.index.tsx +++ b/src/routes/_layout.editor.index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useMutation } from '@tanstack/react-query' -import { FileText, ArrowRight } from 'lucide-react' +import { ArrowRight, FileText } from 'lucide-react' import type { CreateFormInput } from '@/api/forms' import { Button } from '@/components/ui/button' diff --git a/src/routes/_layout.my-responses.tsx b/src/routes/_layout.my-responses.tsx index ff2ffc41..c39db642 100644 --- a/src/routes/_layout.my-responses.tsx +++ b/src/routes/_layout.my-responses.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Link } from '@tanstack/react-router' +import { Link, createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { AlertCircle, Calendar, CheckCircle, ClipboardList, ExternalLink, FileEdit, Loader2 } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' diff --git a/src/routes/_layout.tsx b/src/routes/_layout.tsx index c6df39a1..ef30027b 100644 --- a/src/routes/_layout.tsx +++ b/src/routes/_layout.tsx @@ -5,6 +5,7 @@ import { useMatches, useNavigate, } from '@tanstack/react-router' +import { useEffect } from 'react' import { SidebarInset, SidebarProvider, @@ -13,7 +14,6 @@ import { import { AppSidebar } from '@/components/app-sidebar' import { Separator } from '@/components/ui/separator' import { authClient } from '@/lib/auth-client' -import { useEffect } from 'react' import { Breadcrumb, BreadcrumbItem, diff --git a/src/routes/email-verified.tsx b/src/routes/email-verified.tsx index 858cac45..19b6bc0e 100644 --- a/src/routes/email-verified.tsx +++ b/src/routes/email-verified.tsx @@ -1,7 +1,7 @@ -import { createFileRoute, Link } from '@tanstack/react-router' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Link, createFileRoute } from '@tanstack/react-router' import { CheckCircle2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' export const Route = createFileRoute('/email-verified')({ component: EmailVerified, diff --git a/src/routes/form.$formId.tsx b/src/routes/form.$formId.tsx index 72e03101..7273d03a 100644 --- a/src/routes/form.$formId.tsx +++ b/src/routes/form.$formId.tsx @@ -24,6 +24,7 @@ import { Star, } from 'lucide-react' import type { FormField } from '@/api/forms' +import type { ValidationError } from '@/lib/validation-engine'; import { Input } from '@/components/ui/input' import { Checkbox } from '@/components/ui/checkbox' import { Switch } from '@/components/ui/switch' @@ -34,6 +35,7 @@ import { Field, FieldContent, FieldLabel } from '@/components/ui/field' import { fieldsApi, formsApi } from '@/api/forms' import { responsesApi } from '@/api/responses' import { useToast } from '@/hooks/use-toast' + import { validateForm, validateField, @@ -339,7 +341,7 @@ function FormResponsePage() {
- {!formWithFields.fields || formWithFields.fields.length === 0 ? ( + {formWithFields.fields.length === 0 ? (
This form has no fields yet.
@@ -373,7 +375,7 @@ function FormResponsePage() { }) )} - {formWithFields.fields && formWithFields.fields.length > 0 && ( + {formWithFields.fields.length > 0 && (