# Phase 5 — Polish & Deploy

Responsive design, error handling, deployment, testing, and performance optimization.

## Checklist

- [ ] Responsive design (mobile-first, works on phone browsers too)
- [ ] Loading states, empty states, error handling for every screen
- [ ] Dockerfile + Dokku deployment config
- [ ] E2E tests (Playwright) for critical paths
- [ ] Performance optimization (code splitting, image optimization)
- [ ] SEO + Open Graph meta tags for marketing/demo pages
- [ ] Accessibility audit (keyboard navigation, screen readers, ARIA labels)
- [ ] Analytics integration (PostHog)

---

## 1. Responsive Design

### Breakpoint Strategy

| Breakpoint | Target | Layout |
|------------|--------|--------|
| `<640px` | Mobile phones | Bottom tab bar, stacked cards, full-width forms |
| `640-1023px` | Tablets | Collapsed sidebar (icons only), 2-column grids |
| `≥1024px` | Desktop | Full sidebar, 3-column grids, side panels |

### Mobile Adaptations

- **Navigation**: Bottom tab bar replaces sidebar (matches iOS app experience)
- **Kanban board**: Horizontal scroll with swipeable columns (single column visible at a time on small screens)
- **Cards**: Full-width stacked layout
- **Forms**: Single column, full-width inputs
- **Tables**: Convert to card-based layout on mobile
- **Dialogs**: Full-screen on mobile, centered modal on desktop

### Implementation

Use Tailwind responsive prefixes consistently:

```tsx
// Grid that adapts from 1 → 2 → 3 columns
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
  {residences.map(r => <ResidenceCard key={r.id} residence={r} />)}
</div>

// Sidebar visibility
<aside className="hidden lg:flex lg:w-64 ...">  {/* Desktop sidebar */}
<nav className="lg:hidden fixed bottom-0 ...">   {/* Mobile bottom bar */}
```

---

## 2. Loading, Empty, and Error States

### Loading States

Every data-dependent component must show a loading skeleton:

```tsx
function ResidenceList() {
  const { data, isLoading, error } = useResidences();

  if (isLoading) return <ResidenceListSkeleton />;
  if (error) return <ErrorBanner error={error} onRetry={() => refetch()} />;
  if (!data?.length) return <EmptyState icon="building" title="No residences" cta="Add Residence" />;

  return <div>{data.map(r => <ResidenceCard key={r.id} residence={r} />)}</div>;
}
```

### Skeleton Components

Use shadcn/ui `Skeleton` for consistent loading states:
- Card skeletons for list pages
- Form skeletons for detail pages
- Kanban column skeletons for task board

### Empty States

Every list has a meaningful empty state:

| Screen | Icon | Title | Subtitle | CTA |
|--------|------|-------|----------|-----|
| Residences | Building | No residences yet | Add your first property to get started | Add Residence |
| Tasks | CheckSquare | No tasks | Create a task to start tracking maintenance | Add Task |
| Contractors | HardHat | No contractors | Save your trusted service providers | Add Contractor |
| Documents | FileText | No documents | Store important home documents | Add Document |

### Error Handling

```typescript
// Global error boundary
// src/app/error.tsx
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <AlertCircle className="h-12 w-12 text-destructive" />
      <h2 className="mt-4 text-lg font-semibold">Something went wrong</h2>
      <p className="mt-2 text-muted-foreground">{error.message}</p>
      <Button onClick={reset} className="mt-4">Try Again</Button>
    </div>
  );
}
```

### Toast Notifications

Use shadcn/ui `Toaster` for success/error feedback:
- "Residence created successfully" (green)
- "Task completed" (green)
- "Failed to save changes" (red)
- "Connection lost — retrying..." (yellow)

---

## 3. Dockerfile + Deployment

### Dockerfile

```dockerfile
# Build stage
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D nextjs

# Copy standalone build
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public

USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:3000/ || exit 1

CMD ["node", "server.js"]
```

### next.config.ts

```typescript
const nextConfig = {
  output: 'standalone', // Required for Docker
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'honeyDue.treytartt.com' }, // API media
    ],
  },
};
```

### Dokku Deployment

```bash
# On honeyDueDev server
dokku apps:create honeydue-web
dokku domains:add honeydue-web app.honeyDue.treytartt.com
dokku config:set honeydue-web NEXT_PUBLIC_API_URL=https://honeyDue.treytartt.com/api
dokku letsencrypt:enable honeydue-web

# Deploy
git remote add dokku-web dokku@honeyDueDev:honeydue-web
git push dokku-web main
```

### Environment Variables

| Variable | Description | Required |
|----------|-------------|----------|
| `NEXT_PUBLIC_API_URL` | Go API base URL | Yes |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog project key | No |
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No |
| `PORT` | Server port (default: 3000) | No |

---

## 4. E2E Tests (Playwright)

### Critical Path Tests

```
tests/
├── auth.spec.ts           # Login, register, logout
├── residences.spec.ts     # Create, view, edit, delete residence
├── tasks.spec.ts          # Create task, kanban drag, complete task
├── contractors.spec.ts    # Create, view, edit, delete contractor
├── documents.spec.ts      # Create, upload, view, delete document
├── demo.spec.ts           # Demo mode flows (no backend)
└── responsive.spec.ts     # Mobile/tablet viewport tests
```

### Test Structure

```typescript
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('login with valid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/app/residences');
  });

  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.fill('[name="email"]', 'bad@example.com');
    await page.fill('[name="password"]', 'wrong');
    await page.click('button[type="submit"]');
    await expect(page.locator('[role="alert"]')).toBeVisible();
  });
});
```

### Demo Mode Tests (No Backend)

```typescript
// tests/demo.spec.ts
test.describe('Demo Mode', () => {
  test('loads with mock data', async ({ page }) => {
    await page.goto('/demo');
    await page.click('text=Start Demo');
    await expect(page.locator('text=Maple Street House')).toBeVisible();
  });

  test('can create a task in demo mode', async ({ page }) => {
    await page.goto('/demo/tasks');
    await page.click('text=Add Task');
    await page.fill('[name="title"]', 'Test Demo Task');
    await page.click('button[type="submit"]');
    await expect(page.locator('text=Test Demo Task')).toBeVisible();
  });
});
```

### Playwright Config

```typescript
// playwright.config.ts
export default defineConfig({
  testDir: './tests',
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
    { name: 'Mobile Safari', use: { ...devices['iPhone 14'] } },
    { name: 'Tablet', use: { viewport: { width: 768, height: 1024 } } },
  ],
});
```

---

## 5. Performance Optimization

### Code Splitting

- Dynamic imports for heavy components:
  ```typescript
  const KanbanBoard = dynamic(() => import('@/components/kanban/KanbanBoard'), {
    loading: () => <KanbanSkeleton />,
  });
  const RechartsDashboard = dynamic(() => import('@/components/dashboard/Charts'));
  ```

### Image Optimization

- Use Next.js `<Image>` component for all images
- API media served through Next.js image optimization proxy
- Lazy load images below the fold

### Bundle Analysis

```bash
npm install -D @next/bundle-analyzer
# Analyze with: ANALYZE=true npm run build
```

### TanStack Query Optimization

- `staleTime: 60 * 60 * 1000` (1 hour) for most queries
- `refetchOnWindowFocus: true` for fresh data when user returns
- `keepPreviousData: true` for smooth pagination/filtering transitions
- Prefetch on hover for navigation links

---

## 6. SEO + Open Graph

Marketing and demo pages need proper meta tags:

```typescript
// src/app/(marketing)/page.tsx
export const metadata: Metadata = {
  title: 'honeyDue — Home Maintenance Made Simple',
  description: 'Track tasks, organize contractors, store documents. Manage your home maintenance in one place.',
  openGraph: {
    title: 'honeyDue — Home Maintenance Made Simple',
    description: 'Track tasks, organize contractors, store documents.',
    images: ['/og-image.png'],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'honeyDue',
    description: 'Home Maintenance Made Simple',
    images: ['/og-image.png'],
  },
};
```

---

## 7. Accessibility

### Requirements

- All interactive elements keyboard accessible
- Proper ARIA labels on icons and buttons
- Focus management on modals and route changes
- Color contrast meets WCAG 2.1 AA
- Screen reader compatible forms with error announcements

### Testing

```bash
# axe-core integration
npm install -D @axe-core/playwright

# In tests:
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
```

---

## 8. Analytics (PostHog)

```typescript
// src/lib/analytics.ts
import posthog from 'posthog-js';

export function initAnalytics() {
  if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
      api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://analytics.88oakapps.com',
    });
  }
}

export function trackEvent(event: string, properties?: Record<string, any>) {
  posthog.capture(event, properties);
}

export function trackScreen(screenName: string) {
  posthog.capture('$pageview', { $current_url: screenName });
}
```

Track same events as mobile app for consistent analytics.

---

## Deliverables

At the end of Phase 5, you should have:
1. Fully responsive web app (mobile, tablet, desktop)
2. Consistent loading, empty, and error states everywhere
3. Deployed on Dokku at `app.honeyDue.treytartt.com`
4. E2E tests covering auth, CRUD, demo mode, and responsive viewports
5. Optimized bundle with code splitting and image optimization
6. SEO-ready marketing and demo pages
7. Accessible UI meeting WCAG 2.1 AA
8. PostHog analytics tracking
