Files
honeyDueWeb/docs/05-polish-deploy.md
Trey t 5a50d77515 feat: complete Phase 3 — advanced features for Casera web app
Adds sharing (residence share codes, join, user management, .casera file
export/import), subscription status with feature comparison, notification
preferences with bell icon, profile settings (edit info, change password,
theme picker, delete account), onboarding wizard with create/join paths,
enhanced dashboard with stats cards, Recharts completion chart, recent
activity feed, and task report PDF download.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:31:29 -06:00

11 KiB

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:

// 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:

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

// 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

# 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

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

Dokku Deployment

# On mycribDev server
dokku apps:create casera-web
dokku domains:add casera-web app.casera.treytartt.com
dokku config:set casera-web NEXT_PUBLIC_API_URL=https://mycrib.treytartt.com/api
dokku letsencrypt:enable casera-web

# Deploy
git remote add dokku-web dokku@mycribDev:casera-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

// 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)

// 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

// 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:
    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

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:

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

# 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)

// 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.casera.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