Files
honeyDueWeb/docs/05-polish-deploy.md
T
Trey t e2172c20f2 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across Web project:
- Package name: casera-web -> honeydue-web
- Cookie: casera-token -> honeydue-token
- Theme store: casera-theme -> honeydue-theme
- File sharing: .casera -> .honeydue, component/function renames
- casera-file-handler.tsx -> honeydue-file-handler.tsx
- All UI text, metadata, OG tags updated
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Demo data emails updated
- All documentation updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:59 -06:00

400 lines
11 KiB
Markdown

# 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