feat: Phase 4-5 — demo mode, polish, deploy, and bug fixes

Add demo mode with mock data provider, Docker deployment, Playwright
tests, PostHog analytics, error boundaries, and SEO metadata. Fix
residences API response unwrapping, kanban drag-and-drop with optimistic
updates, trailing slash proxy redirects, and column name mismatches with
Go API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-03 11:37:41 -06:00
parent 5a50d77515
commit 7884ebbfd4
133 changed files with 3904 additions and 300 deletions
+173
View File
@@ -0,0 +1,173 @@
// ---------------------------------------------------------------------------
// DataProvider interface — abstraction over real API vs. in-memory demo store
// ---------------------------------------------------------------------------
// Types are imported from the API modules (the actual source of truth for hooks).
// ---------------------------------------------------------------------------
import type {
CreateResidenceRequest,
UpdateResidenceRequest,
ResidenceResponse,
MyResidenceResponse,
ResidenceSummaryResponse,
ShareCodeResponse,
SharePackageResponse,
GenerateShareCodeRequest,
ResidenceUserResponse,
TasksReportResponse,
MessageResponse as ResidenceMessageResponse,
} from '@/lib/api/residences';
import type {
CreateTaskRequest,
UpdateTaskRequest,
TaskResponse,
KanbanResponse,
CompletionResponse,
CreateCompletionRequest,
MessageResponse as TaskMessageResponse,
} from '@/lib/api/tasks';
import type {
CreateContractorRequest,
UpdateContractorRequest,
ContractorResponse,
ToggleFavoriteResponse,
ContractorTaskResponse,
} from '@/lib/api/contractors';
import type {
DocumentListParams,
CreateDocumentRequest,
UpdateDocumentRequest,
DocumentResponse,
MessageResponse as DocMessageResponse,
} from '@/lib/api/documents';
import type { StaticDataResponse } from '@/lib/api/lookups';
import type {
NotificationListResponse,
UnreadCountResponse,
NotificationPreferencesResponse,
UpdatePreferencesRequest,
} from '@/lib/api/notifications';
import type {
SubscriptionStatusResponse,
FeatureBenefitResponse,
UpgradeTriggerResponse,
} from '@/lib/api/subscription';
import type { UserResponse } from '@/lib/api/auth';
// Unified MessageResponse (all API modules define the same shape)
type MessageResponse = ResidenceMessageResponse | TaskMessageResponse | DocMessageResponse;
// ---------------------------------------------------------------------------
// Domain-split interface
// ---------------------------------------------------------------------------
export interface DataProvider {
basePath: string;
residences: {
list(): Promise<ResidenceResponse[]>;
get(id: number): Promise<ResidenceResponse>;
create(data: CreateResidenceRequest): Promise<ResidenceResponse>;
update(id: number, data: UpdateResidenceRequest): Promise<ResidenceResponse>;
delete(id: number): Promise<MessageResponse>;
getMyResidences(): Promise<MyResidenceResponse[]>;
getSummary(): Promise<ResidenceSummaryResponse>;
};
tasks: {
list(days?: number): Promise<KanbanResponse>;
get(id: number): Promise<TaskResponse>;
create(data: CreateTaskRequest): Promise<TaskResponse>;
update(id: number, data: UpdateTaskRequest): Promise<TaskResponse>;
delete(id: number): Promise<MessageResponse>;
getByResidence(residenceId: number, days?: number): Promise<KanbanResponse>;
getCompletions(taskId: number): Promise<CompletionResponse[]>;
createCompletion(data: CreateCompletionRequest): Promise<CompletionResponse>;
createCompletionWithImages(
data: { task_id: number; notes?: string; actual_cost?: number; completed_at?: string },
images: File[],
): Promise<CompletionResponse>;
markInProgress(id: number): Promise<TaskResponse>;
cancel(id: number): Promise<TaskResponse>;
uncancel(id: number): Promise<TaskResponse>;
archive(id: number): Promise<TaskResponse>;
unarchive(id: number): Promise<TaskResponse>;
quickComplete(id: number): Promise<void>;
};
contractors: {
list(): Promise<ContractorResponse[]>;
get(id: number): Promise<ContractorResponse>;
create(data: CreateContractorRequest): Promise<ContractorResponse>;
update(id: number, data: UpdateContractorRequest): Promise<ContractorResponse>;
delete(id: number): Promise<MessageResponse>;
toggleFavorite(id: number): Promise<ToggleFavoriteResponse>;
getTasks(id: number): Promise<ContractorTaskResponse[]>;
};
documents: {
list(params?: DocumentListParams): Promise<DocumentResponse[]>;
listWarranties(): Promise<DocumentResponse[]>;
get(id: number): Promise<DocumentResponse>;
create(data: CreateDocumentRequest): Promise<DocumentResponse>;
createWithFile(data: CreateDocumentRequest, file: File): Promise<DocumentResponse>;
update(id: number, data: UpdateDocumentRequest): Promise<DocumentResponse>;
delete(id: number): Promise<MessageResponse>;
};
lookups: {
getStaticData(): Promise<StaticDataResponse>;
};
sharing: {
getShareCode(residenceId: number): Promise<{ share_code: ShareCodeResponse | null }>;
generateShareCode(residenceId: number, data?: GenerateShareCodeRequest): Promise<ShareCodeResponse>;
generateSharePackage(residenceId: number, data?: GenerateShareCodeRequest): Promise<SharePackageResponse>;
getResidenceUsers(residenceId: number): Promise<ResidenceUserResponse[]>;
removeUser(residenceId: number, userId: number): Promise<MessageResponse>;
joinWithCode(data: { code: string }): Promise<ResidenceResponse>;
generateTasksReport(residenceId: number, email?: string): Promise<TasksReportResponse>;
};
notifications: {
list(limit?: number, offset?: number): Promise<NotificationListResponse>;
getUnreadCount(): Promise<UnreadCountResponse>;
getPreferences(): Promise<NotificationPreferencesResponse>;
updatePreferences(data: UpdatePreferencesRequest): Promise<NotificationPreferencesResponse>;
markAsRead(id: number): Promise<MessageResponse>;
markAllAsRead(): Promise<MessageResponse>;
};
subscription: {
getStatus(): Promise<SubscriptionStatusResponse>;
getFeatureBenefits(): Promise<FeatureBenefitResponse[]>;
getUpgradeTriggers(): Promise<UpgradeTriggerResponse[]>;
};
auth: {
getCurrentUser(): Promise<UserResponse>;
logout(): Promise<MessageResponse>;
};
}
// Re-export types that hooks/consumers will need
export type {
CreateResidenceRequest,
UpdateResidenceRequest,
CreateTaskRequest,
UpdateTaskRequest,
CreateCompletionRequest,
CreateContractorRequest,
UpdateContractorRequest,
CreateDocumentRequest,
UpdateDocumentRequest,
DocumentListParams,
UpdatePreferencesRequest,
};