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>
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
# 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: 'mycrib.treytartt.com' }, // API media
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Dokku Deployment
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```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: '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
|
||||
|
||||
```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.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
|
||||
Reference in New Issue
Block a user