From 753e62b44f6f5ce9d1fc6058e2ce333ccbd7e2a1 Mon Sep 17 00:00:00 2001 From: mrgenius Date: Sun, 26 Oct 2025 20:32:02 -0500 Subject: [PATCH 1/5] Initial commit --- .github/workflows/deploy.yml | 160 + .gitignore | 27 + API_REFERENCE.md | 572 +++ DEPLOYMENT.md | 432 ++ README.md | 586 ++- SETUP.md | 531 +++ app/globals.css | 125 + app/layout.tsx | 32 + app/page.tsx | 22 + backend | 1 + components.json | 21 + components/auth/auth-page.tsx | 136 + components/form-builder/element-sidebar.tsx | 58 + components/form-builder/elements-sidebar.tsx | 108 + components/form-builder/field-editor.tsx | 123 + components/form-builder/form-builder-page.tsx | 116 + components/form-builder/form-canvas.tsx | 585 +++ components/form-builder/form-editor.tsx | 407 ++ components/form-builder/form-list.tsx | 237 + components/form-builder/left-sidebar.tsx | 67 + .../form-builder/properties-sidebar.tsx | 76 + components/form-builder/section-sidebar.tsx | 95 + components/form-builder/sections-overview.tsx | 100 + components/form-builder/top-tabs.tsx | 39 + components/form-builder/types.ts | 32 + components/theme-provider.tsx | 11 + components/ui/accordion.tsx | 66 + components/ui/alert-dialog.tsx | 157 + components/ui/alert.tsx | 66 + components/ui/aspect-ratio.tsx | 11 + components/ui/avatar.tsx | 53 + components/ui/badge.tsx | 46 + components/ui/breadcrumb.tsx | 109 + components/ui/button-group.tsx | 83 + components/ui/button.tsx | 60 + components/ui/calendar.tsx | 213 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/chart.tsx | 353 ++ components/ui/checkbox.tsx | 32 + components/ui/collapsible.tsx | 33 + components/ui/command.tsx | 184 + components/ui/context-menu.tsx | 252 ++ components/ui/dialog.tsx | 143 + components/ui/drawer.tsx | 135 + components/ui/dropdown-menu.tsx | 257 ++ components/ui/empty.tsx | 104 + components/ui/field.tsx | 244 + components/ui/form.tsx | 167 + components/ui/hover-card.tsx | 44 + components/ui/input-group.tsx | 169 + components/ui/input-otp.tsx | 77 + components/ui/input.tsx | 21 + components/ui/item.tsx | 193 + components/ui/kbd.tsx | 28 + components/ui/label.tsx | 24 + components/ui/menubar.tsx | 276 ++ components/ui/navigation-menu.tsx | 166 + components/ui/pagination.tsx | 127 + components/ui/popover.tsx | 48 + components/ui/progress.tsx | 31 + components/ui/radio-group.tsx | 45 + components/ui/resizable.tsx | 56 + components/ui/scroll-area.tsx | 58 + components/ui/select.tsx | 185 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 139 + components/ui/sidebar.tsx | 726 +++ components/ui/skeleton.tsx | 13 + components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 25 + components/ui/spinner.tsx | 16 + components/ui/switch.tsx | 31 + components/ui/table.tsx | 116 + components/ui/tabs.tsx | 66 + components/ui/textarea.tsx | 18 + components/ui/toast.tsx | 129 + components/ui/toaster.tsx | 39 + components/ui/toggle-group.tsx | 73 + components/ui/toggle.tsx | 47 + components/ui/tooltip.tsx | 61 + components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 191 + docker-compose.yml | 80 + frontend/.dockerignore | 10 + frontend/Dockerfile | 23 + hooks/use-mobile.ts | 19 + hooks/use-toast.ts | 193 + lib/api-client.ts | 175 + lib/auth-context.tsx | 120 + lib/utils.ts | 6 + next.config.mjs | 19 + nginx.conf | 39 + package-lock.json | 3970 +++++++++++++++++ package.json | 73 + pnpm-lock.yaml | 5 + postcss.config.mjs | 8 + public/placeholder-logo.png | Bin 0 -> 568 bytes public/placeholder-logo.svg | 1 + public/placeholder-user.jpg | Bin 0 -> 1635 bytes public/placeholder.jpg | Bin 0 -> 1064 bytes public/placeholder.svg | 1 + styles/globals.css | 125 + tsconfig.json | 41 + 104 files changed, 16055 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 API_REFERENCE.md create mode 100644 DEPLOYMENT.md create mode 100644 SETUP.md create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 160000 backend create mode 100644 components.json create mode 100644 components/auth/auth-page.tsx create mode 100644 components/form-builder/element-sidebar.tsx create mode 100644 components/form-builder/elements-sidebar.tsx create mode 100644 components/form-builder/field-editor.tsx create mode 100644 components/form-builder/form-builder-page.tsx create mode 100644 components/form-builder/form-canvas.tsx create mode 100644 components/form-builder/form-editor.tsx create mode 100644 components/form-builder/form-list.tsx create mode 100644 components/form-builder/left-sidebar.tsx create mode 100644 components/form-builder/properties-sidebar.tsx create mode 100644 components/form-builder/section-sidebar.tsx create mode 100644 components/form-builder/sections-overview.tsx create mode 100644 components/form-builder/top-tabs.tsx create mode 100644 components/form-builder/types.ts create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button-group.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/empty.tsx create mode 100644 components/ui/field.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/item.tsx create mode 100644 components/ui/kbd.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/Dockerfile create mode 100644 hooks/use-mobile.ts create mode 100644 hooks/use-toast.ts create mode 100644 lib/api-client.ts create mode 100644 lib/auth-context.tsx create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 nginx.conf create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 styles/globals.css create mode 100644 tsconfig.json diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..2941c0a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,160 @@ +name: Build and Deploy + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install frontend dependencies + run: | + cd frontend + npm ci + + - name: Build frontend + run: | + cd frontend + npm run build + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: pdo_mysql, mbstring, exif, pcntl, bcmath, gd + + - name: Install backend dependencies + run: | + cd backend + composer install --no-interaction --optimize-autoloader + + - name: Run Laravel optimizations + run: | + cd backend + php artisan config:cache + php artisan route:cache + + - name: Log in to Container Registry + if: github.event_name == 'push' + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push frontend image + if: github.event_name == 'push' + uses: docker/build-push-action@v4 + with: + context: ./frontend + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:${{ github.sha }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend:latest + + - name: Build and push backend image + if: github.event_name == 'push' + uses: docker/build-push-action@v4 + with: + context: ./backend + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:${{ github.sha }} + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend:latest + + test: + runs-on: ubuntu-latest + needs: build + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_DATABASE: form_builder_test + MYSQL_ROOT_PASSWORD: root + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + ports: + - 3306:3306 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: pdo_mysql, mbstring + + - name: Install backend dependencies + run: | + cd backend + composer install --no-interaction + + - name: Run Laravel tests + run: | + cd backend + php artisan test + env: + DB_HOST: 127.0.0.1 + DB_DATABASE: form_builder_test + DB_USERNAME: root + DB_PASSWORD: root + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install frontend dependencies + run: | + cd frontend + npm ci + + - name: Run frontend tests + run: | + cd frontend + npm run test -- --passWithNoTests + + deploy: + runs-on: ubuntu-latest + needs: [build, test] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Deploy to staging + run: | + echo "Deploying to staging environment..." + # Add your deployment script here + # Example: ssh deploy@staging.example.com 'cd /app && docker-compose pull && docker-compose up -d' + + - name: Run smoke tests + run: | + echo "Running smoke tests..." + # Add your smoke test script here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/API_REFERENCE.md b/API_REFERENCE.md new file mode 100644 index 0000000..c0c0473 --- /dev/null +++ b/API_REFERENCE.md @@ -0,0 +1,572 @@ +# API Reference + +Complete API documentation for the Form Builder application. + +## Base URL + +\`\`\` +http://localhost:8000/api +\`\`\` + +## Authentication + +All protected endpoints require a JWT token in the Authorization header: + +\`\`\` +Authorization: Bearer {token} +\`\`\` + +## Response Format + +All API responses follow this format: + +\`\`\`json +{ + "success": true, + "message": "Operation successful", + "data": {} +} +\`\`\` + +## Error Responses + +\`\`\`json +{ + "success": false, + "message": "Error description", + "error": "Detailed error information" +} +\`\`\` + +## Endpoints + +### Authentication + +#### POST /register +Register a new user. + +**Request**: +\`\`\`json +{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + "password_confirmation": "password123" +} +\`\`\` + +**Response** (201): +\`\`\`json +{ + "success": true, + "message": "User registered successfully", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + } +} +\`\`\` + +**Errors**: +- 422: Validation failed (email already exists, password too short, etc.) +- 500: Server error + +--- + +#### POST /login +Authenticate user and receive JWT token. + +**Request**: +\`\`\`json +{ + "email": "john@example.com", + "password": "password123" +} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "message": "Login successful", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + } +} +\`\`\` + +**Errors**: +- 401: Invalid credentials +- 422: Validation failed +- 500: Server error + +--- + +#### GET /me +Get authenticated user details. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "data": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +**Errors**: +- 401: Unauthorized (invalid or missing token) +- 500: Server error + +--- + +#### POST /logout +Invalidate current JWT token. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "message": "Logout successful" +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 500: Server error + +--- + +#### POST /refresh +Refresh JWT token. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "data": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." + } +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 500: Server error + +--- + +### Forms + +#### GET /forms +List all forms for authenticated user. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Query Parameters**: +- `page` (optional): Page number for pagination +- `per_page` (optional): Items per page (default: 15) + +**Response** (200): +\`\`\`json +{ + "success": true, + "data": [ + { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": { + "sections": [ + { + "id": "section-1", + "title": "Personal Details", + "groups": [ + { + "id": "group-1", + "title": "Your Personal Details", + "fields": [ + { + "id": "field-1", + "label": "First Name", + "type": "text", + "required": true + }, + { + "id": "field-2", + "label": "Last Name", + "type": "text", + "required": true + }, + { + "id": "field-3", + "label": "Email Address", + "type": "email", + "required": true + } + ] + } + ] + } + ] + }, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 500: Server error + +--- + +#### POST /forms +Create a new form. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +Content-Type: application/json +\`\`\` + +**Request**: +\`\`\`json +{ + "title": "Membership Application", + "description": "Apply for membership", + "structure": { + "sections": [ + { + "id": "section-1", + "title": "Personal Details", + "groups": [ + { + "id": "group-1", + "title": "Your Personal Details", + "fields": [] + } + ] + } + ] + } +} +\`\`\` + +**Response** (201): +\`\`\`json +{ + "success": true, + "message": "Form created successfully", + "data": { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": {...}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 422: Validation failed (title required, invalid JSON structure) +- 500: Server error + +--- + +#### GET /forms/{id} +Get specific form details. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "data": { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": {...}, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 404: Form not found +- 500: Server error + +--- + +#### PUT /forms/{id} +Update form details and structure. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +Content-Type: application/json +\`\`\` + +**Request**: +\`\`\`json +{ + "title": "Updated Title", + "description": "Updated description", + "structure": { + "sections": [...] + } +} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "message": "Form updated successfully", + "data": { + "id": 1, + "title": "Updated Title", + "description": "Updated description", + "structure": {...}, + "updated_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 404: Form not found +- 422: Validation failed +- 500: Server error + +--- + +#### DELETE /forms/{id} +Delete a form. + +**Headers**: +\`\`\` +Authorization: Bearer {token} +\`\`\` + +**Response** (200): +\`\`\`json +{ + "success": true, + "message": "Form deleted successfully" +} +\`\`\` + +**Errors**: +- 401: Unauthorized +- 404: Form not found +- 500: Server error + +--- + +## Field Types + +Supported field types in form structure: + +| Type | Description | Example | +|------|-------------|---------| +| `text` | Single-line text input | Name, Address | +| `email` | Email input with validation | Email Address | +| `phone` | Phone number input | Phone Number | +| `radio` | Radio button group | Yes/No, Gender | +| `checkbox` | Checkbox group | Interests, Agreements | +| `select` | Dropdown select | Country, State | +| `date` | Date picker | Date of Birth | +| `file` | File upload | Document, Photo | + +--- + +## Form Structure Schema + +\`\`\`json +{ + "sections": [ + { + "id": "section-1", + "title": "Section Title", + "groups": [ + { + "id": "group-1", + "title": "Group Title", + "fields": [ + { + "id": "field-1", + "label": "Field Label", + "type": "text", + "required": true, + "placeholder": "Enter text", + "options": [] + } + ] + } + ] + } + ] +} +\`\`\` + +--- + +## HTTP Status Codes + +| Code | Meaning | +|------|---------| +| 200 | OK - Request successful | +| 201 | Created - Resource created successfully | +| 400 | Bad Request - Invalid request format | +| 401 | Unauthorized - Missing or invalid token | +| 404 | Not Found - Resource not found | +| 422 | Unprocessable Entity - Validation failed | +| 500 | Internal Server Error - Server error | + +--- + +## Rate Limiting + +Currently no rate limiting is implemented. For production, consider implementing: +- 100 requests per minute per user +- 1000 requests per hour per user + +--- + +## Pagination + +List endpoints support pagination: + +\`\`\` +GET /api/forms?page=1&per_page=15 +\`\`\` + +Response includes pagination metadata: +\`\`\`json +{ + "success": true, + "data": [...], + "pagination": { + "current_page": 1, + "per_page": 15, + "total": 50, + "last_page": 4 + } +} +\`\`\` + +--- + +## Examples + +### Complete Workflow + +1. **Register User** +\`\`\`bash +curl -X POST http://localhost:8000/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + "password_confirmation": "password123" + }' +\`\`\` + +2. **Create Form** +\`\`\`bash +curl -X POST http://localhost:8000/api/forms \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My Form", + "description": "My first form", + "structure": {"sections": []} + }' +\`\`\` + +3. **Update Form** +\`\`\`bash +curl -X PUT http://localhost:8000/api/forms/1 \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "Updated Form", + "structure": { + "sections": [{ + "id": "s1", + "title": "Section 1", + "groups": [] + }] + } + }' +\`\`\` + +4. **List Forms** +\`\`\`bash +curl -X GET http://localhost:8000/api/forms \ + -H "Authorization: Bearer {token}" +\`\`\` + +5. **Delete Form** +\`\`\`bash +curl -X DELETE http://localhost:8000/api/forms/1 \ + -H "Authorization: Bearer {token}" +\`\`\` + +--- + +## Support + +For API issues: +- Check error messages in response +- Review this documentation +- Check application logs +- Open GitHub issue diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..329b2f8 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,432 @@ +# Form Builder - Deployment Guide + +## Local Development with Docker + +### Prerequisites +- Docker Desktop installed +- Docker Compose installed +- Git + +### Quick Start + +1. Clone the repository +\`\`\`bash +git clone +cd form-builder +\`\`\` + +2. Create environment files +\`\`\`bash +# Frontend +cp frontend/.env.example frontend/.env.local + +# Backend +cp backend/.env.example backend/.env +\`\`\` + +3. Update backend environment variables +\`\`\`bash +# backend/.env +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=form_builder +DB_USERNAME=form_builder_user +DB_PASSWORD=form_builder_password +JWT_SECRET=your-secret-key-here +CORS_ALLOWED_ORIGINS=http://localhost:3000 +\`\`\` + +4. Start the application +\`\`\`bash +docker-compose up -d +\`\`\` + +5. Run database migrations +\`\`\`bash +docker-compose exec backend php artisan migrate +\`\`\` + +6. Access the application +- Frontend: http://localhost:3000 +- Backend API: http://localhost:8000/api +- Nginx Proxy: http://localhost + +### Stopping the Application +\`\`\`bash +docker-compose down +\`\`\` + +### Viewing Logs +\`\`\`bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f backend +docker-compose logs -f frontend +docker-compose logs -f db +\`\`\` + +## Production Deployment + +### Recommended Platforms + +#### Railway.app (Recommended - Easiest) +**Why**: Simple deployment, automatic HTTPS, free tier available, excellent documentation + +1. **Connect GitHub Repository** + - Go to railway.app + - Click "New Project" + - Select "Deploy from GitHub" + - Authorize and select your repository + +2. **Configure Services** + - Railway auto-detects docker-compose.yml + - Creates services for frontend, backend, database, redis + +3. **Set Environment Variables** + - Go to each service settings + - Add variables from .env.example + - Set production values: + - \`APP_ENV=production\` + - \`APP_DEBUG=false\` + - Generate new \`APP_KEY\`: \`php artisan key:generate\` + - Generate new \`JWT_SECRET\` + +4. **Deploy** + - Push to main branch + - Railway automatically deploys + - Runs migrations automatically + - HTTPS enabled by default + +5. **Access Your App** + - Railway provides public URL + - Example: \`https://form-builder-prod.railway.app\` + +#### Render.com +**Why**: Free tier, good performance, simple deployment + +1. **Create New Web Service** + - Go to render.com + - Click "New +" + - Select "Web Service" + - Connect GitHub repository + +2. **Configure Build** + - Build Command: \`docker-compose build\` + - Start Command: \`docker-compose up\` + +3. **Set Environment Variables** + - Add all variables from .env.example + - Set production values + +4. **Deploy** + - Render automatically deploys on push + - HTTPS enabled automatically + +#### Fly.io +**Why**: Global deployment, edge computing, good free tier + +1. **Install Fly CLI** + \`\`\`bash + curl -L https://fly.io/install.sh | sh + \`\`\` + +2. **Initialize Project** + \`\`\`bash + fly launch + \`\`\` + +3. **Configure fly.toml** + - Set app name + - Configure services + - Set environment variables + +4. **Deploy** + \`\`\`bash + fly deploy + \`\`\` + +#### AWS/DigitalOcean (Self-Hosted) +**Why**: Full control, scalability, suitable for production + +1. **Create Server** + - Ubuntu 22.04 LTS recommended + - Minimum 2GB RAM, 20GB storage + +2. **Install Docker** + \`\`\`bash + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + sudo usermod -aG docker $USER + \`\`\` + +3. **Clone Repository** + \`\`\`bash + git clone + cd form-builder + \`\`\` + +4. **Configure Environment** + \`\`\`bash + cp backend/.env.example backend/.env + # Edit backend/.env with production values + \`\`\` + +5. **Start Services** + \`\`\`bash + docker-compose up -d + docker-compose exec backend php artisan migrate + \`\`\` + +6. **Configure SSL with Let's Encrypt** + \`\`\`bash + sudo apt-get install certbot python3-certbot-nginx + sudo certbot certonly --standalone -d your-domain.com + \`\`\` + +### SSL/HTTPS Configuration + +#### Using Let's Encrypt (Free) + +1. **Install Certbot** + \`\`\`bash + sudo apt-get install certbot python3-certbot-nginx + \`\`\` + +2. **Generate Certificate** + \`\`\`bash + sudo certbot certonly --standalone -d your-domain.com + \`\`\` + +3. **Update nginx.conf** + \`\`\`nginx + server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + # ... rest of configuration + } + + # Redirect HTTP to HTTPS + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + \`\`\` + +4. **Auto-Renewal** + \`\`\`bash + sudo systemctl enable certbot.timer + sudo systemctl start certbot.timer + \`\`\` + +### Environment Variables for Production + +\`\`\`bash +# Application +APP_NAME=FormBuilder +APP_ENV=production +APP_KEY=base64:your-generated-key +APP_DEBUG=false +APP_URL=https://your-domain.com + +# Database +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=form_builder +DB_USERNAME=form_builder_user +DB_PASSWORD=secure_random_password + +# JWT +JWT_SECRET=your-secure-jwt-secret +JWT_ALGORITHM=HS256 +JWT_TTL=60 + +# CORS +CORS_ALLOWED_ORIGINS=https://your-domain.com + +# Mail (optional) +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=465 +MAIL_USERNAME=your-username +MAIL_PASSWORD=your-password +MAIL_FROM_ADDRESS=noreply@your-domain.com + +# Redis +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 +\`\`\` + +## Monitoring & Maintenance + +### Health Checks +\`\`\`bash +# Check API health +curl https://your-domain.com/api/health + +# Check database connection +docker-compose exec backend php artisan tinker +>>> DB::connection()->getPdo(); +\`\`\` + +### Database Backups + +**Automated Backup (Daily)** +\`\`\`bash +# Create backup script +cat > backup.sh << 'EOF' +#!/bin/bash +BACKUP_DIR="/backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +docker-compose exec -T db mysqldump -u form_builder_user -pform_builder_password form_builder > $BACKUP_DIR/backup_$TIMESTAMP.sql +# Keep only last 7 days +find $BACKUP_DIR -name "backup_*.sql" -mtime +7 -delete +EOF + +chmod +x backup.sh + +# Add to crontab +crontab -e +# Add: 0 2 * * * /path/to/backup.sh +\`\`\` + +**Manual Backup** +\`\`\`bash +docker-compose exec db mysqldump -u form_builder_user -pform_builder_password form_builder > backup.sql +\`\`\` + +**Restore Backup** +\`\`\`bash +docker-compose exec -T db mysql -u form_builder_user -pform_builder_password form_builder < backup.sql +\`\`\` + +### Logs & Monitoring + +\`\`\`bash +# View application logs +docker-compose logs -f backend + +# View database logs +docker-compose logs -f db + +# View frontend logs +docker-compose logs -f frontend + +# Export logs +docker-compose logs backend > backend.log +\`\`\` + +### Performance Optimization + +1. **Enable Caching** + \`\`\`bash + docker-compose exec backend php artisan config:cache + docker-compose exec backend php artisan route:cache + \`\`\` + +2. **Database Optimization** + \`\`\`bash + # Add indexes + docker-compose exec backend php artisan tinker + >>> DB::statement('ALTER TABLE forms ADD INDEX idx_user_id (user_id)'); + \`\`\` + +3. **Redis Configuration** + - Use Redis for sessions + - Use Redis for caching + - Configure in .env: \`CACHE_DRIVER=redis\` + +4. **CDN for Static Assets** + - Configure Cloudflare or similar + - Point to your domain + +## Security Checklist + +- [ ] Change default database password +- [ ] Generate strong JWT secret +- [ ] Enable HTTPS/SSL +- [ ] Configure firewall rules (allow only 80, 443) +- [ ] Set up regular backups +- [ ] Enable CORS only for trusted domains +- [ ] Use environment variables for all secrets +- [ ] Keep dependencies updated +- [ ] Enable database encryption +- [ ] Set up monitoring and alerts +- [ ] Configure rate limiting +- [ ] Enable CSRF protection +- [ ] Set security headers in Nginx +- [ ] Regular security audits + +## Troubleshooting + +### Database Connection Issues +\`\`\`bash +docker-compose exec backend php artisan tinker +>>> DB::connection()->getPdo(); +\`\`\` + +### Clear Cache +\`\`\`bash +docker-compose exec backend php artisan cache:clear +docker-compose exec backend php artisan config:clear +\`\`\` + +### Rebuild Images +\`\`\`bash +docker-compose build --no-cache +docker-compose up -d +\`\`\` + +### Check Service Status +\`\`\`bash +docker-compose ps +\`\`\` + +### View Detailed Logs +\`\`\`bash +docker-compose logs --tail=100 backend +\`\`\` + +## Rollback Procedure + +If deployment fails: + +\`\`\`bash +# Stop current deployment +docker-compose down + +# Restore previous database backup +docker-compose up -d db +docker-compose exec -T db mysql -u form_builder_user -pform_builder_password form_builder < backup_previous.sql + +# Restart services +docker-compose up -d +\`\`\` + +## Post-Deployment Checklist + +- [ ] Test user registration +- [ ] Test user login +- [ ] Create test form +- [ ] Verify form persistence +- [ ] Test API endpoints +- [ ] Check HTTPS certificate +- [ ] Verify email notifications (if configured) +- [ ] Test database backups +- [ ] Monitor error logs +- [ ] Load test application +- [ ] Verify monitoring alerts +- [ ] Document deployment details diff --git a/README.md b/README.md index da3226e..b150117 100644 --- a/README.md +++ b/README.md @@ -1 +1,585 @@ -start here. + +# Form Builder Application + +A full-stack dynamic form builder application that allows users to create, manage, and customize forms with a drag-and-drop interface. Built with React/Next.js frontend, Laravel backend, and Docker containerization. + +## Live Demo + +**URL**: + +**Test Credentials**: +- email: demo@example.com +- password: demo1237 + + - email: john@example.com, +- password: password123 + +paostman docs: https://documenter.getpostman.com/view/28328497/2sB3WmS2SV +## Quick Start with Docker + +### Prerequisites +- Docker and Docker Compose installed +- Git + +### Local Development Setup + +1. **Clone the repository** + \`\`\`bash + git clone + cd form-builder + \`\`\` + +2. **Start all services** + \`\`\`bash + docker-compose up -d + \`\`\` + +3. **Run database migrations** + \`\`\`bash + docker-compose exec backend php artisan migrate + \`\`\` + +4. **Create a test user (optional)** + \`\`\`bash + docker-compose exec backend php artisan tinker + >>> User::create(['name' => 'Demo User', 'email' => 'demo@example.com', 'password' => Hash::make('demo1237')]) + \`\`\` + +5. **Access the application** + - Frontend: http://localhost:3000 + - Backend API: http://localhost:8000/api + - Database: localhost:3306 (MySQL) + +### Stopping Services +\`\`\`bash +docker-compose down +\`\`\` + +### Viewing Logs +\`\`\`bash +docker-compose logs -f backend +docker-compose logs -f frontend +\`\`\` + +## API Documentation + +### Base URL +\`\`\` +http://localhost:8000/api +\`\`\` + +### Authentication Endpoints + +#### Register User +\`\`\`http +POST /api/register +Content-Type: application/json + +{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + "password_confirmation": "password123" +} +\`\`\` + +**Response (201)**: +\`\`\`json +{ + "success": true, + "message": "User registered successfully", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "created_at": "2024-01-01T00:00:00Z" + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGc..." + } +} +\`\`\` + +#### Login User +\`\`\`http +POST /api/login +Content-Type: application/json + +{ + "email": "john@example.com", + "password": "password123" +} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "message": "Login successful", + "data": { + "user": { + "id": 1, + "name": "John Doe", + "email": "john@example.com" + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGc..." + } +} +\`\`\` + +#### Get Current User +\`\`\`http +GET /api/me +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "data": { + "id": 1, + "name": "John Doe", + "email": "john@example.com" + } +} +\`\`\` + +#### Logout User +\`\`\`http +POST /api/logout +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "message": "Logout successful" +} +\`\`\` + +#### Refresh Token +\`\`\`http +POST /api/refresh +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "data": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGc..." + } +} +\`\`\` + +### Form Management Endpoints + +#### List All Forms +\`\`\`http +GET /api/forms +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "data": [ + { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": { + "sections": [ + { + "id": "section-1", + "title": "Personal Details", + "groups": [ + { + "id": "group-1", + "title": "Your Personal Details", + "fields": [ + { + "id": "field-1", + "label": "First Name", + "type": "text", + "required": true + } + ] + } + ] + } + ] + }, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + } + ] +} +\`\`\` + +#### Create New Form +\`\`\`http +POST /api/forms +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "Membership Application", + "description": "Apply for membership", + "structure": { + "sections": [ + { + "id": "section-1", + "title": "Personal Details", + "groups": [] + } + ] + } +} +\`\`\` + +**Response (201)**: +\`\`\`json +{ + "success": true, + "message": "Form created successfully", + "data": { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": {...}, + "created_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +#### Get Specific Form +\`\`\`http +GET /api/forms/{id} +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "data": { + "id": 1, + "title": "Membership Application", + "description": "Apply for membership", + "structure": {...}, + "created_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +#### Update Form +\`\`\`http +PUT /api/forms/{id} +Authorization: Bearer {token} +Content-Type: application/json + +{ + "title": "Updated Title", + "description": "Updated description", + "structure": {...} +} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "message": "Form updated successfully", + "data": { + "id": 1, + "title": "Updated Title", + "description": "Updated description", + "structure": {...}, + "updated_at": "2024-01-01T00:00:00Z" + } +} +\`\`\` + +#### Delete Form +\`\`\`http +DELETE /api/forms/{id} +Authorization: Bearer {token} +\`\`\` + +**Response (200)**: +\`\`\`json +{ + "success": true, + "message": "Form deleted successfully" +} +\`\`\` + +## Frontend Features + +### Authentication +- User registration with email and password +- Secure login with JWT token storage +- Automatic token refresh +- Protected routes for authenticated users +- Logout functionality + +### Form Builder +- **Create Forms**: Start new forms with custom titles and descriptions +- **Drag-and-Drop Interface**: Intuitive form structure management +- **Sections & Groups**: Organize forms hierarchically +- **Field Types**: Support for 8 different field types: + - Text Input + - Email Input + - Phone Number + - Radio Buttons + - Checkboxes + - Select Dropdown + - Date Picker + - File Upload + +### Form Management +- **List View**: See all created forms in a dashboard +- **Edit Forms**: Modify form structure and fields +- **Delete Forms**: Remove forms permanently +- **Save Forms**: Auto-save functionality with backend persistence +- **Real-time Updates**: Instant UI updates when forms change + +### User Experience +- Clean, modern interface with teal/green accent colors +- Responsive design for desktop and tablet +- Loading states and error handling +- Confirmation dialogs for destructive actions +- Intuitive field configuration panel + +## Technical Decisions + +### Frontend Architecture +- **Framework**: Next.js 16 with React 19 +- **State Management**: React Context API for authentication, local state for form builder +- **Styling**: Tailwind CSS v4 with shadcn/ui components +- **HTTP Client**: Custom API client with JWT token management +- **Why**: Next.js provides excellent performance, SSR capabilities, and built-in optimizations. React Context is sufficient for this application's state needs. + +### Backend Architecture +- **Framework**: Laravel 10 +- **Authentication**: JWT (JSON Web Tokens) via tymon/jwt-auth +- **Database**: MySQL with Eloquent ORM +- **API Design**: RESTful with consistent JSON response format +- **Why**: Laravel provides robust authentication, validation, and ORM out of the box. JWT is stateless and perfect for API authentication. + +### Database Design +- **Users Table**: Standard authentication with email uniqueness +- **Forms Table**: Stores form metadata and JSON structure +- **Structure Column**: JSON column for flexible form structure storage +- **Why**: JSON column allows flexible form structure without complex normalization, while maintaining relational integrity for user-form relationships. + +### Containerization +- **Docker Compose**: Multi-container setup with separate services +- **Services**: PHP-FPM, Nginx, MySQL, Redis +- **Why**: Ensures consistency across development and production environments, simplifies onboarding, and enables easy scaling. + +## Deployment + +### Recommended Platforms +- **Railway.app** - Simple deployment with automatic HTTPS + + +4. **Deploy** + - Railway automatically detects Docker Compose + - Services deploy automatically + - Database migrations run on first deploy + +5. **Access Your App** + - Railway provides a public URL + - HTTPS is automatically enabled + +### Environment Variables + +**Backend (.env)** +\`\`\` +APP_NAME=Laravel +APP_ENV=production +APP_KEY=base64:919ymTRgcTFUcOFiLXxCHShMrWqPrw7lz2MWTyNco3Y= +APP_DEBUG=false +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=nozomi.proxy.rlwy.net +DB_PORT=19330 +DB_DATABASE=railway +DB_USERNAME=root +DB_PASSWORD=jeVLpXJetxQnuydCQhSyknssabrnmYKP + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000 +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +JWT_SECRET=Jr6M5Uwvle9QCUzk1gcEzub7PEWlpLNAo8mNP7huIdqWe17rOvKexOLiSiR3nKkt + +JWT_ALGO=HS256 + +\`\`\` + +**Frontend (.env.local)** +\`\`\` +NEXT_PUBLIC_API_URL=https://form-builder-backend-production-e5f8.up.railway.app +\`\`\` + +## Project Structure + +\`\`\` +form-builder/ +├── frontend/ # Next.js React application +│ ├── app/ +│ │ ├── page.tsx # Main entry point +│ │ ├── layout.tsx # Root layout +│ │ └── globals.css # Global styles +│ ├── components/ +│ │ ├── auth/ # Authentication components +│ │ └── form-builder/ # Form builder components +│ ├── lib/ +│ │ ├── api-client.ts # API communication +│ │ └── auth-context.tsx # Auth state management +│ └── package.json +├── backend/ # Laravel API +│ ├── app/ +│ │ ├── Http/ +│ │ │ └── Controllers/ # API controllers +│ │ └── Models/ # Database models +│ ├── database/ +│ │ └── migrations/ # Database migrations +│ ├── routes/ +│ │ └── api.php # API routes +│ ├── .env.example # Environment template +│ └── composer.json +├── docker-compose.yml # Multi-container orchestration +├── nginx.conf # Nginx configuration +└── README.md # This file +\`\`\` + +## Known Limitations + +### Current Implementation +- **Form Submissions**: Not implemented - forms are builder-only, not submission-ready +- **Form Versioning**: No version history or rollback functionality +- **Collaboration**: Single-user only, no real-time collaboration +- **Advanced Validations**: Basic field validation only, no custom validation rules +- **File Storage**: File upload fields configured but storage not implemented +- **Form Analytics**: No submission tracking or analytics +- **Conditional Logic**: No field dependencies or conditional visibility +- **Themes**: Single theme only, no customization options + +### Why These Limitations +- **Scope**: These features would significantly increase complexity +- **Time**: MVP focuses on core form building functionality +- **Scalability**: Can be added incrementally without breaking existing code +- **User Feedback**: Better to gather feedback on core features first + +### Future Enhancements +- Form submission and response collection +- Advanced field validation and conditional logic +- Multi-user collaboration with real-time updates +- Form templates and pre-built forms +- Analytics and reporting dashboard +- Custom CSS and theming +- API for embedding forms on external sites +- Mobile app for form filling + +## Troubleshooting + +### Database Connection Error +\`\`\` +Error: SQLSTATE[HY000] [2002] Connection refused +\`\`\` +**Solution**: Ensure MySQL container is running +\`\`\`bash +docker-compose ps +docker-compose restart db +\`\`\` + +### JWT Token Expired +**Solution**: Token automatically refreshes on API calls. If manual refresh needed: +\`\`\`bash +POST /api/refresh +Authorization: Bearer {expired_token} +\`\`\` + +### CORS Errors +**Solution**: Check `CORS_ALLOWED_ORIGINS` in backend `.env` +\`\`\` +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000 +\`\`\` + +### Frontend Can't Connect to Backend +**Solution**: Verify `NEXT_PUBLIC_API_URL` in frontend `.env.local` +\`\`\` +NEXT_PUBLIC_API_URL=https://form-builder-backend-production-e5f8.up.railway.app +\`\`\` + +## Development + +### Running Tests +\`\`\`bash +# Backend tests +docker-compose exec backend php artisan test + +# Frontend tests +docker-compose exec frontend npm test +\`\`\` + +### Code Standards +- Backend: PSR-12 PHP coding standards +- Frontend: ESLint and Prettier configuration included + +### Contributing +1. Create a feature branch +2. Make your changes +3. Submit a pull request + + +>>>>>>> 4f29a71 (Initial commit) diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..49a8d99 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,531 @@ +# Form Builder - Setup Instructions + +## Project Structure + +\`\`\` +form-builder/ +├── frontend/ # Next.js React application +│ ├── app/ +│ │ ├── page.tsx # Main entry point +│ │ ├── layout.tsx # Root layout +│ │ └── globals.css # Global styles +│ ├── components/ +│ │ ├── auth/ # Authentication components +│ │ └── form-builder/ # Form builder components +│ ├── lib/ +│ │ ├── api-client.ts # API communication +│ │ └── auth-context.tsx # Auth state management +│ ├── public/ # Static assets +│ ├── Dockerfile # Frontend container +│ ├── package.json # Dependencies +│ └── tsconfig.json # TypeScript config +├── backend/ # Laravel API +│ ├── app/ +│ │ ├── Http/ +│ │ │ └── Controllers/ # API controllers +│ │ └── Models/ # Database models +│ ├── database/ +│ │ └── migrations/ # Database migrations +│ ├── routes/ +│ │ └── api.php # API routes +│ ├── Dockerfile # Backend container +│ ├── .env.example # Environment template +│ └── composer.json # PHP dependencies +├── docker-compose.yml # Multi-container orchestration +├── nginx.conf # Nginx reverse proxy config +├── README.md # Main documentation +├── DEPLOYMENT.md # Deployment guide +├── API_REFERENCE.md # API documentation +└── QUICK_REFERENCE.md # Quick command reference +\`\`\` + +## Technology Stack + +### Frontend +- **Next.js 16** - React framework with SSR +- **React 19** - UI library +- **TypeScript** - Type safety +- **Tailwind CSS v4** - Utility-first CSS +- **shadcn/ui** - Component library +- **SWR** - Data fetching and caching + +### Backend +- **Laravel 10+** - PHP framework +- **PHP 8.2+** - Programming language +- **MySQL 8.0+** - Database +- **JWT Auth** - Token-based authentication +- **RESTful API** - API design pattern + +### DevOps +- **Docker** - Containerization +- **Docker Compose** - Multi-container orchestration +- **Nginx** - Reverse proxy and web server +- **GitHub Actions** - CI/CD pipeline + +## Prerequisites + +### System Requirements +- **OS**: macOS, Linux, or Windows (with WSL2) +- **RAM**: Minimum 4GB (8GB recommended) +- **Disk**: 10GB free space +- **Internet**: Required for downloading dependencies + +### Required Software +- **Docker Desktop** (includes Docker and Docker Compose) + - [Download for Mac](https://www.docker.com/products/docker-desktop) + - [Download for Windows](https://www.docker.com/products/docker-desktop) + - [Download for Linux](https://docs.docker.com/engine/install/) +- **Git** - Version control + - [Download Git](https://git-scm.com/downloads) +- **Code Editor** (optional but recommended) + - [VS Code](https://code.visualstudio.com/) + - [WebStorm](https://www.jetbrains.com/webstorm/) + +## Quick Start (5 Minutes) + +### 1. Clone Repository +\`\`\`bash +git clone +cd form-builder +\`\`\` + +### 2. Start Docker Services +\`\`\`bash +docker-compose up -d +\`\`\` + +### 3. Run Migrations +\`\`\`bash +docker-compose exec backend php artisan migrate +\`\`\` + +### 4. Access Application +- **Frontend**: http://localhost:3000 +- **Backend API**: https://form-builder-backend-production-e5f8.up.railway.app/api + +### 5. Test Login +- email: demo@example.com +- password: demo1237 + +## Detailed Setup Instructions + +### Step 2: Create Environment Files + +**Backend** +\`\`\`bash +cp backend/.env.example backend/.env +\`\`\` + +**Frontend** +\`\`\`bash +cp frontend/.env.example frontend/.env.local +\`\`\` + +### Step 3: Configure Environment Variables + +**backend/.env** (default values work for local development) +\`\`\`bash +APP_NAME=Laravel +APP_ENV=production +APP_KEY=base64:919ymTRgcTFUcOFiLXxCHShMrWqPrw7lz2MWTyNco3Y= +APP_DEBUG=false +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=nozomi.proxy.rlwy.net +DB_PORT=19330 +DB_DATABASE=railway +DB_USERNAME=root +DB_PASSWORD=jeVLpXJetxQnuydCQhSyknssabrnmYKP + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000 +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +JWT_SECRET=Jr6M5Uwvle9QCUzk1gcEzub7PEWlpLNAo8mNP7huIdqWe17rOvKexOLiSiR3nKkt + +JWT_ALGO=HS256 + +\`\`\` + +**frontend/.env.local** +\`\`\`bash +NEXT_PUBLIC_API_URL=https://form-builder-backend-production-e5f8.up.railway.app +\`\`\` + +### Step 4: Start Docker Services +\`\`\`bash +docker-compose up -d +\`\`\` + +This starts: +- **Frontend** (Next.js) on port 3000 +- **Backend** (Laravel) on port 8000 +- **Database** (MySQL) on port 3306 +- **Redis** on port 6379 +- **Nginx** on port 80 + +### Step 5: Run Database Migrations +\`\`\`bash +docker-compose exec backend php artisan migrate +\`\`\` + +### Step 6: Create Test User (Optional) +\`\`\`bash +docker-compose exec backend php artisan tinker +>>> User::create(['name' => 'Demo User', 'email' => 'demo@example.com', 'password' => Hash::make('demo1237')]) +>>> exit +\`\`\` + +### Step 7: Verify Installation +\`\`\`bash +# Check all services are running +docker-compose ps + +# Test API +curl http://localhost:8000/api/login \ + -H "Content-Type: application/json" \ + -d '{"email":"demo@example.com","password":"demo1237"}' +\`\`\` + +## Development Workflow + +### Running Services +\`\`\`bash +# Start all services +docker-compose up -d + +# Stop all services +docker-compose down + +# View logs +docker-compose logs -f + +# View specific service logs +docker-compose logs -f backend +docker-compose logs -f frontend +\`\`\` + +### Backend Development + +**Run Migrations** +\`\`\`bash +docker-compose exec backend php artisan migrate +\`\`\` + +**Create Migration** +\`\`\`bash +docker-compose exec backend php artisan make:migration create_table_name +\`\`\` + +**Rollback Migrations** +\`\`\`bash +docker-compose exec backend php artisan migrate:rollback +\`\`\` + +**Fresh Migration (Reset Database)** +\`\`\`bash +docker-compose exec backend php artisan migrate:fresh +\`\`\` + +**Run Tests** +\`\`\`bash +docker-compose exec backend php artisan test +\`\`\` + +**Clear Cache** +\`\`\`bash +docker-compose exec backend php artisan cache:clear +docker-compose exec backend php artisan config:clear +\`\`\` + +**Interactive Shell (Tinker)** +\`\`\`bash +docker-compose exec backend php artisan tinker +\`\`\` + +### Frontend Development + +**Install Dependencies** +\`\`\`bash +docker-compose exec frontend npm install +\`\`\` + +**Run Development Server** +\`\`\`bash +docker-compose exec frontend npm run dev +\`\`\` + +**Build for Production** +\`\`\`bash +docker-compose exec frontend npm run build +\`\`\` + +**Run Tests** +\`\`\`bash +docker-compose exec frontend npm test +\`\`\` + +**Lint Code** +\`\`\`bash +docker-compose exec frontend npm run lint +\`\`\` + +### Database Management + +**Access MySQL** +\`\`\`bash +docker-compose exec db mysql -u form_builder_user -pform_builder_password form_builder +\`\`\` + +**Backup Database** +\`\`\`bash +docker-compose exec db mysqldump -u form_builder_user -pform_builder_password form_builder > backup.sql +\`\`\` + +**Restore Database** +\`\`\`bash +docker-compose exec -T db mysql -u form_builder_user -pform_builder_password form_builder < backup.sql +\`\`\` + +## API Integration + +### Base URL +- Development: \`http://localhost:8000/api\` +- Production: \`https://your-domain.com/api\` + +### Authentication +All protected endpoints require JWT token: +\`\`\` +Authorization: Bearer +\`\`\` + +### Response Format +\`\`\`json +{ + "success": true, + "message": "Operation successful", + "data": {} +} +\`\`\` + +### Example API Call +\`\`\`bash +# Register +curl -X POST http://localhost:8000/api/register \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + "password_confirmation": "password123" + }' + +# Login +curl -X POST http://localhost:8000/api/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "password123" + }' + +# Create Form (replace TOKEN) +curl -X POST http://localhost:8000/api/forms \ + -H "Authorization: Bearer TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My Form", + "description": "Test form", + "structure": {"sections": []} + }' +\`\`\` + +## Debugging + +### Enable Debug Mode +\`\`\`bash +# Edit backend/.env +APP_DEBUG=true +\`\`\` + +### View Application Logs +\`\`\`bash +# Backend logs +docker-compose logs -f backend + +# Frontend logs +docker-compose logs -f frontend + +# Database logs +docker-compose logs -f db +\`\`\` + +### Check Service Status +\`\`\`bash +docker-compose ps +\`\`\` + +### Rebuild Services +\`\`\`bash +docker-compose build --no-cache +docker-compose up -d +\`\`\` + +### Common Issues + +**Port Already in Use** +\`\`\`bash +# Find process using port +lsof -i :3000 +lsof -i :8000 + +# Kill process +kill -9 +\`\`\` + +**Database Connection Error** +\`\`\`bash +# Restart database +docker-compose restart db + +# Check logs +docker-compose logs db +\`\`\` + +**Container Won't Start** +\`\`\`bash +# Check logs +docker-compose logs backend + +# Rebuild +docker-compose build --no-cache +docker-compose up -d +\`\`\` + +## IDE Setup + +### VS Code Extensions +- **ES7+ React/Redux/React-Native snippets** +- **Tailwind CSS IntelliSense** +- **Thunder Client** (for API testing) +- **Docker** +- **PHP Intelephense** +- **Laravel Blade Snippets** + +### WebStorm Configuration +- Enable Docker integration +- Configure PHP interpreter from Docker +- Set up Laravel plugin +- Configure Node.js interpreter + +## Git Workflow + +### Create Feature Branch +\`\`\`bash +git checkout -b feature/your-feature-name +\`\`\` + +### Make Changes and Commit +\`\`\`bash +git add . +git commit -m "feat: add your feature" +\`\`\` + +### Push to Remote +\`\`\`bash +git push origin feature/your-feature-name +\`\`\` + +### Create Pull Request +- Go to GitHub +- Create pull request +- Wait for CI/CD checks +- Request review +- Merge after approval + +## Testing + +### Backend Tests +\`\`\`bash +docker-compose exec backend php artisan test +\`\`\` + +### Frontend Tests +\`\`\`bash +docker-compose exec frontend npm test +\`\`\` + +### API Testing with cURL +\`\`\`bash +# Test endpoint +curl -X GET http://localhost:8000/api/forms \ + -H "Authorization: Bearer YOUR_TOKEN" +\`\`\` + +## Performance Tips + +1. **Use Docker volumes** for faster file access +2. **Enable caching** in Laravel +3. **Use Redis** for sessions +4. **Optimize database queries** +5. **Enable gzip compression** in Nginx +6. **Use CDN** for static assets + +## Next Steps + +1. Read [README.md](README.md) for project overview +2. Read [API_REFERENCE.md](API_REFERENCE.md) for API documentation +3. Read [DEPLOYMENT.md](DEPLOYMENT.md) for deployment instructions +4. Start building features! + +## Support & Resources + +- [Laravel Documentation](https://laravel.com/docs) +- [Next.js Documentation](https://nextjs.org/docs) +- [Docker Documentation](https://docs.docker.com) +- [Tailwind CSS](https://tailwindcss.com/docs) +- [shadcn/ui](https://ui.shadcn.com) + +--- + +**Happy coding! 🚀** diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..dc2aea1 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..5342ff1 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,32 @@ +import type React from "react" +import type { Metadata } from "next" +import { Geist, Geist_Mono } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import { AuthProvider } from "@/lib/auth-context" +import { Toaster } from "@/components/ui/toaster" +import "./globals.css" + +const _geist = Geist({ subsets: ["latin"] }) +const _geistMono = Geist_Mono({ subsets: ["latin"] }) + +export const metadata: Metadata = { + title: "Form Builder", + description: "Create and manage dynamic forms", + generator: "v0.app", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..420d51c --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,22 @@ +"use client" + +import { useAuth } from "@/lib/auth-context" +import { AuthPage } from "@/components/auth/auth-page" +import { FormBuilderPage } from "@/components/form-builder/form-builder-page" + +export default function Home() { + const { isAuthenticated, isLoading } = useAuth() + + if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ) + } + + return
{isAuthenticated ? : }
+} diff --git a/backend b/backend new file mode 160000 index 0000000..814bf2d --- /dev/null +++ b/backend @@ -0,0 +1 @@ +Subproject commit 814bf2db88bfe641c34203c19eacf204406bcaa9 diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/auth/auth-page.tsx b/components/auth/auth-page.tsx new file mode 100644 index 0000000..abc83a0 --- /dev/null +++ b/components/auth/auth-page.tsx @@ -0,0 +1,136 @@ +"use client" + +import type React from "react" +import { useState } from "react" +import { useAuth } from "@/lib/auth-context" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { AlertCircle, Info } from "lucide-react" +import { useToast } from "@/hooks/use-toast" + +export function AuthPage() { + const { login, register, error: authError } = useAuth() + const { toast } = useToast() + const [isLogin, setIsLogin] = useState(true) + const [email, setEmail] = useState("demo@example.com") + const [password, setPassword] = useState("demo1237") + const [name, setName] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError("") + setLoading(true) + + try { + if (isLogin) { + await login(email, password) + toast({ + title: "Login successful", + description: "Welcome back!", + }) + } else { + if (!name.trim()) { + setError("Please enter your name") + setLoading(false) + return + } + await register(name, email, password) + toast({ + title: "Registration successful", + description: "Your account has been created successfully.", + }) + } + } catch (err) { + toast({ + title: "Authentication failed", + description: authError || "Please check your credentials and try again.", + variant: "destructive", + }) + setError(authError || "Authentication failed") + } finally { + setLoading(false) + } + } + + return ( +
+ + + {isLogin ? "Login" : "Sign Up"} + + {isLogin ? "Enter your credentials to access the form builder" : "Create a new account to get started"} + + + + + +
+ {error && ( +
+ + {error} +
+ )} + + {!isLogin && ( +
+ + setName(e.target.value)} + disabled={loading} + /> +
+ )} + +
+ + setEmail(e.target.value)} + disabled={loading} + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + /> +
+ + +
+ +
+ + {isLogin ? "Don't have an account? " : "Already have an account? "} + + +
+
+
+
+ ) +} diff --git a/components/form-builder/element-sidebar.tsx b/components/form-builder/element-sidebar.tsx new file mode 100644 index 0000000..8956828 --- /dev/null +++ b/components/form-builder/element-sidebar.tsx @@ -0,0 +1,58 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Type, Radio, CheckSquare, FileUp, List, Calendar, Mail, Phone } from "lucide-react" + +const FIELD_TYPES = [ + { id: "text", label: "Text Input", icon: Type }, + { id: "email", label: "Email", icon: Mail }, + { id: "phone", label: "Phone", icon: Phone }, + { id: "radio", label: "Radio Button", icon: Radio }, + { id: "checkbox", label: "Checkbox", icon: CheckSquare }, + { id: "select", label: "Dropdown", icon: List }, + { id: "date", label: "Date", icon: Calendar }, + { id: "file", label: "File Upload", icon: FileUp }, +] + +interface ElementSidebarProps { + selectedGroupId: string | null + onSelectFieldType: (type: string) => void +} + +export function ElementSidebar({ selectedGroupId, onSelectFieldType }: ElementSidebarProps) { + return ( +
+
+
+

Form Elements

+

Click to add elements to selected group

+
+ +
+ {FIELD_TYPES.map((fieldType) => { + const Icon = fieldType.icon + return ( + + ) + })} +
+ + {!selectedGroupId && ( + + Select a group to add elements + + )} +
+
+ ) +} diff --git a/components/form-builder/elements-sidebar.tsx b/components/form-builder/elements-sidebar.tsx new file mode 100644 index 0000000..a704569 --- /dev/null +++ b/components/form-builder/elements-sidebar.tsx @@ -0,0 +1,108 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import { Search, Plus, X, Type, Mail, Phone, Circle, Square, ChevronDown, Calendar, Paperclip, FileText, Hash, Link, Lock } from "lucide-react" +import { Input } from "@/components/ui/input" + +const AVAILABLE_ELEMENTS = [ + { id: "text", label: "Text Input", icon: Type, color: "text-blue-600" }, + { id: "email", label: "Email", icon: Mail, color: "text-green-600" }, + { id: "phone", label: "Phone Number", icon: Phone, color: "text-purple-600" }, + { id: "radio", label: "Radio Button", icon: Circle, color: "text-orange-600" }, + { id: "checkbox", label: "Checkbox", icon: Square, color: "text-red-600" }, + { id: "select", label: "Dropdown", icon: ChevronDown, color: "text-indigo-600" }, + { id: "date", label: "Date Picker", icon: Calendar, color: "text-pink-600" }, + { id: "file", label: "File Upload", icon: Paperclip, color: "text-yellow-600" }, + { id: "textarea", label: "Text Area", icon: FileText, color: "text-teal-600" }, + { id: "number", label: "Number", icon: Hash, color: "text-cyan-600" }, + { id: "url", label: "URL", icon: Link, color: "text-lime-600" }, + { id: "password", label: "Password", icon: Lock, color: "text-gray-600" }, +] + +interface ElementsSidebarProps { + onDragStart: (elementId: string) => void + onDragEnd: () => void +} + +export function ElementsSidebar({ onDragStart, onDragEnd }: ElementsSidebarProps) { + const [searchQuery, setSearchQuery] = useState("") + const [isCollapsed, setIsCollapsed] = useState(true) + + const filteredElements = AVAILABLE_ELEMENTS.filter((el) => el.label.toLowerCase().includes(searchQuery.toLowerCase())) + + const handleDragStart = (e: React.DragEvent, elementId: string) => { + e.dataTransfer.effectAllowed = "copy" + e.dataTransfer.setData("elementType", elementId) + onDragStart(elementId) + } + + if (isCollapsed) { + return ( +
+
+ +
+
+ + Add Element + +
+
+ ) + } + + return ( +
+ {/* Header */} +
+

Elements

+ +
+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 h-8 text-sm" + /> +
+
+ + {/* Elements list */} +
+
+ {filteredElements.map((element) => ( +
handleDragStart(e, element.id)} + onDragEnd={onDragEnd} + className="p-3 rounded-lg cursor-move hover:bg-gray-50 transition-colors border border-gray-200 bg-white flex items-center gap-3 text-sm text-gray-700 select-none shadow-sm" + > + + {element.label} +
+ ))} +
+
+
+ ) +} diff --git a/components/form-builder/field-editor.tsx b/components/form-builder/field-editor.tsx new file mode 100644 index 0000000..a6db988 --- /dev/null +++ b/components/form-builder/field-editor.tsx @@ -0,0 +1,123 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Checkbox } from "@/components/ui/checkbox" +import { Textarea } from "@/components/ui/textarea" +import { Trash2, Plus, X } from "lucide-react" +import type { FormField } from "./types" + +interface FieldEditorProps { + field: FormField + onUpdate: (updates: Partial) => void + onDelete: () => void +} + +export function FieldEditor({ field, onUpdate, onDelete }: FieldEditorProps) { + const [newOption, setNewOption] = useState("") + + const handleAddOption = () => { + if (newOption.trim()) { + const currentOptions = field.options || [] + onUpdate({ options: [...currentOptions, newOption.trim()] }) + setNewOption("") + } + } + + const handleRemoveOption = (index: number) => { + const currentOptions = field.options || [] + onUpdate({ options: currentOptions.filter((_, i) => i !== index) }) + } + + const needsOptions = ["radio", "checkbox", "select"].includes(field.type) + + return ( +
+
+

Field Properties

+ +
+ +
+
+ + onUpdate({ label: e.target.value })} + placeholder="Field label" + /> +
+ +
+ + onUpdate({ placeholder: e.target.value })} + placeholder="Enter placeholder text" + /> +
+ +
+ onUpdate({ required: checked as boolean })} + /> + +
+ + {needsOptions && ( +
+ +
+ {(field.options || []).map((option, index) => ( +
+ { + const newOptions = [...(field.options || [])] + newOptions[index] = e.target.value + onUpdate({ options: newOptions }) + }} + className="flex-1" + /> + +
+ ))} +
+ setNewOption(e.target.value)} + placeholder="Add new option" + onKeyPress={(e) => e.key === "Enter" && handleAddOption()} + className="flex-1" + /> + +
+
+
+ )} +
+
+ ) +} diff --git a/components/form-builder/form-builder-page.tsx b/components/form-builder/form-builder-page.tsx new file mode 100644 index 0000000..a59aa80 --- /dev/null +++ b/components/form-builder/form-builder-page.tsx @@ -0,0 +1,116 @@ +"use client" + +import { useState, useEffect } from "react" +import { useAuth } from "@/lib/auth-context" +import { apiClient, type Form } from "@/lib/api-client" +import { Button } from "@/components/ui/button" +import { FormList } from "./form-list" +import { FormEditor } from "./form-editor" +import { User, LogOut, ChevronDown } from "lucide-react" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function FormBuilderPage() { + const { logout, user } = useAuth() + const [selectedFormId, setSelectedFormId] = useState(null) + const [forms, setForms] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + loadForms() + }, []) + + const loadForms = async () => { + try { + setLoading(true) + const response = await apiClient.getForms() + if (response.success && response.data) { + setForms(response.data) + } + } catch (err) { + console.error("Failed to load forms:", err) + } finally { + setLoading(false) + } + } + + const handleLogout = async () => { + await logout() + } + + const handleCreateForm = async (title: string, description: string) => { + try { + const response = await apiClient.createForm(title, description, { sections: [] }) + if (response.success && response.data) { + setForms([response.data, ...forms]) + setSelectedFormId(response.data.id.toString()) + } + } catch (err) { + console.error("Failed to create form:", err) + } + } + + const handleDeleteForm = async (id: string) => { + try { + await apiClient.deleteForm(id) + setForms(forms.filter((f) => f.id !== id)) + } catch (err) { + console.error("Failed to delete form:", err) + } + } + + return ( +
+
+
+
+

Form Builder

+

Create and manage your custom forms

+
+ + + + + + +
+

{user?.name || 'User'}

+

{user?.email || ''}

+
+
+ + + + Logout + +
+
+
+
+ +
+ {selectedFormId ? ( + setSelectedFormId(null)} onSave={() => loadForms()} /> + ) : ( + + )} +
+
+ ) +} diff --git a/components/form-builder/form-canvas.tsx b/components/form-builder/form-canvas.tsx new file mode 100644 index 0000000..3f62ac2 --- /dev/null +++ b/components/form-builder/form-canvas.tsx @@ -0,0 +1,585 @@ +"use client" + +import React, { useState } from "react" +import { + Plus, + Trash2, + GripVertical, + Eye, + EyeOff, + Copy, + Settings, + Move, + FileText, + ImageIcon, + List, +} from "lucide-react" +import type { FormSection, FormField, FormGroup } from "./types" + +/** + * Utility helpers + */ +const uid = (prefix = "") => `${prefix}${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` + +const cloneGroup = (g: FormGroup): FormGroup => ({ + ...g, + id: uid("g-"), + fields: g.fields.map((f) => ({ ...f, id: uid("f-") })), +}) + +interface FormCanvasProps { + sections: FormSection[] + selectedSectionId: string | null + selectedGroupId: string | null + selectedFieldId: string | null + onSelectSection: (id: string | null) => void + onSelectGroup: (id: string | null) => void + onSelectField: (id: string | null) => void + onUpdateSections: (sections: FormSection[]) => void + groupsOnly?: boolean +} + +/** + * FormCanvas component for rendering and editing form sections/groups/fields. + */ +export default function FormCanvas({ + sections, + selectedSectionId, + selectedGroupId, + selectedFieldId, + onSelectSection, + onSelectGroup, + onSelectField, + onUpdateSections, + groupsOnly = false, +}: FormCanvasProps) { + // Get the current section (assuming groupsOnly means we work with the selected section's groups) + const currentSection = selectedSectionId ? sections.find(s => s.id === selectedSectionId) : sections[0] + const groups = currentSection?.groups || [] + + // collapsed state for groups + const [collapsedGroups, setCollapsedGroups] = useState>(new Set()) + + // drag state: indicates what's being dragged and origin + const [dragging, setDragging] = useState< + | { + type: "group" + groupId: string + } + | { + type: "field" + fieldId: string + fromGroupId: string + } + | null + >(null) + + // drag over target indicator (groupId + optional fieldId for insertion before) + const [dragOver, setDragOver] = useState<{ groupId?: string; beforeFieldId?: string | null } | null>(null) + + /* ---------------- handlers ---------------- */ + + const toggleCollapse = (groupId: string) => { + setCollapsedGroups((prev) => { + const next = new Set(prev) + if (next.has(groupId)) next.delete(groupId) + else next.add(groupId) + return next + }) + } + + const addGroup = (indexAfter?: number) => { + const newGroup: FormGroup = { id: uid("g-"), title: "Group Title", required: false, fields: [] } + const updatedGroups = [...groups] + if (indexAfter === undefined) updatedGroups.push(newGroup) + else updatedGroups.splice(indexAfter + 1, 0, newGroup) + updateSectionsWithGroups(updatedGroups) + } + + const deleteGroup = (groupId: string) => { + const updatedGroups = groups.filter((g) => g.id !== groupId) + updateSectionsWithGroups(updatedGroups) + } + + const copyGroup = (groupId: string) => { + const source = groups.find((g) => g.id === groupId) + if (!source) return + const cloned = cloneGroup(source) + const idx = groups.findIndex((g) => g.id === groupId) + const updatedGroups = [...groups] + updatedGroups.splice(idx + 1, 0, cloned) + updateSectionsWithGroups(updatedGroups) + } + + const updateGroupTitle = (groupId: string, value: string) => { + const updatedGroups = groups.map((g) => (g.id === groupId ? { ...g, title: value } : g)) + updateSectionsWithGroups(updatedGroups) + } + + const toggleGroupRequired = (groupId: string, value: boolean) => { + const updatedGroups = groups.map((g) => (g.id === groupId ? { ...g, required: value } : g)) + updateSectionsWithGroups(updatedGroups) + } + + const addFieldToGroup = (groupId: string, field?: Partial) => { + const newField: FormField = { + id: uid("f-"), + type: field?.type || "text", + label: field?.label || "Untitled", + placeholder: field?.placeholder, + options: field?.options, + required: field?.required || false, + fullWidth: field?.fullWidth || false, + } + const updatedGroups = groups.map((g) => (g.id === groupId ? { ...g, fields: [...g.fields, newField] } : g)) + updateSectionsWithGroups(updatedGroups) + } + + const deleteField = (groupId: string, fieldId: string) => { + const updatedGroups = groups.map((g) => (g.id === groupId ? { ...g, fields: g.fields.filter((f) => f.id !== fieldId) } : g)) + updateSectionsWithGroups(updatedGroups) + } + + const toggleFieldRequired = (groupId: string, fieldId: string, value: boolean) => { + const updatedGroups = groups.map((g) => + g.id === groupId ? { ...g, fields: g.fields.map((f) => (f.id === fieldId ? { ...f, required: value } : f)) } : g + ) + updateSectionsWithGroups(updatedGroups) + } + + const updateSectionsWithGroups = (updatedGroups: FormGroup[]) => { + if (!currentSection) return + const updatedSections = sections.map(s => s.id === currentSection.id ? { ...s, groups: updatedGroups } : s) + onUpdateSections(updatedSections) + } + + /* ---------------- drag & drop (groups) ---------------- */ + + const onGroupDragStart = (e: React.DragEvent, groupId: string) => { + try { + e.dataTransfer.setData("application/x-builder", JSON.stringify({ type: "group", groupId })) + e.dataTransfer.effectAllowed = "move" + } catch {} + setDragging({ type: "group", groupId }) + } + + const onGroupDragOver = (e: React.DragEvent, targetGroupId: string) => { + e.preventDefault() + // we show target group but no before field + setDragOver({ groupId: targetGroupId, beforeFieldId: null }) + } + + const onGroupDrop = (e: React.DragEvent, targetGroupId: string) => { + e.preventDefault() + if (!dragging) return + if (dragging.type !== "group") { + setDragging(null) + setDragOver(null) + return + } + if (dragging.groupId === targetGroupId) { + setDragging(null) + setDragOver(null) + return + } + const fromIndex = groups.findIndex((g) => g.id === dragging.groupId) + const toIndex = groups.findIndex((g) => g.id === targetGroupId) + if (fromIndex === -1 || toIndex === -1) { + setDragging(null) + setDragOver(null) + return + } + const updatedGroups = [...groups] + const [moved] = updatedGroups.splice(fromIndex, 1) + // insert after the target (mimics the screenshot flow) + updatedGroups.splice(toIndex + (toIndex > fromIndex ? 0 : 1), 0, moved) + updateSectionsWithGroups(updatedGroups) + setDragging(null) + setDragOver(null) + } + + /* ---------------- drag & drop (fields) ---------------- */ + + const onFieldDragStart = (e: React.DragEvent, fromGroupId: string, fieldId: string) => { + e.stopPropagation() + try { + e.dataTransfer.setData("application/x-builder", JSON.stringify({ type: "field", fromGroupId, fieldId })) + e.dataTransfer.effectAllowed = "move" + } catch {} + setDragging({ type: "field", fromGroupId, fieldId }) + } + + const onFieldDragOver = (e: React.DragEvent, targetGroupId: string, beforeFieldId?: string | null) => { + e.preventDefault() + setDragOver({ groupId: targetGroupId, beforeFieldId: beforeFieldId ?? null }) + } + + const onFieldDrop = (e: React.DragEvent, targetGroupId: string, beforeFieldId?: string | null) => { + e.preventDefault() + + // Check if we're dropping an element from the sidebar + const elementType = e.dataTransfer.getData("elementType") + if (elementType) { + // Adding new element from sidebar + const newField: FormField = { + id: uid("f-"), + type: elementType as FormField["type"], + label: elementType.charAt(0).toUpperCase() + elementType.slice(1) + " Field", + placeholder: "", + required: false, + fullWidth: false, + } + const insertIdx = beforeFieldId ? Math.max(0, groups.find(g => g.id === targetGroupId)?.fields.findIndex((f) => f.id === beforeFieldId) || 0) : groups.find(g => g.id === targetGroupId)?.fields.length || 0 + const updatedGroups = groups.map((g) => + g.id === targetGroupId + ? { ...g, fields: [...g.fields.slice(0, insertIdx), newField, ...g.fields.slice(insertIdx)] } + : g + ) + updateSectionsWithGroups(updatedGroups) + setDragging(null) + setDragOver(null) + return + } + + // Handle moving existing fields + if (!dragging || dragging.type !== "field") { + setDragging(null) + setDragOver(null) + return + } + const { fieldId, fromGroupId } = dragging + if (!fieldId || !fromGroupId) { + setDragging(null) + setDragOver(null) + return + } + // remove from source + const groupsCopy = groups.map((g) => ({ ...g, fields: [...g.fields] })) + const srcGroup = groupsCopy.find((g) => g.id === fromGroupId) + const targetGroup = groupsCopy.find((g) => g.id === targetGroupId) + if (!srcGroup || !targetGroup) { + setDragging(null) + setDragOver(null) + return + } + const fieldIndex = srcGroup.fields.findIndex((f) => f.id === fieldId) + if (fieldIndex === -1) { + setDragging(null) + setDragOver(null) + return + } + const [movedField] = srcGroup.fields.splice(fieldIndex, 1) + // if moving within same group and beforeFieldId is right after removal, handle indexes + if (fromGroupId === targetGroupId) { + // determine insertion index + const insertIdx = beforeFieldId ? Math.max(0, targetGroup.fields.findIndex((f) => f.id === beforeFieldId)) : targetGroup.fields.length + targetGroup.fields.splice(insertIdx, 0, movedField) + } else { + const insertIdx = beforeFieldId ? Math.max(0, targetGroup.fields.findIndex((f) => f.id === beforeFieldId)) : targetGroup.fields.length + targetGroup.fields.splice(insertIdx, 0, movedField) + } + // commit + updateSectionsWithGroups(groupsCopy) + setDragging(null) + setDragOver(null) + } + + /* ---------------- UI Rendering Helpers ---------------- */ + + const renderFieldInputPreview = (f: FormField) => { + switch (f.type) { + case "textarea": + return