-
Current Week
-
Your weekly goals will appear here
+
+
+
+
+ Dream Book
+ Dream Connect
+ Scorecard
+ Dream Team
+ People
+ Build Overview
+ Health Check
+
+
+
+
+
+
+
+ Quick Stats
+
+
+
This Week
+
Goals completed: 0/0
-
-
-
Dream Book
-
Your dreams will appear here
+
+
This Month
+
Dreams updated: 0
-
-
-
Progress
-
Your progress will appear here
+
-
-
-
- 🚧 Monorepo Migration in Progress
-
-
- The application is being migrated to a modern NextJS monorepo architecture.
- This dashboard is a placeholder that will be fully implemented as we migrate
- the remaining Azure Functions to server actions.
-
-
- User ID: {session.user.id}
-
-
- Email: {session.user.email}
-
-
-
-
+
+
);
}
diff --git a/apps/web/app/dream-book/page.tsx b/apps/web/app/dream-book/page.tsx
new file mode 100644
index 0000000..5fec2b3
--- /dev/null
+++ b/apps/web/app/dream-book/page.tsx
@@ -0,0 +1,35 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Dream Book page
+ * Manage dreams, goals, and year vision
+ */
+export default async function DreamBookPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Dream Book
;
+ }
+
+ return (
+
+
+
+
+
+
+ My Dreams
+ + Add Dream
+
+
Your dreams will appear here
+
+
+
+ );
+}
diff --git a/apps/web/app/dream-connect/page.tsx b/apps/web/app/dream-connect/page.tsx
new file mode 100644
index 0000000..12d2787
--- /dev/null
+++ b/apps/web/app/dream-connect/page.tsx
@@ -0,0 +1,48 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Dream Connect page
+ * Network with others who share similar goals and interests
+ */
+export default async function DreamConnectPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Dream Connect
;
+ }
+
+ return (
+
+
+
+
+ Connection Filters
+
+
+ Filter by category:
+
+ All
+
+
+
+
+
+
+ Suggested Connections
+
+
Connection suggestions will appear here
+
+
+
+
+ Recent Connections
+
+
Your recent connections will appear here
+
+
+
+ );
+}
diff --git a/apps/web/app/dream-team/page.tsx b/apps/web/app/dream-team/page.tsx
new file mode 100644
index 0000000..1a6b8ca
--- /dev/null
+++ b/apps/web/app/dream-team/page.tsx
@@ -0,0 +1,61 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Dream Team page
+ * Team collaboration, meetings, and progress tracking
+ */
+export default async function DreamTeamPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Dream Team
;
+ }
+
+ return (
+
+
+
+
+
+
+ Team Members
+
+
Team members will appear here
+
+
+
+
+ Meeting Schedule
+
+
Next meeting: Not scheduled
+
Schedule Meeting
+
+
+
+
+ Meeting Attendance
+
+
Attendance records will appear here
+
+
+
+
+ Recently Completed Dreams
+
+
Team achievements will appear here
+
+
+
+ );
+}
diff --git a/apps/web/app/health/page.tsx b/apps/web/app/health/page.tsx
new file mode 100644
index 0000000..ec2fbcd
--- /dev/null
+++ b/apps/web/app/health/page.tsx
@@ -0,0 +1,54 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Health Check page
+ * System diagnostics and monitoring
+ */
+export default async function HealthCheckPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Health Check
;
+ }
+
+ return (
+
+
+
+
+ System Status
+
+
+
Database
+
Status: Unknown
+
+
+
API
+
Status: Unknown
+
+
+
Storage
+
Status: Unknown
+
+
+
+
+
+ Recent Errors
+
+
Error log will appear here
+
+
+
+
+ Performance Metrics
+
+
Performance data will appear here
+
+
+
+ );
+}
diff --git a/apps/web/app/labs/adaptive-cards/page.tsx b/apps/web/app/labs/adaptive-cards/page.tsx
new file mode 100644
index 0000000..b37b2aa
--- /dev/null
+++ b/apps/web/app/labs/adaptive-cards/page.tsx
@@ -0,0 +1,45 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Adaptive Cards Lab page
+ * Experimental features and testing
+ */
+export default async function AdaptiveCardsLabPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Labs
;
+ }
+
+ return (
+
+
+
+
+ Card Templates
+
+
Adaptive card templates will appear here
+
+
+
+
+ Testing Area
+
+ Test Card 1
+ Test Card 2
+ Test Card 3
+
+
+
+
+ Preview
+
+
Card preview will appear here
+
+
+
+ );
+}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 4a608dd..0b58b57 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import "./globals.css";
+import { AppProviders } from "@/lib/contexts/AppProviders";
export const metadata: Metadata = {
title: "Dreamspace - Turn Dreams into Reality",
@@ -14,7 +15,9 @@ export default function RootLayout({
return (
- {children}
+
+ {children}
+
);
diff --git a/apps/web/app/people/page.tsx b/apps/web/app/people/page.tsx
new file mode 100644
index 0000000..44576ae
--- /dev/null
+++ b/apps/web/app/people/page.tsx
@@ -0,0 +1,52 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * People Dashboard page
+ * Admin view for managing coaches and users
+ */
+export default async function PeoplePage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access People Dashboard
;
+ }
+
+ return (
+
+
+
+
+ Coaches
+ Users
+
+
+
+ Coaches
+
+
Coach list will appear here
+
+
+
+
+ Team Metrics
+
+
+
+
+
Completed This Week
+
0
+
+
+
+
+ );
+}
diff --git a/apps/web/app/scorecard/page.tsx b/apps/web/app/scorecard/page.tsx
new file mode 100644
index 0000000..e1eccf6
--- /dev/null
+++ b/apps/web/app/scorecard/page.tsx
@@ -0,0 +1,54 @@
+import { auth } from '@/lib/auth';
+
+/**
+ * Scorecard page
+ * Track activity points and progress history
+ */
+export default async function ScorecardPage() {
+ const session = await auth();
+
+ if (!session?.user) {
+ return
Please log in to access Scorecard
;
+ }
+
+ return (
+
+
+
+
+ Summary
+
+
+
Total Score
+
0 points
+
+
+
+
This Month
+
0 points
+
+
+
+
+
+ Activity History
+
+
Your activity history will appear here
+
+
+
+
+ Year Breakdown
+
+
Yearly trends will appear here
+
+
+
+ );
+}
diff --git a/apps/web/components/dashboard/DashboardDreamCard.tsx b/apps/web/components/dashboard/DashboardDreamCard.tsx
new file mode 100644
index 0000000..44ffac4
--- /dev/null
+++ b/apps/web/components/dashboard/DashboardDreamCard.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { useDreams } from '@/lib/contexts';
+
+/**
+ * Dashboard Dream Card
+ * Displays dream overview on dashboard
+ */
+export function DashboardDreamCard() {
+ const { dreams } = useDreams();
+
+ return (
+
+
My Dreams
+
+ {dreams.length === 0 ? (
+
No dreams yet. Start building your dream book!
+ ) : (
+
+ {dreams.map((dream) => (
+
+ {dream.title}
+ {dream.category}
+
+
Progress: {dream.progress}%
+
+
+
+ ))}
+
+ )}
+
+
View All Dreams →
+
+ );
+}
diff --git a/apps/web/components/dashboard/DashboardHeader.tsx b/apps/web/components/dashboard/DashboardHeader.tsx
new file mode 100644
index 0000000..cdb4e22
--- /dev/null
+++ b/apps/web/components/dashboard/DashboardHeader.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import { useUser } from '@/lib/contexts';
+
+/**
+ * Dashboard Header
+ * Displays user greeting and quick stats
+ */
+export function DashboardHeader() {
+ const { currentUser } = useUser();
+
+ return (
+
+ Welcome back, {currentUser?.displayName || currentUser?.name || 'Dreamer'}!
+ Track your dreams and achieve your goals
+ {currentUser && (
+
+
+ Score:
+ {currentUser.score || 0}
+
+
+ Dreams:
+ {currentUser.dreamsCount || 0}
+
+
+ Connections:
+ {currentUser.connectsCount || 0}
+
+
+ )}
+
+ );
+}
diff --git a/apps/web/components/dashboard/WeekGoalsWidget.tsx b/apps/web/components/dashboard/WeekGoalsWidget.tsx
new file mode 100644
index 0000000..03d30ca
--- /dev/null
+++ b/apps/web/components/dashboard/WeekGoalsWidget.tsx
@@ -0,0 +1,38 @@
+'use client';
+
+import { useGoals } from '@/lib/contexts';
+
+/**
+ * Week Goals Widget
+ * Displays and manages goals for the current week
+ */
+export function WeekGoalsWidget() {
+ const { weeklyGoals, toggleWeeklyGoal } = useGoals();
+
+ return (
+
+
This Week's Goals
+
+ {weeklyGoals.length === 0 ? (
+
No goals for this week. Add a goal to get started!
+ ) : (
+
+ )}
+
+
+ Add Goal
+
+ );
+}
diff --git a/apps/web/components/dashboard/index.ts b/apps/web/components/dashboard/index.ts
new file mode 100644
index 0000000..a9b0b06
--- /dev/null
+++ b/apps/web/components/dashboard/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Dashboard component exports
+ */
+
+export * from './WeekGoalsWidget';
+export * from './DashboardDreamCard';
+export * from './DashboardHeader';
diff --git a/apps/web/components/shared/Navigation.tsx b/apps/web/components/shared/Navigation.tsx
new file mode 100644
index 0000000..893dd02
--- /dev/null
+++ b/apps/web/components/shared/Navigation.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+/**
+ * Main navigation component
+ * Displays primary navigation links
+ */
+export function Navigation() {
+ const pathname = usePathname();
+
+ const links = [
+ { href: '/dashboard', label: 'Dashboard' },
+ { href: '/dream-book', label: 'Dream Book' },
+ { href: '/dream-connect', label: 'Dream Connect' },
+ { href: '/scorecard', label: 'Scorecard' },
+ { href: '/dream-team', label: 'Dream Team' },
+ { href: '/people', label: 'People' },
+ { href: '/build-overview', label: 'Build Overview' },
+ { href: '/health', label: 'Health' },
+ ];
+
+ return (
+
+
+ {links.map((link) => {
+ const isActive = pathname === link.href;
+ return (
+
+
+ {link.label}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/components/shared/index.ts b/apps/web/components/shared/index.ts
new file mode 100644
index 0000000..aebde32
--- /dev/null
+++ b/apps/web/components/shared/index.ts
@@ -0,0 +1,5 @@
+/**
+ * Shared component exports
+ */
+
+export * from './Navigation';
diff --git a/apps/web/lib/contexts/AppProviders.tsx b/apps/web/lib/contexts/AppProviders.tsx
new file mode 100644
index 0000000..624e0b5
--- /dev/null
+++ b/apps/web/lib/contexts/AppProviders.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import React, { ReactNode } from 'react';
+import {
+ DreamProvider,
+ GoalProvider,
+ UserProvider,
+ ConnectProvider,
+ TeamProvider,
+ ScoringProvider,
+} from './';
+
+/**
+ * Root app providers
+ * Wraps all context providers for the application
+ */
+export function AppProviders({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/contexts/ConnectContext.tsx b/apps/web/lib/contexts/ConnectContext.tsx
new file mode 100644
index 0000000..55ff5a1
--- /dev/null
+++ b/apps/web/lib/contexts/ConnectContext.tsx
@@ -0,0 +1,92 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * Connect data type
+ */
+export type Connect = {
+ id: string;
+ userId: string;
+ dreamId?: string;
+ withWhom: string;
+ when?: string;
+ notes?: string;
+ status?: 'pending' | 'completed';
+ agenda?: string;
+ proposedWeeks?: string[];
+ schedulingMethod?: string;
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+/**
+ * Connect context state
+ */
+type ConnectContextState = {
+ connects: Connect[];
+ isLoading: boolean;
+ setConnects: (connects: Connect[]) => void;
+ addConnect: (connect: Connect) => void;
+ updateConnect: (id: string, updates: Partial
) => void;
+ deleteConnect: (id: string) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const ConnectContext = createContext(undefined);
+
+/**
+ * Connect context provider
+ * Manages dream connect/networking state
+ */
+export function ConnectProvider({ children }: { children: ReactNode }) {
+ const [connects, setConnects] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const addConnect = (connect: Connect) => {
+ setConnects((prev) => [...prev, connect]);
+ };
+
+ const updateConnect = (id: string, updates: Partial) => {
+ setConnects((prev) =>
+ prev.map((connect) =>
+ connect.id === id ? { ...connect, ...updates } : connect
+ )
+ );
+ };
+
+ const deleteConnect = (id: string) => {
+ setConnects((prev) => prev.filter((connect) => connect.id !== id));
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use connect context
+ */
+export function useConnects() {
+ const context = useContext(ConnectContext);
+ if (context === undefined) {
+ throw new Error('useConnects must be used within a ConnectProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/DreamContext.tsx b/apps/web/lib/contexts/DreamContext.tsx
new file mode 100644
index 0000000..630b791
--- /dev/null
+++ b/apps/web/lib/contexts/DreamContext.tsx
@@ -0,0 +1,137 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * Dream data type
+ */
+export type Dream = {
+ id: string;
+ title: string;
+ category: string;
+ description?: string;
+ progress: number;
+ image?: string;
+ goals?: Goal[];
+ notes?: Note[];
+ coachNotes?: CoachNote[];
+ history?: HistoryEntry[];
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export type Goal = {
+ id: string;
+ dreamId: string;
+ title: string;
+ description?: string;
+ type: 'consistency' | 'deadline';
+ recurrence?: 'weekly' | 'once';
+ targetWeeks?: number;
+ targetDate?: string;
+ startDate?: string;
+ weekId?: string;
+ active: boolean;
+ completed: boolean;
+ completedAt?: string;
+};
+
+export type Note = {
+ id: string;
+ text: string;
+ createdAt: string;
+};
+
+export type CoachNote = {
+ id: string;
+ text: string;
+ sender: string;
+ createdAt: string;
+};
+
+export type HistoryEntry = {
+ id: string;
+ action: string;
+ details: string;
+ timestamp: string;
+};
+
+/**
+ * Dream context state
+ */
+type DreamContextState = {
+ dreams: Dream[];
+ yearVision: string;
+ isLoading: boolean;
+ setDreams: (dreams: Dream[]) => void;
+ setYearVision: (vision: string) => void;
+ addDream: (dream: Dream) => void;
+ updateDream: (id: string, updates: Partial) => void;
+ deleteDream: (id: string) => void;
+ reorderDreams: (dreams: Dream[]) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const DreamContext = createContext(undefined);
+
+/**
+ * Dream context provider
+ * Manages dream book state (dreams, year vision)
+ */
+export function DreamProvider({ children }: { children: ReactNode }) {
+ const [dreams, setDreams] = useState([]);
+ const [yearVision, setYearVision] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const addDream = (dream: Dream) => {
+ setDreams((prev) => [...prev, dream]);
+ };
+
+ const updateDream = (id: string, updates: Partial) => {
+ setDreams((prev) =>
+ prev.map((dream) => (dream.id === id ? { ...dream, ...updates } : dream))
+ );
+ };
+
+ const deleteDream = (id: string) => {
+ setDreams((prev) => prev.filter((dream) => dream.id !== id));
+ };
+
+ const reorderDreams = (newDreams: Dream[]) => {
+ setDreams(newDreams);
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use dream context
+ */
+export function useDreams() {
+ const context = useContext(DreamContext);
+ if (context === undefined) {
+ throw new Error('useDreams must be used within a DreamProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/GoalContext.tsx b/apps/web/lib/contexts/GoalContext.tsx
new file mode 100644
index 0000000..cd80323
--- /dev/null
+++ b/apps/web/lib/contexts/GoalContext.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * Weekly goal data type
+ */
+export type WeeklyGoal = {
+ id: string;
+ title: string;
+ dreamId?: string;
+ goalId?: string;
+ completed: boolean;
+ completedAt?: string;
+ weekId: string;
+ recurrence: 'weekly' | 'once';
+ active: boolean;
+ weekLog?: Record;
+};
+
+/**
+ * Goal context state
+ */
+type GoalContextState = {
+ weeklyGoals: WeeklyGoal[];
+ isLoading: boolean;
+ setWeeklyGoals: (goals: WeeklyGoal[]) => void;
+ addWeeklyGoal: (goal: WeeklyGoal) => void;
+ updateWeeklyGoal: (id: string, updates: Partial) => void;
+ deleteWeeklyGoal: (id: string) => void;
+ toggleWeeklyGoal: (id: string) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const GoalContext = createContext(undefined);
+
+/**
+ * Goal context provider
+ * Manages weekly goals state
+ */
+export function GoalProvider({ children }: { children: ReactNode }) {
+ const [weeklyGoals, setWeeklyGoals] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const addWeeklyGoal = (goal: WeeklyGoal) => {
+ setWeeklyGoals((prev) => [...prev, goal]);
+ };
+
+ const updateWeeklyGoal = (id: string, updates: Partial) => {
+ setWeeklyGoals((prev) =>
+ prev.map((goal) => (goal.id === id ? { ...goal, ...updates } : goal))
+ );
+ };
+
+ const deleteWeeklyGoal = (id: string) => {
+ setWeeklyGoals((prev) => prev.filter((goal) => goal.id !== id));
+ };
+
+ const toggleWeeklyGoal = (id: string) => {
+ setWeeklyGoals((prev) =>
+ prev.map((goal) =>
+ goal.id === id
+ ? {
+ ...goal,
+ completed: !goal.completed,
+ completedAt: !goal.completed ? new Date().toISOString() : undefined,
+ }
+ : goal
+ )
+ );
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use goal context
+ */
+export function useGoals() {
+ const context = useContext(GoalContext);
+ if (context === undefined) {
+ throw new Error('useGoals must be used within a GoalProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/ScoringContext.tsx b/apps/web/lib/contexts/ScoringContext.tsx
new file mode 100644
index 0000000..117cc61
--- /dev/null
+++ b/apps/web/lib/contexts/ScoringContext.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * Scoring entry data type
+ */
+export type ScoringEntry = {
+ id: string;
+ date: string;
+ score: number;
+ activity: string;
+ points: number;
+ category?: string;
+};
+
+/**
+ * Scoring context state
+ */
+type ScoringContextState = {
+ scoringHistory: ScoringEntry[];
+ allYearsScoring: ScoringEntry[];
+ allTimeScore: number;
+ isLoading: boolean;
+ setScoringHistory: (history: ScoringEntry[]) => void;
+ setAllYearsScoring: (scoring: ScoringEntry[]) => void;
+ setAllTimeScore: (score: number) => void;
+ addScoringEntry: (entry: ScoringEntry) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const ScoringContext = createContext(undefined);
+
+/**
+ * Scoring context provider
+ * Manages scorecard and activity scoring state
+ */
+export function ScoringProvider({ children }: { children: ReactNode }) {
+ const [scoringHistory, setScoringHistory] = useState([]);
+ const [allYearsScoring, setAllYearsScoring] = useState([]);
+ const [allTimeScore, setAllTimeScore] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const addScoringEntry = (entry: ScoringEntry) => {
+ setScoringHistory((prev) => [entry, ...prev]);
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use scoring context
+ */
+export function useScoring() {
+ const context = useContext(ScoringContext);
+ if (context === undefined) {
+ throw new Error('useScoring must be used within a ScoringProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/TeamContext.tsx b/apps/web/lib/contexts/TeamContext.tsx
new file mode 100644
index 0000000..76842ba
--- /dev/null
+++ b/apps/web/lib/contexts/TeamContext.tsx
@@ -0,0 +1,116 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * Team member data type
+ */
+export type TeamMember = {
+ id: string;
+ name: string;
+ email?: string;
+ avatar?: string;
+ role?: string;
+ dreamsCompleted?: number;
+ meetingAttendance?: number;
+};
+
+/**
+ * Meeting data type
+ */
+export type Meeting = {
+ id: string;
+ date: string;
+ attendees: string[];
+ notes?: string;
+ teamId?: string;
+};
+
+/**
+ * Team info data type
+ */
+export type TeamInfo = {
+ id: string;
+ name: string;
+ mission?: string;
+ coachId?: string;
+ members?: TeamMember[];
+};
+
+/**
+ * Team context state
+ */
+type TeamContextState = {
+ teamInfo: TeamInfo | null;
+ meetings: Meeting[];
+ isLoading: boolean;
+ setTeamInfo: (info: TeamInfo | null) => void;
+ updateTeamInfo: (updates: Partial) => void;
+ setMeetings: (meetings: Meeting[]) => void;
+ addMeeting: (meeting: Meeting) => void;
+ updateMeeting: (id: string, updates: Partial) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const TeamContext = createContext(undefined);
+
+/**
+ * Team context provider
+ * Manages team collaboration state
+ */
+export function TeamProvider({ children }: { children: ReactNode }) {
+ const [teamInfo, setTeamInfo] = useState(null);
+ const [meetings, setMeetings] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const updateTeamInfo = (updates: Partial) => {
+ if (teamInfo) {
+ setTeamInfo({ ...teamInfo, ...updates });
+ }
+ };
+
+ const addMeeting = (meeting: Meeting) => {
+ setMeetings((prev) => [...prev, meeting]);
+ };
+
+ const updateMeeting = (id: string, updates: Partial) => {
+ setMeetings((prev) =>
+ prev.map((meeting) =>
+ meeting.id === id ? { ...meeting, ...updates } : meeting
+ )
+ );
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use team context
+ */
+export function useTeam() {
+ const context = useContext(TeamContext);
+ if (context === undefined) {
+ throw new Error('useTeam must be used within a TeamProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/UserContext.tsx b/apps/web/lib/contexts/UserContext.tsx
new file mode 100644
index 0000000..b25e504
--- /dev/null
+++ b/apps/web/lib/contexts/UserContext.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import React, { createContext, useContext, useState, ReactNode } from 'react';
+
+/**
+ * User data type
+ */
+export type User = {
+ id: string;
+ email: string;
+ name: string;
+ displayName?: string;
+ office?: string;
+ avatar?: string;
+ jobTitle?: string;
+ department?: string;
+ role?: 'user' | 'coach' | 'admin';
+ isCoach?: boolean;
+ score?: number;
+ dreamsCount?: number;
+ connectsCount?: number;
+ dreamCategories?: string[];
+};
+
+/**
+ * User context state
+ */
+type UserContextState = {
+ currentUser: User | null;
+ isLoading: boolean;
+ setCurrentUser: (user: User | null) => void;
+ updateUserProfile: (updates: Partial) => void;
+ updateScore: (score: number) => void;
+ setLoading: (loading: boolean) => void;
+};
+
+const UserContext = createContext(undefined);
+
+/**
+ * User context provider
+ * Manages current user profile state
+ */
+export function UserProvider({ children }: { children: ReactNode }) {
+ const [currentUser, setCurrentUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const updateUserProfile = (updates: Partial) => {
+ if (currentUser) {
+ setCurrentUser({ ...currentUser, ...updates });
+ }
+ };
+
+ const updateScore = (score: number) => {
+ if (currentUser) {
+ setCurrentUser({ ...currentUser, score });
+ }
+ };
+
+ const setLoading = (loading: boolean) => {
+ setIsLoading(loading);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use user context
+ */
+export function useUser() {
+ const context = useContext(UserContext);
+ if (context === undefined) {
+ throw new Error('useUser must be used within a UserProvider');
+ }
+ return context;
+}
diff --git a/apps/web/lib/contexts/index.ts b/apps/web/lib/contexts/index.ts
new file mode 100644
index 0000000..b829bc0
--- /dev/null
+++ b/apps/web/lib/contexts/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Context exports
+ * Barrel export for all context providers and hooks
+ */
+
+export * from './DreamContext';
+export * from './GoalContext';
+export * from './UserContext';
+export * from './ConnectContext';
+export * from './TeamContext';
+export * from './ScoringContext';
From b893cbb51fa012f72f757a42f563e65d0178cf30 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Feb 2026 20:31:06 +0000
Subject: [PATCH 03/12] Add comprehensive frontend migration documentation
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
---
COMPONENT_STUBBING_GUIDE.md | 606 ++++++++++++++++++++++++++++++++++
FRONTEND_MIGRATION_PLAN.md | 523 +++++++++++++++++++++++++++++
FRONTEND_MIGRATION_SUMMARY.md | 294 +++++++++++++++++
3 files changed, 1423 insertions(+)
create mode 100644 COMPONENT_STUBBING_GUIDE.md
create mode 100644 FRONTEND_MIGRATION_PLAN.md
create mode 100644 FRONTEND_MIGRATION_SUMMARY.md
diff --git a/COMPONENT_STUBBING_GUIDE.md b/COMPONENT_STUBBING_GUIDE.md
new file mode 100644
index 0000000..990f10a
--- /dev/null
+++ b/COMPONENT_STUBBING_GUIDE.md
@@ -0,0 +1,606 @@
+# Component Stubbing Guide
+
+**Purpose**: Guidelines for creating functional, unstyled component stubs during frontend migration
+
+## Principles
+
+1. **Functional First**: Components must work without styling
+2. **Native Elements**: Use standard HTML elements (no UI libraries)
+3. **Type-Safe**: All props and state properly typed
+4. **Context-Aware**: Use appropriate contexts for state
+5. **Server-First**: Prefer Server Components unless interactivity needed
+
+## Component Template
+
+### Client Component Template
+
+```tsx
+'use client';
+
+import { useDreams } from '@/lib/contexts'; // or other contexts
+
+/**
+ * ComponentName
+ * Brief description of what this component does
+ */
+export function ComponentName() {
+ // 1. Get data from context
+ const { dreams, addDream } = useDreams();
+
+ // 2. Local state (if needed)
+ const [isOpen, setIsOpen] = useState(false);
+
+ // 3. Event handlers
+ const handleClick = () => {
+ // Handle user interaction
+ };
+
+ // 4. Render with native HTML
+ return (
+
+
Section Title
+
Action
+
+ {/* Conditional rendering */}
+ {dreams.length === 0 ? (
+
No data yet
+ ) : (
+
+ {dreams.map(dream => (
+ {dream.title}
+ ))}
+
+ )}
+
+ );
+}
+```
+
+### Server Component Template
+
+```tsx
+import { auth } from '@/lib/auth';
+import { getDreams } from '@/services/dreams';
+
+/**
+ * ComponentName
+ * Brief description of what this component does
+ */
+export async function ComponentName() {
+ // 1. Auth check (if needed)
+ const session = await auth();
+
+ // 2. Fetch data
+ const result = await getDreams(session.user.id);
+
+ if (result.failed) {
+ return Error: {result.errors._errors.join(', ')}
;
+ }
+
+ const { dreams } = result;
+
+ // 3. Render
+ return (
+
+
Dreams
+ {dreams.map(dream => (
+
+
{dream.title}
+
{dream.description}
+
+ ))}
+
+ );
+}
+```
+
+## Component Patterns
+
+### 1. List Display
+
+```tsx
+export function ItemList() {
+ const { items } = useItems();
+
+ return (
+
+
Items
+ {items.length === 0 ? (
+
No items found
+ ) : (
+
+ {items.map(item => (
+
+ {item.name}
+ handleEdit(item.id)}>Edit
+
+ ))}
+
+ )}
+
Add Item
+
+ );
+}
+```
+
+### 2. Form Input
+
+```tsx
+export function ItemForm() {
+ const { addItem } = useItems();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+
+ const item = {
+ id: crypto.randomUUID(),
+ name: formData.get('name') as string,
+ description: formData.get('description') as string,
+ };
+
+ addItem(item);
+ e.currentTarget.reset();
+ };
+
+ return (
+
+ );
+}
+```
+
+### 3. Modal/Dialog
+
+```tsx
+export function ItemModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}>
+
+
+ {/* Modal content */}
+
+
+
+
+ );
+}
+```
+
+### 4. Tabs
+
+```tsx
+export function TabView() {
+ const [activeTab, setActiveTab] = useState<'overview' | 'details'>('overview');
+
+ return (
+
+
+ setActiveTab('overview')}>Overview
+ setActiveTab('details')}>Details
+
+
+
+ {activeTab === 'overview' && }
+ {activeTab === 'details' && }
+
+
+ );
+}
+```
+
+### 5. Filters/Search
+
+```tsx
+export function FilterableList() {
+ const { items } = useItems();
+ const [filter, setFilter] = useState('');
+
+ const filteredItems = items.filter(item =>
+ item.name.toLowerCase().includes(filter.toLowerCase())
+ );
+
+ return (
+
+
setFilter(e.target.value)}
+ />
+
+ {filteredItems.map(item => (
+ {item.name}
+ ))}
+
+
+ );
+}
+```
+
+### 6. Stats/Metrics
+
+```tsx
+export function StatsCard({ label, value }: { label: string; value: number | string }) {
+ return (
+
+ );
+}
+
+export function StatsGrid() {
+ const { currentUser } = useUser();
+
+ return (
+
+
+
+
+
+ );
+}
+```
+
+### 7. Progress Indicator
+
+```tsx
+export function ProgressBar({ value, max = 100 }: { value: number; max?: number }) {
+ const percentage = (value / max) * 100;
+
+ return (
+
+
+
{percentage.toFixed(0)}%
+
+ );
+}
+```
+
+## Context Usage Patterns
+
+### Single Context
+
+```tsx
+'use client';
+
+import { useDreams } from '@/lib/contexts';
+
+export function DreamList() {
+ const { dreams, deleteDream } = useDreams();
+ // ... component logic
+}
+```
+
+### Multiple Contexts
+
+```tsx
+'use client';
+
+import { useDreams, useGoals, useUser } from '@/lib/contexts';
+
+export function Dashboard() {
+ const { currentUser } = useUser();
+ const { dreams } = useDreams();
+ const { weeklyGoals } = useGoals();
+
+ // ... component logic
+}
+```
+
+### Optional Context (for shared components)
+
+```tsx
+'use client';
+
+import { useDreams } from '@/lib/contexts';
+
+export function SharedComponent({ standalone = false }: { standalone?: boolean }) {
+ // Only use context if not standalone
+ const context = standalone ? null : useDreams();
+
+ // ... component logic
+}
+```
+
+## HTML Elements Reference
+
+### Common Elements
+
+- `` - Container
+- `
` - Thematic grouping
+- `` - Self-contained content
+- `` - Introductory content
+- `` - Footer content
+- `` - Navigation links
+- `` - Main content
+
+### Text Elements
+
+- `` to `` - Headings
+- ` ` - Paragraph
+- `` - Inline text
+- `` - Bold text
+- `` - Italic text
+- `` - Code snippet
+- `` - Preformatted text
+
+### Form Elements
+
+- ` ` - Form container
+- ` ` - Input field
+ - `type="text"` - Text input
+ - `type="email"` - Email input
+ - `type="password"` - Password input
+ - `type="number"` - Number input
+ - `type="date"` - Date picker
+ - `type="checkbox"` - Checkbox
+ - `type="radio"` - Radio button
+ - `type="search"` - Search input
+- `` - Multi-line text
+- `` - Dropdown
+- `` - Dropdown option
+- `` - Button
+ - `type="submit"` - Submit button
+ - `type="button"` - Regular button
+ - `type="reset"` - Reset button
+- `` - Input label
+- `` - Group of inputs
+- `` - Fieldset title
+
+### List Elements
+
+- `` - Unordered list
+- `` - Ordered list
+- `` - List item
+- `` - Description list
+- `` - Description term
+- ` ` - Description details
+
+### Table Elements
+
+- `` - Table
+- `` - Table header
+- ` ` - Table body
+- ` ` - Table footer
+- `` - Table row
+- `` - Table header cell
+- ` ` - Table data cell
+
+### Interactive Elements
+
+- `` - Button
+- `` - Link (use ` ` from next/link)
+- `` - Collapsible content
+- `` - Details summary
+
+### Media Elements
+
+- ` ` - Image (use `` from next/image)
+- `` - Video
+- `` - Audio
+
+## Common Gotchas
+
+### 1. Client Component Required For:
+- `useState`, `useEffect`, hooks
+- Event handlers (`onClick`, `onChange`)
+- Context consumers
+- Browser APIs
+
+### 2. Server Component Advantages:
+- Direct database access
+- No client JS bundle
+- SEO-friendly
+- Automatic code splitting
+
+### 3. Form Handling
+```tsx
+// ✅ Good: Uncontrolled form
+
+
+ Save
+
+
+// ❌ Avoid: Controlled form (unnecessary state)
+const [title, setTitle] = useState('');
+ setTitle(e.target.value)} />
+```
+
+### 4. Key Props
+```tsx
+// ✅ Good: Stable unique key
+{items.map(item => (
+ {item.name}
+))}
+
+// ❌ Bad: Index as key (can cause bugs)
+{items.map((item, i) => (
+ {item.name}
+))}
+```
+
+### 5. Event Handlers
+```tsx
+// ✅ Good: Arrow function or defined function
+ handleClick(id)}>Click
+Click
+
+// ❌ Bad: Calling function immediately
+Click
+```
+
+## File Organization
+
+```
+components/
+└── feature-name/
+ ├── FeatureList.tsx # Main list view
+ ├── FeatureCard.tsx # Individual item
+ ├── FeatureForm.tsx # Create/edit form
+ ├── FeatureModal.tsx # Detail modal
+ ├── FeatureFilters.tsx # Filtering UI
+ └── index.ts # Barrel export
+```
+
+### Barrel Export (index.ts)
+
+```ts
+/**
+ * Feature component exports
+ */
+
+export * from './FeatureList';
+export * from './FeatureCard';
+export * from './FeatureForm';
+export * from './FeatureModal';
+export * from './FeatureFilters';
+```
+
+## Testing Considerations
+
+Even without tests yet, write components to be testable:
+
+```tsx
+// ✅ Good: Props clearly defined, easy to test
+type Props = {
+ items: Item[];
+ onSelect: (id: string) => void;
+};
+
+export function ItemList({ items, onSelect }: Props) {
+ // ...
+}
+
+// ❌ Bad: Hard to test, unclear dependencies
+export function ItemList() {
+ const items = useItems(); // Hidden dependency
+ // ...
+}
+```
+
+## Quick Checklist
+
+For each component, ensure:
+
+- [ ] JSDoc comment explaining purpose
+- [ ] Type-safe props and state
+- [ ] Uses appropriate context(s)
+- [ ] Native HTML elements only
+- [ ] Event handlers properly typed
+- [ ] Conditional rendering for empty states
+- [ ] Loading/error states (where applicable)
+- [ ] Exported from barrel index.ts
+- [ ] 'use client' directive (if needed)
+
+## Example: Complete Component
+
+```tsx
+'use client';
+
+import { useState } from 'react';
+import { useDreams } from '@/lib/contexts';
+
+/**
+ * Dream Form
+ * Creates or edits a dream with title, category, and description
+ */
+export function DreamForm({ dreamId, onClose }: { dreamId?: string; onClose: () => void }) {
+ const { dreams, addDream, updateDream } = useDreams();
+ const existingDream = dreamId ? dreams.find(d => d.id === dreamId) : null;
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+
+ const dream = {
+ id: dreamId || crypto.randomUUID(),
+ title: formData.get('title') as string,
+ category: formData.get('category') as string,
+ description: formData.get('description') as string,
+ progress: existingDream?.progress || 0,
+ };
+
+ if (dreamId) {
+ updateDream(dreamId, dream);
+ } else {
+ addDream(dream);
+ }
+
+ onClose();
+ };
+
+ return (
+
+ {dreamId ? 'Edit Dream' : 'Create Dream'}
+
+
+
+ Title:
+
+
+
+
+
+
+ Category:
+
+ Select category
+ Personal
+ Career
+ Health
+
+
+
+
+
+
+ Description:
+
+
+
+
+
+ Save
+ Cancel
+
+
+ );
+}
+```
+
+---
+
+**Remember**: The goal is functionality, not beauty. Keep it simple, use native elements, and ensure everything works before styling.
diff --git a/FRONTEND_MIGRATION_PLAN.md b/FRONTEND_MIGRATION_PLAN.md
new file mode 100644
index 0000000..32d5243
--- /dev/null
+++ b/FRONTEND_MIGRATION_PLAN.md
@@ -0,0 +1,523 @@
+# Frontend Migration Plan: Legacy React → NextJS App Router
+
+**Status**: In Progress
+**Date**: February 2026
+**Goal**: Migrate legacy `/src` React app to modern NextJS App Router in `apps/web`
+
+## Executive Summary
+
+This migration refactors the Dreamspace application from a client-heavy Redux-based React app to a progressive, isomorphic NextJS application using Server Components and React Contexts. The migration maintains all existing functionality while improving performance, maintainability, and deployment readiness.
+
+## Migration Strategy
+
+### 1. State Management Transformation
+
+**From**: Centralized Redux store with 30+ actions
+**To**: Domain-specific React Contexts (6 contexts)
+
+| Context | Responsibility | State |
+|---------|---------------|-------|
+| `UserContext` | Current user profile | user data, score, stats |
+| `DreamContext` | Dream book management | dreams[], yearVision |
+| `GoalContext` | Weekly goal tracking | weeklyGoals[] |
+| `ConnectContext` | Network connections | connects[] |
+| `TeamContext` | Team collaboration | teamInfo, meetings[] |
+| `ScoringContext` | Activity scoring | scoringHistory[], allTimeScore |
+
+**Benefits**:
+- Reduced cognitive load (separate concerns)
+- Better tree-shaking (only load needed contexts)
+- Easier testing (isolated state)
+- Clearer data flow
+
+### 2. Page Structure Mapping
+
+All 9 legacy pages mapped to NextJS App Router:
+
+| Legacy Route | NextJS Route | Status |
+|-------------|--------------|--------|
+| `/` (Dashboard) | `/dashboard/page.tsx` | ✅ Stubbed |
+| `/dream-book` | `/dream-book/page.tsx` | ✅ Stubbed |
+| `/dream-connect` | `/dream-connect/page.tsx` | ✅ Stubbed |
+| `/scorecard` | `/scorecard/page.tsx` | ✅ Stubbed |
+| `/dream-team` | `/dream-team/page.tsx` | ✅ Stubbed |
+| `/people` | `/people/page.tsx` | ✅ Stubbed |
+| `/build-overview` | `/build-overview/page.tsx` | ✅ Stubbed |
+| `/health` | `/health/page.tsx` | ✅ Stubbed |
+| `/labs/adaptive-cards` | `/labs/adaptive-cards/page.tsx` | ✅ Stubbed |
+
+### 3. Component Architecture
+
+**Legacy Pattern**: Three-layer architecture
+1. Layout (orchestrator)
+2. Custom Hook (business logic)
+3. Presentational Components
+
+**New Pattern**: Server-first architecture
+1. Server Component Page (data fetching)
+2. Client Components (interactivity)
+3. Server Actions (mutations)
+
+## Implementation Details
+
+### Phase 1: Contexts ✅
+
+Created 6 domain-specific contexts in `apps/web/lib/contexts/`:
+
+```typescript
+// Example: DreamContext
+type Dream = {
+ id: string;
+ title: string;
+ category: string;
+ progress: number;
+ goals?: Goal[];
+ // ... more fields
+};
+
+type DreamContextState = {
+ dreams: Dream[];
+ yearVision: string;
+ addDream: (dream: Dream) => void;
+ updateDream: (id: string, updates: Partial) => void;
+ deleteDream: (id: string) => void;
+ // ... more actions
+};
+```
+
+All contexts follow the same pattern:
+- Type-safe state and actions
+- useState for local state
+- Clear mutation methods
+- Loading state management
+
+### Phase 2: Pages ✅
+
+All pages created with:
+- Server Component by default
+- Auth check using `auth()`
+- Functional structure (no styling)
+- Native HTML elements
+
+Example structure:
+```tsx
+export default async function DashboardPage() {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return Unauthorized
;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+```
+
+### Phase 3: Components - In Progress
+
+**Completed**:
+- Dashboard components (3): Header, WeekGoalsWidget, DreamCard
+- Shared components (1): Navigation
+
+**Remaining** (80+ components to stub):
+
+#### Dashboard Components ✅
+- [x] DashboardHeader
+- [x] WeekGoalsWidget
+- [x] DashboardDreamCard
+
+#### Dream Book Components
+- [ ] DreamForm (create/edit)
+- [ ] DreamGrid (display all)
+- [ ] DreamCard (individual card)
+- [ ] YearVisionCard (vision statement)
+- [ ] DreamRadarChart (analytics)
+- [ ] ImageUploadSection
+- [ ] FirstGoalSetup
+
+#### Dream Tracker Components
+- [ ] DreamTrackerModal (detail view)
+- [ ] DreamHeader
+- [ ] DreamTabNavigation
+- [ ] OverviewTab
+- [ ] GoalsTab
+- [ ] NotesTab
+- [ ] CoachNotesTab
+- [ ] HistoryTab
+
+#### Dream Connect Components
+- [ ] ConnectionFilters
+- [ ] SuggestedConnections
+- [ ] RecentConnects
+- [ ] ConnectionCard
+- [ ] ConnectDetailModal
+- [ ] ConnectRequestModal
+
+#### Scorecard Components
+- [ ] SummaryView
+- [ ] HistoryView
+- [ ] YearBreakdownView
+- [ ] ActivityCard
+- [ ] ProgressCard
+
+#### Dream Team Components
+- [ ] TeamMembersSection
+- [ ] TeamMemberCard
+- [ ] TeamMemberModal
+- [ ] MeetingScheduleCard
+- [ ] MeetingAttendanceCard
+- [ ] RecentlyCompletedDreamsCard
+- [ ] MeetingHistoryModal
+
+#### People Dashboard Components
+- [ ] PeopleHeader
+- [ ] CoachesPanel
+- [ ] UsersPanel
+- [ ] CoachList
+- [ ] CoachCard
+- [ ] TeamMemberRow
+- [ ] TeamMetrics
+- [ ] CoachDetailModal
+- [ ] ReplaceCoachModal
+
+#### Shared Components
+- [x] Navigation
+- [ ] Modal (base)
+- [ ] ConfirmModal
+- [ ] LoadingSpinner
+- [ ] ErrorBoundary
+- [ ] Toast notifications
+
+## Data Flow Pattern
+
+### Legacy (Client-Heavy)
+```
+User Action → Hook → Dispatch → Reducer → Context →
+Component Rerender → Service → API → Update State →
+useAutosave → localStorage
+```
+
+### New (Progressive/Isomorphic)
+```
+Server Component → Server Action (data fetch) → Render →
+Client Component (interactive) → User Action →
+Server Action (mutation) → revalidatePath → Rerender
+```
+
+## Key Differences
+
+| Aspect | Legacy | New |
+|--------|--------|-----|
+| Rendering | Client-side only | Server Components + Client Components |
+| State | Redux reducer | React Contexts (client) |
+| Data Fetching | useEffect + services | Server Actions + fetch |
+| Forms | Controlled + React state | Uncontrolled + FormData |
+| Validation | Zod schemas | Zod schemas (same) |
+| Routing | React Router | NextJS App Router |
+| Code Splitting | Lazy imports | Automatic (Next) |
+
+## Migration Phases
+
+### Phase 1: Foundation ✅ (Complete)
+- Create context architecture
+- Stub all page routes
+- Basic component structure
+
+### Phase 2: Component Migration (Current)
+- Stub all components with native HTML
+- Ensure functional without styling
+- Wire contexts to components
+
+### Phase 3: Integration
+- Connect server actions to UI
+- Implement data fetching
+- Add form handling
+- Wire up navigation
+
+### Phase 4: Testing & Validation
+- Test all user flows
+- Verify data persistence
+- Check auth flows
+- Performance testing
+
+### Phase 5: Styling (Future)
+- Apply Tailwind classes
+- Match legacy UI
+- Responsive design
+- Accessibility
+
+## Technical Considerations
+
+### Context Usage Guidelines
+
+**When to use Context:**
+- User profile data (needed across app)
+- Dreams/goals (modified from multiple places)
+- UI state that needs persistence (modals, filters)
+
+**When NOT to use Context:**
+- One-off data fetching → use Server Components
+- Local component state → use useState
+- Form state → use uncontrolled components
+
+### Server vs Client Components
+
+**Use Server Components for:**
+- Data fetching
+- Static content
+- SEO-critical content
+- Markdown rendering
+
+**Use Client Components for:**
+- Interactivity (clicks, forms)
+- Browser APIs (localStorage)
+- React hooks (useState, useEffect)
+- Context consumers
+
+### Server Actions Pattern
+
+All mutations follow this pattern:
+```typescript
+'use server';
+
+export async function saveDream(formData: FormData) {
+ // 1. Auth check
+ const session = await auth();
+ if (!session?.user?.id) {
+ return { failed: true, errors: { _errors: ['Unauthorized'] } };
+ }
+
+ // 2. Validation
+ const data = schema.parse(formData);
+
+ // 3. Business logic
+ const db = getDatabaseClient();
+ const dream = await db.dreams.save({ ...data, userId: session.user.id });
+
+ // 4. Revalidate
+ revalidatePath('/dream-book');
+
+ // 5. Return result
+ return { failed: false, dream };
+}
+```
+
+## File Structure
+
+```
+apps/web/
+├── app/ # NextJS App Router
+│ ├── dashboard/
+│ │ └── page.tsx # Server Component
+│ ├── dream-book/
+│ │ └── page.tsx
+│ ├── dream-connect/
+│ │ └── page.tsx
+│ ├── scorecard/
+│ │ └── page.tsx
+│ ├── dream-team/
+│ │ └── page.tsx
+│ ├── people/
+│ │ └── page.tsx
+│ ├── build-overview/
+│ │ └── page.tsx
+│ ├── health/
+│ │ └── page.tsx
+│ ├── labs/
+│ │ └── adaptive-cards/
+│ │ └── page.tsx
+│ └── layout.tsx # Root layout with providers
+│
+├── components/ # Client Components
+│ ├── dashboard/
+│ │ ├── WeekGoalsWidget.tsx
+│ │ ├── DashboardDreamCard.tsx
+│ │ ├── DashboardHeader.tsx
+│ │ └── index.ts
+│ ├── dream-book/ # TBD
+│ ├── dream-connect/ # TBD
+│ ├── scorecard/ # TBD
+│ ├── dream-team/ # TBD
+│ ├── people/ # TBD
+│ └── shared/
+│ ├── Navigation.tsx
+│ └── index.ts
+│
+├── lib/
+│ ├── contexts/ # React Contexts
+│ │ ├── DreamContext.tsx
+│ │ ├── GoalContext.tsx
+│ │ ├── UserContext.tsx
+│ │ ├── ConnectContext.tsx
+│ │ ├── TeamContext.tsx
+│ │ ├── ScoringContext.tsx
+│ │ ├── AppProviders.tsx
+│ │ └── index.ts
+│ ├── actions/ # Server Action utilities
+│ └── auth.ts # Auth configuration
+│
+└── services/ # Server Actions (already exist)
+ ├── dreams/
+ ├── users/
+ ├── weeks/
+ ├── teams/
+ ├── scoring/
+ └── ...
+```
+
+## Dependencies
+
+### Keep Same
+- `next-auth` - Authentication
+- `zod` - Validation
+- `zod-form-data` - Form parsing
+- `tailwindcss` - Styling (future)
+
+### Remove (Legacy Only)
+- `react-router-dom` - Replaced by NextJS router
+- Redux (if any) - Replaced by contexts
+
+### Add (Already Added)
+- `lucide-react` - Icons
+- `canvas-confetti` - Celebrations
+
+## Testing Strategy
+
+### Unit Tests
+- Context actions (add, update, delete)
+- Server actions (auth, validation, errors)
+- Utility functions
+
+### Integration Tests
+- Page rendering
+- Form submission
+- Navigation
+- Data persistence
+
+### E2E Tests
+- Complete user flows
+- Auth flows
+- Error handling
+
+## Performance Considerations
+
+1. **Server Components** - Reduce client JS bundle
+2. **Parallel Data Fetching** - Use Promise.all for multiple fetches
+3. **Streaming** - Use Suspense for progressive loading
+4. **Code Splitting** - Automatic with NextJS
+5. **Image Optimization** - Use next/image component
+
+## Security Considerations
+
+1. **Auth Checks** - Every server action checks session
+2. **Input Validation** - Zod schemas on all inputs
+3. **CSRF Protection** - Built into Next.js
+4. **SQL Injection** - Using parameterized queries
+5. **XSS Protection** - React escapes by default
+
+## Deployment
+
+### Development
+```bash
+pnpm dev # Start dev server
+pnpm type-check # TypeScript validation
+pnpm lint # ESLint
+```
+
+### Production
+```bash
+pnpm build # Build for production
+pnpm start # Start production server
+```
+
+### Environments
+- **Local**: Development server
+- **Staging**: Azure App Service (staging slot)
+- **Production**: Azure App Service or Vercel
+
+## Rollout Plan
+
+### Option 1: Big Bang
+- Complete all migration work
+- Switch traffic from legacy to new
+- Monitor for issues
+
+### Option 2: Gradual (Recommended)
+1. Deploy new routes alongside legacy
+2. Redirect one route at a time
+3. Monitor metrics and errors
+4. Roll back if needed
+5. Gradually migrate all routes
+
+## Success Metrics
+
+- [ ] All 9 pages functional
+- [ ] All user flows working
+- [ ] Performance ≥ legacy app
+- [ ] Zero critical bugs
+- [ ] Type-safe (no TypeScript errors)
+- [ ] Accessible (WCAG AA)
+- [ ] Tests passing (>80% coverage)
+
+## Next Steps
+
+1. **Immediate**: Complete component stubbing
+ - Dream Book components
+ - Dream Connect components
+ - Scorecard components
+ - Dream Team components
+ - People components
+
+2. **Short-term**: Wire up data
+ - Connect contexts to server actions
+ - Implement form handlers
+ - Add loading states
+ - Error handling
+
+3. **Medium-term**: Polish
+ - Add Tailwind styling
+ - Responsive design
+ - Accessibility improvements
+ - Performance optimization
+
+4. **Long-term**: Enhancements
+ - Real-time updates (websockets)
+ - Offline support (service workers)
+ - PWA features
+ - Advanced analytics
+
+## Questions & Decisions
+
+### Q: Should we use React Query for server state?
+**A**: No, use Server Components + Server Actions. Simpler mental model, less client JS.
+
+### Q: How to handle real-time updates?
+**A**: Phase 1: polling with revalidatePath. Phase 2: websockets/SSE if needed.
+
+### Q: What about the existing Azure Functions?
+**A**: They're being migrated to Server Actions (see `MIGRATION_GUIDE.md`).
+
+### Q: Should we keep localStorage sync?
+**A**: No, Server Components fetch fresh data on each request. Use database as source of truth.
+
+### Q: How to handle optimistic updates?
+**A**: Use `useOptimistic` hook for instant feedback, revalidate after server action completes.
+
+## References
+
+- [NextJS App Router Docs](https://nextjs.org/docs/app)
+- [Server Actions Guide](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
+- [Netsurit Architecture Standards](/.claude/skills/software-architecture/references/)
+- [Migration Guide](./MIGRATION_GUIDE.md)
+- [Monorepo README](./README.monorepo.md)
+
+---
+
+**Last Updated**: February 4, 2026
+**Author**: GitHub Copilot Agent
+**Reviewers**: TBD
diff --git a/FRONTEND_MIGRATION_SUMMARY.md b/FRONTEND_MIGRATION_SUMMARY.md
new file mode 100644
index 0000000..ed14d6d
--- /dev/null
+++ b/FRONTEND_MIGRATION_SUMMARY.md
@@ -0,0 +1,294 @@
+# Frontend Migration Analysis Summary
+
+## Overview
+
+Analysis complete for migrating the Dreamspace frontend from legacy React app (`/src`) to NextJS App Router (`apps/web`).
+
+## Current State
+
+### Legacy App (`/src`)
+- **9 main pages** with React Router
+- **80+ components** organized by feature
+- **32 custom hooks** for business logic
+- **21 services** for API calls
+- **Redux-like reducer** with AppContext (30+ actions)
+- Client-heavy rendering with lazy loading
+
+### New App (`apps/web`)
+- **Server actions** already created (70+ actions across domains)
+- **NextJS App Router** structure in place
+- **Authentication** via NextAuth (configured)
+- **Database layer** with @dreamspace/database package
+- **TypeScript** throughout
+
+## Migration Approach
+
+### 1. State Management: Redux → React Contexts
+
+Created 6 domain-specific contexts to replace centralized Redux:
+
+| Context | Purpose | State Size |
+|---------|---------|-----------|
+| `UserContext` | Current user profile | ~10 fields |
+| `DreamContext` | Dreams & year vision | dreams[] + vision |
+| `GoalContext` | Weekly goals | weeklyGoals[] |
+| `ConnectContext` | Networking | connects[] |
+| `TeamContext` | Team collaboration | teamInfo + meetings[] |
+| `ScoringContext` | Activity tracking | scoringHistory[] + scores |
+
+**Benefit**: Reduced coupling, better tree-shaking, clearer boundaries
+
+### 2. Page Structure: Client → Server Components
+
+All 9 pages created as NextJS routes:
+
+```
+✅ /dashboard/page.tsx (with 3 client components)
+✅ /dream-book/page.tsx (stub)
+✅ /dream-connect/page.tsx (stub)
+✅ /scorecard/page.tsx (stub)
+✅ /dream-team/page.tsx (stub)
+✅ /people/page.tsx (stub)
+✅ /build-overview/page.tsx (stub)
+✅ /health/page.tsx (stub)
+✅ /labs/adaptive-cards/page.tsx (stub)
+```
+
+**Benefit**: Server-first rendering, SEO-friendly, faster initial load
+
+### 3. Components: Progressive Migration
+
+**Completed** (4 components):
+- DashboardHeader (user greeting + stats)
+- WeekGoalsWidget (goal checklist)
+- DashboardDreamCard (dream overview)
+- Navigation (shared nav)
+
+**Remaining** (~80 components):
+- Dream Book: DreamForm, DreamGrid, YearVisionCard, etc.
+- Dream Connect: ConnectionCard, Filters, Modals
+- Scorecard: SummaryView, HistoryView, YearBreakdown
+- Dream Team: TeamMembers, Meetings, Stats
+- People: CoachList, UserList, Metrics
+- Shared: Modals, Forms, LoadingSpinner
+
+## Key Architectural Changes
+
+### Before (Legacy)
+```
+Client Rendering → Redux State → useEffect → API Call →
+Update State → localStorage Sync → Rerender
+```
+
+### After (New)
+```
+Server Component → Fetch Data → Render →
+Client Component (interactive) → Server Action →
+Revalidate → Rerender
+```
+
+## Benefits of New Architecture
+
+1. **Performance**
+ - Server Components = less client JS
+ - Automatic code splitting
+ - Parallel data fetching
+ - Streaming with Suspense
+
+2. **Maintainability**
+ - Domain-specific contexts (vs. global Redux)
+ - Collocated components by feature
+ - Type-safe throughout
+ - Clear server/client boundary
+
+3. **Developer Experience**
+ - Server Actions (no API routes needed)
+ - Built-in form handling
+ - Automatic revalidation
+ - Better error boundaries
+
+4. **Progressive Enhancement**
+ - Works without JavaScript (mostly)
+ - Native form validation
+ - Accessible by default
+
+## File Structure
+
+```
+apps/web/
+├── app/ # Pages (Server Components)
+│ ├── dashboard/
+│ ├── dream-book/
+│ ├── dream-connect/
+│ ├── scorecard/
+│ ├── dream-team/
+│ ├── people/
+│ ├── build-overview/
+│ ├── health/
+│ └── labs/
+├── components/ # UI Components (Client)
+│ ├── dashboard/ ✅ Started (3 components)
+│ ├── dream-book/ ⏳ TODO
+│ ├── dream-connect/ ⏳ TODO
+│ ├── scorecard/ ⏳ TODO
+│ ├── dream-team/ ⏳ TODO
+│ ├── people/ ⏳ TODO
+│ └── shared/ ✅ Started (1 component)
+├── lib/
+│ └── contexts/ ✅ Complete (6 contexts)
+└── services/ ✅ Exists (70+ server actions)
+```
+
+## Migration Status
+
+### ✅ Phase 1: Foundation (COMPLETE)
+- Context architecture
+- Page structure
+- Root providers
+- Basic components
+
+### 🔄 Phase 2: Components (IN PROGRESS)
+- Stub all components with native HTML
+- No styling (as required)
+- Functional only
+- Wire to contexts
+
+### ⏳ Phase 3: Integration (TODO)
+- Connect server actions
+- Form handling
+- Data fetching
+- Navigation
+
+### ⏳ Phase 4: Testing (TODO)
+- Unit tests
+- Integration tests
+- E2E tests
+
+### ⏳ Phase 5: Styling (FUTURE)
+- Apply Tailwind
+- Responsive design
+- Accessibility
+
+## Context API Examples
+
+### Using Contexts in Client Components
+
+```tsx
+'use client';
+
+import { useDreams, useGoals } from '@/lib/contexts';
+
+export function MyComponent() {
+ const { dreams, addDream } = useDreams();
+ const { weeklyGoals, toggleWeeklyGoal } = useGoals();
+
+ // Component logic...
+}
+```
+
+### Server Components Don't Use Context
+
+```tsx
+// Server Component
+import { auth } from '@/lib/auth';
+import { getUserDreams } from '@/services/dreams';
+
+export default async function DreamBookPage() {
+ const session = await auth();
+ const dreams = await getUserDreams(session.user.id);
+
+ // Pass data to client components as props
+ return ;
+}
+```
+
+## Data Flow Patterns
+
+### Reading Data
+```
+Server Component → fetch/server action →
+Client Component (via props) →
+Context (if needed for mutations)
+```
+
+### Mutating Data
+```
+Client Component → User Action →
+Server Action (validate + persist) →
+revalidatePath →
+Server Component re-fetches →
+Client Component receives new props
+```
+
+## Next Immediate Steps
+
+1. **Stub Dream Book Components** (~10 components)
+ - DreamForm, DreamGrid, DreamCard
+ - YearVisionCard, DreamRadarChart
+ - ImageUploadSection, FirstGoalSetup
+
+2. **Stub Dream Connect Components** (~6 components)
+ - ConnectionFilters, SuggestedConnections
+ - ConnectionCard, ConnectDetailModal
+ - ConnectRequestModal, RecentConnects
+
+3. **Stub Scorecard Components** (~5 components)
+ - SummaryView, HistoryView, YearBreakdownView
+ - ActivityCard, ProgressCard
+
+4. **Stub Dream Team Components** (~7 components)
+ - TeamMembersSection, TeamMemberCard
+ - MeetingScheduleCard, MeetingAttendanceCard
+ - TeamMemberModal, MeetingHistoryModal
+
+5. **Stub People Components** (~8 components)
+ - CoachesPanel, UsersPanel, CoachList
+ - CoachCard, TeamMemberRow, TeamMetrics
+ - CoachDetailModal, ReplaceCoachModal
+
+6. **Stub Shared Components** (~5 components)
+ - Modal, ConfirmModal, LoadingSpinner
+ - ErrorBoundary, Toast
+
+## Risks & Mitigations
+
+| Risk | Mitigation |
+|------|-----------|
+| Breaking existing functionality | Keep legacy app running during migration |
+| Performance regression | Use Server Components by default |
+| Type safety gaps | Strict TypeScript, no `any` types |
+| Context overuse | Only use for cross-cutting concerns |
+| Server action errors | Comprehensive error handling + logging |
+
+## Timeline Estimate
+
+- **Components Stubbing**: 2-3 days (80+ components)
+- **Integration**: 3-5 days (wire up data + actions)
+- **Testing**: 2-3 days (ensure all flows work)
+- **Styling**: 5-7 days (apply Tailwind, responsive)
+
+**Total**: ~15-20 days for complete migration
+
+## Success Criteria
+
+- [ ] All 9 pages render without errors
+- [ ] All user flows functional (even without styling)
+- [ ] No TypeScript errors in new code
+- [ ] Navigation works between all pages
+- [ ] Contexts integrate with server actions
+- [ ] Forms submit and validate correctly
+- [ ] Auth works on all protected routes
+- [ ] Data persists to database
+- [ ] Documentation updated
+
+## References
+
+- Full plan: `FRONTEND_MIGRATION_PLAN.md`
+- Architecture guide: `.claude/skills/software-architecture/`
+- Migration guide: `MIGRATION_GUIDE.md`
+- Monorepo setup: `README.monorepo.md`
+
+---
+
+**Status**: Phase 1 Complete, Phase 2 In Progress
+**Last Updated**: February 4, 2026
From 557d45077b5a5006f771ea339c704ae1db386bbe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Feb 2026 20:32:14 +0000
Subject: [PATCH 04/12] Add architecture comparison diagram
---
ARCHITECTURE_DIAGRAM.md | 327 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 327 insertions(+)
create mode 100644 ARCHITECTURE_DIAGRAM.md
diff --git a/ARCHITECTURE_DIAGRAM.md b/ARCHITECTURE_DIAGRAM.md
new file mode 100644
index 0000000..5c254f0
--- /dev/null
+++ b/ARCHITECTURE_DIAGRAM.md
@@ -0,0 +1,327 @@
+# Dreamspace Architecture: Legacy vs New
+
+## Legacy Architecture (src/)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Browser (Client Only) │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────┐ ┌─────────────────────┐ │
+│ │ React Router │────────▶│ Page Components │ │
+│ └──────────────┘ └──────────┬──────────┘ │
+│ │ │
+│ ┌──────────▼──────────┐ │
+│ │ AppContext │ │
+│ │ (Redux-like) │ │
+│ │ │ │
+│ │ • Dreams │ │
+│ │ • Goals │ │
+│ │ • User │ │
+│ │ • Connects │ │
+│ │ • Scoring │ │
+│ │ • Team │ │
+│ │ │ │
+│ │ 30+ Actions │ │
+│ │ 315 LOC Reducer │ │
+│ └──────────┬──────────┘ │
+│ │ │
+│ ┌──────────▼──────────┐ │
+│ │ Custom Hooks │ │
+│ │ (32 hooks) │ │
+│ └──────────┬──────────┘ │
+│ │ │
+│ ┌──────────▼──────────┐ │
+│ │ Services Layer │ │
+│ │ (21 services) │ │
+│ └──────────┬──────────┘ │
+└──────────────────────────────────────┼──────────────────────┘
+ │
+ │ HTTP/REST
+ │
+ ┌──────────▼──────────┐
+ │ Azure Functions │
+ │ (50+ functions) │
+ └──────────┬──────────┘
+ │
+ ┌──────────▼──────────┐
+ │ Cosmos DB │
+ └─────────────────────┘
+```
+
+## New Architecture (apps/web/)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Server (NextJS) │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────┐ ┌─────────────────────┐ │
+│ │ App Router │────────▶│ Server Components │ │
+│ │ │ │ (Pages - RSC) │ │
+│ └──────────────┘ └──────────┬──────────┘ │
+│ │ │
+│ │ fetch data │
+│ │ │
+│ ┌──────────▼──────────┐ │
+│ │ Server Actions │ │
+│ │ (70+ actions) │ │
+│ │ │ │
+│ │ • users/ │ │
+│ │ • dreams/ │ │
+│ │ • weeks/ │ │
+│ │ • teams/ │ │
+│ │ • scoring/ │ │
+│ │ • connects/ │ │
+│ │ │ │
+│ └──────────┬──────────┘ │
+│ │ │
+│ ┌──────────▼──────────┐ │
+│ │ Database Client │ │
+│ │ (@dreamspace/db) │ │
+│ └──────────┬──────────┘ │
+└──────────────────────────────────────┼──────────────────────┘
+ │
+ ┌──────────▼──────────┐
+ │ Cosmos DB │
+ └─────────────────────┘
+
+┌─────────────────────────────────────────────────────────────┐
+│ Browser (Client) │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────────────────────────────────────────┐ │
+│ │ Client Components (Interactive) │ │
+│ │ │ │
+│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
+│ │ │ Dream │ │ Goal │ │ User │ │ │
+│ │ │ Context │ │ Context │ │ Context │ │ │
+│ │ └──────────┘ └──────────┘ └──────────┘ │ │
+│ │ │ │
+│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
+│ │ │ Connect │ │ Team │ │ Scoring │ │ │
+│ │ │ Context │ │ Context │ │ Context │ │ │
+│ │ └──────────┘ └──────────┘ └──────────┘ │ │
+│ │ │ │
+│ │ (Contexts only for UI state) │ │
+│ └──────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+## Key Differences
+
+| Aspect | Legacy | New |
+|--------|--------|-----|
+| **Rendering** | Client-side only | Server + Client |
+| **State** | Centralized Redux | Distributed Contexts |
+| **Data Flow** | Client → API → DB | Server → DB → Client |
+| **Routing** | React Router (client) | NextJS (server) |
+| **API Layer** | Azure Functions | Server Actions |
+| **Bundle Size** | Large (all client) | Small (server-first) |
+| **SEO** | Poor | Excellent |
+| **Performance** | Slower initial load | Faster initial load |
+| **Complexity** | 30+ actions, 1 reducer | 6 contexts, focused |
+
+## Data Flow Comparison
+
+### Legacy: Client-Heavy
+```
+User Action
+ ↓
+React Component
+ ↓
+Dispatch Action → Reducer → Context Update
+ ↓
+useEffect triggered
+ ↓
+Service Layer (client)
+ ↓
+HTTP Request → Azure Function
+ ↓
+Cosmos DB
+ ↓
+Response → State Update
+ ↓
+localStorage sync
+ ↓
+Component Re-render
+```
+
+### New: Server-First
+```
+Page Load
+ ↓
+Server Component
+ ↓
+Server Action (direct DB access)
+ ↓
+Cosmos DB
+ ↓
+Render HTML
+ ↓
+Send to Client
+
+User Interaction
+ ↓
+Client Component
+ ↓
+Server Action (mutation)
+ ↓
+Cosmos DB
+ ↓
+revalidatePath
+ ↓
+Server Component re-fetches
+ ↓
+Render updated HTML
+```
+
+## Component Architecture
+
+### Legacy Three-Layer Pattern
+```
+┌─────────────────────────────────┐
+│ DashboardLayout.jsx │ ◀─── Orchestrator
+│ - State management │
+│ - Side effects │
+│ - Event handlers │
+└──────────┬──────────────────────┘
+ │
+ ┌──────▼─────────┐
+ │ useDashboard │ ◀─── Business Logic Hook
+ │ Hook │
+ │ - Data fetch │
+ │ - Calculations │
+ └──────┬─────────┘
+ │
+ ┌──────▼─────────┐
+ │ Presentational │ ◀─── Pure UI Components
+ │ Components │
+ │ - No state │
+ │ - Props only │
+ └────────────────┘
+```
+
+### New Server-First Pattern
+```
+┌─────────────────────────────────┐
+│ page.tsx (Server) │ ◀─── Server Component
+│ - Auth check │
+│ - Data fetching │
+│ - Render │
+└──────────┬──────────────────────┘
+ │
+ │ Pass data as props
+ │
+ ┌──────▼─────────┐
+ │ Client │ ◀─── Client Component
+ │ Components │
+ │ - Interactive │
+ │ - Use contexts │
+ │ - Event handlers│
+ └──────┬─────────┘
+ │
+ │ Mutations
+ │
+ ┌──────▼─────────┐
+ │ Server Actions │ ◀─── Server-side logic
+ │ - Validation │
+ │ - DB updates │
+ │ - Revalidation │
+ └────────────────┘
+```
+
+## File Organization
+
+### Legacy
+```
+src/
+├── pages/
+│ ├── Dashboard.jsx (thin wrapper)
+│ └── dashboard/
+│ ├── DashboardLayout.jsx (orchestrator)
+│ ├── DashboardHeader.jsx
+│ ├── WeekGoalsWidget.jsx
+│ └── week-goals/
+│ ├── index.js
+│ ├── GoalItem.jsx
+│ └── AddGoalForm.jsx
+├── context/
+│ ├── AppContext.jsx (global state)
+│ └── AuthContext.jsx
+├── state/
+│ ├── appReducer.js (315 LOC)
+│ └── actionTypes.js
+├── hooks/
+│ ├── useDashboard.js
+│ ├── useDreamActions.js
+│ └── ... (32 hooks)
+└── services/
+ ├── apiClient.js
+ ├── dreamService.js
+ └── ... (21 services)
+```
+
+### New
+```
+apps/web/
+├── app/
+│ ├── dashboard/
+│ │ └── page.tsx (server component)
+│ ├── dream-book/
+│ │ └── page.tsx
+│ └── layout.tsx (with providers)
+├── components/
+│ ├── dashboard/
+│ │ ├── DashboardHeader.tsx
+│ │ ├── WeekGoalsWidget.tsx
+│ │ └── DashboardDreamCard.tsx
+│ └── shared/
+│ └── Navigation.tsx
+├── lib/
+│ └── contexts/ (6 focused contexts)
+│ ├── DreamContext.tsx
+│ ├── GoalContext.tsx
+│ ├── UserContext.tsx
+│ ├── ConnectContext.tsx
+│ ├── TeamContext.tsx
+│ ├── ScoringContext.tsx
+│ └── AppProviders.tsx
+└── services/ (70+ server actions)
+ ├── dreams/
+ ├── users/
+ ├── weeks/
+ └── ...
+```
+
+## Migration Benefits
+
+1. **Performance**
+ - ⚡ Faster initial page load (server rendering)
+ - 📦 Smaller client bundle (no Redux, less JS)
+ - 🎯 Automatic code splitting (NextJS)
+ - 🔄 Parallel data fetching
+
+2. **Developer Experience**
+ - 📝 Type-safe throughout (TypeScript)
+ - 🎨 Clearer separation of concerns
+ - 🧪 Easier to test (isolated contexts)
+ - 📚 Better documentation
+
+3. **Maintainability**
+ - 🔍 Easier to understand (smaller contexts)
+ - 🔧 Easier to modify (focused responsibilities)
+ - 🚀 Easier to deploy (NextJS built-in)
+ - 📊 Better error tracking
+
+4. **User Experience**
+ - 🌐 SEO-friendly (server rendering)
+ - ♿ More accessible (progressive enhancement)
+ - 📱 Better mobile performance
+ - 🔒 More secure (server-side auth)
+
+---
+
+**Migration Status**: Phase 1 Complete ✅
+**Next Phase**: Component Stubbing (80+ components)
From f4a81c400c91ba107731f7f4d5bac0cc7a450a8e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Feb 2026 20:47:42 +0000
Subject: [PATCH 05/12] Refactor contexts to use Immutable.js and integrate
with server actions
- Add immutable.js for complex state management
- Refactor all 6 contexts (Dream, Goal, User, Connect, Team, Scoring) to use Records and Lists
- Implement optimistic updates with rollback on server action failures
- Add loadData methods to initialize contexts from server
- Integrate contexts with existing server actions (saveDreams, saveCurrentWeek, etc.)
- Convert immutable data back to plain objects in hooks for easy component consumption
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
---
apps/web/lib/contexts/ConnectContext.tsx | 150 ++++++++++++++---
apps/web/lib/contexts/DreamContext.tsx | 206 ++++++++++++++++++++---
apps/web/lib/contexts/GoalContext.tsx | 201 ++++++++++++++++++----
apps/web/lib/contexts/ScoringContext.tsx | 137 ++++++++++++---
apps/web/lib/contexts/TeamContext.tsx | 170 +++++++++++++++----
apps/web/lib/contexts/UserContext.tsx | 109 +++++++++---
apps/web/package.json | 2 +
pnpm-lock.yaml | 37 +++-
8 files changed, 851 insertions(+), 161 deletions(-)
diff --git a/apps/web/lib/contexts/ConnectContext.tsx b/apps/web/lib/contexts/ConnectContext.tsx
index 55ff5a1..784593d 100644
--- a/apps/web/lib/contexts/ConnectContext.tsx
+++ b/apps/web/lib/contexts/ConnectContext.tsx
@@ -1,6 +1,8 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { List, Record } from 'immutable';
+import { saveConnect, deleteConnect } from '@/services/connects';
/**
* Connect data type
@@ -10,6 +12,7 @@ export type Connect = {
userId: string;
dreamId?: string;
withWhom: string;
+ withWhomId: string;
when?: string;
notes?: string;
status?: 'pending' | 'completed';
@@ -20,47 +23,140 @@ export type Connect = {
updatedAt?: string;
};
+/**
+ * Immutable Connect record
+ */
+const ConnectRecord = Record({
+ id: '',
+ userId: '',
+ dreamId: '',
+ withWhom: '',
+ withWhomId: '',
+ when: '',
+ notes: '',
+ status: 'pending',
+ agenda: '',
+ proposedWeeks: [],
+ schedulingMethod: '',
+ createdAt: '',
+ updatedAt: '',
+});
+
/**
* Connect context state
*/
type ConnectContextState = {
- connects: Connect[];
+ connects: List>;
isLoading: boolean;
- setConnects: (connects: Connect[]) => void;
- addConnect: (connect: Connect) => void;
- updateConnect: (id: string, updates: Partial) => void;
- deleteConnect: (id: string) => void;
- setLoading: (loading: boolean) => void;
+ userId: string | null;
+ loadConnects: (userId: string, initialConnects?: Connect[]) => void;
+ addConnect: (connect: Connect) => Promise;
+ updateConnect: (id: string, updates: Partial) => Promise;
+ deleteConnect: (id: string) => Promise;
};
const ConnectContext = createContext(undefined);
/**
* Connect context provider
- * Manages dream connect/networking state
+ * Manages dream connect/networking state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function ConnectProvider({ children }: { children: ReactNode }) {
- const [connects, setConnects] = useState([]);
+ const [connects, setConnects] = useState>>(List());
const [isLoading, setIsLoading] = useState(false);
+ const [userId, setUserId] = useState(null);
- const addConnect = (connect: Connect) => {
- setConnects((prev) => [...prev, connect]);
+ /**
+ * Load connects for a user
+ */
+ const loadConnects = (userId: string, initialConnects?: Connect[]) => {
+ setUserId(userId);
+ if (initialConnects) {
+ const connectRecords = List(initialConnects.map(c => ConnectRecord(c)));
+ setConnects(connectRecords);
+ }
};
- const updateConnect = (id: string, updates: Partial) => {
- setConnects((prev) =>
- prev.map((connect) =>
- connect.id === id ? { ...connect, ...updates } : connect
- )
- );
+ /**
+ * Add a connect with optimistic update and server persistence
+ */
+ const addConnect = async (connect: Connect) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const connectRecord = ConnectRecord(connect);
+ const previousConnects = connects;
+ setConnects(connects.push(connectRecord));
+
+ try {
+ const result = await saveConnect({ userId, connectData: connect });
+ if (result.failed) {
+ // Rollback on failure
+ setConnects(previousConnects);
+ console.error('Failed to save connect:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setConnects(previousConnects);
+ console.error('Error saving connect:', error);
+ }
};
- const deleteConnect = (id: string) => {
- setConnects((prev) => prev.filter((connect) => connect.id !== id));
+ /**
+ * Update a connect with optimistic update and server persistence
+ */
+ const updateConnect = async (id: string, updates: Partial) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousConnects = connects;
+ const index = connects.findIndex(c => c.get('id') === id);
+ if (index === -1) return;
+
+ const updatedConnect = connects.get(index)!.merge(updates);
+ setConnects(connects.set(index, updatedConnect));
+
+ try {
+ const connectData = updatedConnect.toObject();
+ const result = await saveConnect({ userId, connectData });
+ if (result.failed) {
+ // Rollback on failure
+ setConnects(previousConnects);
+ console.error('Failed to update connect:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setConnects(previousConnects);
+ console.error('Error updating connect:', error);
+ }
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Delete a connect with optimistic update and server persistence
+ */
+ const deleteConnectAction = async (id: string) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousConnects = connects;
+ const index = connects.findIndex(c => c.get('id') === id);
+ if (index === -1) return;
+
+ setConnects(connects.delete(index));
+
+ try {
+ const result = await deleteConnect({ userId, connectId: id });
+ if (result.failed) {
+ // Rollback on failure
+ setConnects(previousConnects);
+ console.error('Failed to delete connect:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setConnects(previousConnects);
+ console.error('Error deleting connect:', error);
+ }
};
return (
@@ -68,11 +164,11 @@ export function ConnectProvider({ children }: { children: ReactNode }) {
value={{
connects,
isLoading,
- setConnects,
+ userId,
+ loadConnects,
addConnect,
updateConnect,
- deleteConnect,
- setLoading,
+ deleteConnect: deleteConnectAction,
}}
>
{children}
@@ -82,11 +178,17 @@ export function ConnectProvider({ children }: { children: ReactNode }) {
/**
* Hook to use connect context
+ * Returns connects as plain JavaScript array for easier consumption
*/
export function useConnects() {
const context = useContext(ConnectContext);
if (context === undefined) {
throw new Error('useConnects must be used within a ConnectProvider');
}
- return context;
+
+ // Convert immutable List to plain array for component consumption
+ return {
+ ...context,
+ connects: context.connects.toArray().map(c => c.toObject()),
+ };
}
diff --git a/apps/web/lib/contexts/DreamContext.tsx b/apps/web/lib/contexts/DreamContext.tsx
index 630b791..0d0909c 100644
--- a/apps/web/lib/contexts/DreamContext.tsx
+++ b/apps/web/lib/contexts/DreamContext.tsx
@@ -1,6 +1,8 @@
'use client';
-import React, { createContext, useContext, useState, ReactNode } from 'react';
+import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import { List, Record } from 'immutable';
+import { saveDreams, saveYearVision } from '@/services/dreams';
/**
* Dream data type
@@ -56,53 +58,197 @@ export type HistoryEntry = {
timestamp: string;
};
+/**
+ * Immutable Dream record
+ */
+const DreamRecord = Record({
+ id: '',
+ title: '',
+ category: '',
+ description: '',
+ progress: 0,
+ image: '',
+ goals: [],
+ notes: [],
+ coachNotes: [],
+ history: [],
+ createdAt: '',
+ updatedAt: '',
+});
+
/**
* Dream context state
*/
type DreamContextState = {
- dreams: Dream[];
+ dreams: List>;
yearVision: string;
isLoading: boolean;
- setDreams: (dreams: Dream[]) => void;
- setYearVision: (vision: string) => void;
- addDream: (dream: Dream) => void;
- updateDream: (id: string, updates: Partial) => void;
- deleteDream: (id: string) => void;
- reorderDreams: (dreams: Dream[]) => void;
- setLoading: (loading: boolean) => void;
+ userId: string | null;
+ loadDreams: (userId: string, initialDreams?: Dream[], initialVision?: string) => void;
+ setYearVision: (vision: string) => Promise;
+ addDream: (dream: Dream) => Promise;
+ updateDream: (id: string, updates: Partial) => Promise;
+ deleteDream: (id: string) => Promise;
+ reorderDreams: (dreams: Dream[]) => Promise;
};
const DreamContext = createContext(undefined);
/**
* Dream context provider
- * Manages dream book state (dreams, year vision)
+ * Manages dream book state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function DreamProvider({ children }: { children: ReactNode }) {
- const [dreams, setDreams] = useState([]);
- const [yearVision, setYearVision] = useState('');
+ const [dreams, setDreams] = useState>>(List());
+ const [yearVision, setYearVisionState] = useState('');
const [isLoading, setIsLoading] = useState(false);
+ const [userId, setUserId] = useState(null);
- const addDream = (dream: Dream) => {
- setDreams((prev) => [...prev, dream]);
+ /**
+ * Load dreams data for a user
+ */
+ const loadDreams = (userId: string, initialDreams?: Dream[], initialVision?: string) => {
+ setUserId(userId);
+ if (initialDreams) {
+ const dreamRecords = List(initialDreams.map(d => DreamRecord(d)));
+ setDreams(dreamRecords);
+ }
+ if (initialVision !== undefined) {
+ setYearVisionState(initialVision);
+ }
};
- const updateDream = (id: string, updates: Partial) => {
- setDreams((prev) =>
- prev.map((dream) => (dream.id === id ? { ...dream, ...updates } : dream))
- );
+ /**
+ * Update year vision with optimistic update and server persistence
+ */
+ const setYearVision = async (vision: string) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousVision = yearVision;
+ setYearVisionState(vision);
+
+ try {
+ const result = await saveYearVision({ userId, yearVision: vision });
+ if (result.failed) {
+ // Rollback on failure
+ setYearVisionState(previousVision);
+ console.error('Failed to save year vision:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setYearVisionState(previousVision);
+ console.error('Error saving year vision:', error);
+ }
};
- const deleteDream = (id: string) => {
- setDreams((prev) => prev.filter((dream) => dream.id !== id));
+ /**
+ * Add a dream with optimistic update and server persistence
+ */
+ const addDream = async (dream: Dream) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const dreamRecord = DreamRecord(dream);
+ const previousDreams = dreams;
+ setDreams(dreams.push(dreamRecord));
+
+ try {
+ const dreamsArray = dreams.push(dreamRecord).toArray().map(d => d.toObject());
+ const result = await saveDreams({ userId, dreams: dreamsArray });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error('Failed to save dream:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error('Error saving dream:', error);
+ }
};
- const reorderDreams = (newDreams: Dream[]) => {
- setDreams(newDreams);
+ /**
+ * Update a dream with optimistic update and server persistence
+ */
+ const updateDream = async (id: string, updates: Partial) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousDreams = dreams;
+ const index = dreams.findIndex(d => d.get('id') === id);
+ if (index === -1) return;
+
+ const updatedDream = dreams.get(index)!.merge(updates);
+ setDreams(dreams.set(index, updatedDream));
+
+ try {
+ const dreamsArray = dreams.set(index, updatedDream).toArray().map(d => d.toObject());
+ const result = await saveDreams({ userId, dreams: dreamsArray });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error('Failed to update dream:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error('Error updating dream:', error);
+ }
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Delete a dream with optimistic update and server persistence
+ */
+ const deleteDream = async (id: string) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousDreams = dreams;
+ const index = dreams.findIndex(d => d.get('id') === id);
+ if (index === -1) return;
+
+ setDreams(dreams.delete(index));
+
+ try {
+ const dreamsArray = dreams.delete(index).toArray().map(d => d.toObject());
+ const result = await saveDreams({ userId, dreams: dreamsArray });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error('Failed to delete dream:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error('Error deleting dream:', error);
+ }
+ };
+
+ /**
+ * Reorder dreams with optimistic update and server persistence
+ */
+ const reorderDreams = async (newDreams: Dream[]) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousDreams = dreams;
+ const dreamRecords = List(newDreams.map(d => DreamRecord(d)));
+ setDreams(dreamRecords);
+
+ try {
+ const result = await saveDreams({ userId, dreams: newDreams });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error('Failed to reorder dreams:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error('Error reordering dreams:', error);
+ }
};
return (
@@ -111,13 +257,13 @@ export function DreamProvider({ children }: { children: ReactNode }) {
dreams,
yearVision,
isLoading,
- setDreams,
+ userId,
+ loadDreams,
setYearVision,
addDream,
updateDream,
deleteDream,
reorderDreams,
- setLoading,
}}
>
{children}
@@ -127,11 +273,17 @@ export function DreamProvider({ children }: { children: ReactNode }) {
/**
* Hook to use dream context
+ * Returns dreams as plain JavaScript array for easier consumption
*/
export function useDreams() {
const context = useContext(DreamContext);
if (context === undefined) {
throw new Error('useDreams must be used within a DreamProvider');
}
- return context;
+
+ // Convert immutable List to plain array for component consumption
+ return {
+ ...context,
+ dreams: context.dreams.toArray().map(d => d.toObject()),
+ };
}
diff --git a/apps/web/lib/contexts/GoalContext.tsx b/apps/web/lib/contexts/GoalContext.tsx
index cd80323..b468e2c 100644
--- a/apps/web/lib/contexts/GoalContext.tsx
+++ b/apps/web/lib/contexts/GoalContext.tsx
@@ -1,6 +1,8 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { List, Record } from 'immutable';
+import { saveCurrentWeek } from '@/services/weeks';
/**
* Weekly goal data type
@@ -18,60 +20,184 @@ export type WeeklyGoal = {
weekLog?: Record;
};
+/**
+ * Immutable WeeklyGoal record
+ */
+const WeeklyGoalRecord = Record({
+ id: '',
+ title: '',
+ dreamId: '',
+ goalId: '',
+ completed: false,
+ completedAt: '',
+ weekId: '',
+ recurrence: 'weekly',
+ active: true,
+ weekLog: {},
+});
+
/**
* Goal context state
*/
type GoalContextState = {
- weeklyGoals: WeeklyGoal[];
+ weeklyGoals: List>;
isLoading: boolean;
- setWeeklyGoals: (goals: WeeklyGoal[]) => void;
- addWeeklyGoal: (goal: WeeklyGoal) => void;
- updateWeeklyGoal: (id: string, updates: Partial) => void;
- deleteWeeklyGoal: (id: string) => void;
- toggleWeeklyGoal: (id: string) => void;
- setLoading: (loading: boolean) => void;
+ userId: string | null;
+ currentWeekId: string | null;
+ loadWeeklyGoals: (userId: string, weekId: string, initialGoals?: WeeklyGoal[]) => void;
+ addWeeklyGoal: (goal: WeeklyGoal) => Promise;
+ updateWeeklyGoal: (id: string, updates: Partial) => Promise;
+ deleteWeeklyGoal: (id: string) => Promise;
+ toggleWeeklyGoal: (id: string) => Promise;
};
const GoalContext = createContext(undefined);
/**
* Goal context provider
- * Manages weekly goals state
+ * Manages weekly goals state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function GoalProvider({ children }: { children: ReactNode }) {
- const [weeklyGoals, setWeeklyGoals] = useState([]);
+ const [weeklyGoals, setWeeklyGoals] = useState>>(List());
const [isLoading, setIsLoading] = useState(false);
+ const [userId, setUserId] = useState(null);
+ const [currentWeekId, setCurrentWeekId] = useState(null);
- const addWeeklyGoal = (goal: WeeklyGoal) => {
- setWeeklyGoals((prev) => [...prev, goal]);
+ /**
+ * Load weekly goals for a user and week
+ */
+ const loadWeeklyGoals = (userId: string, weekId: string, initialGoals?: WeeklyGoal[]) => {
+ setUserId(userId);
+ setCurrentWeekId(weekId);
+ if (initialGoals) {
+ const goalRecords = List(initialGoals.map(g => WeeklyGoalRecord(g)));
+ setWeeklyGoals(goalRecords);
+ }
};
- const updateWeeklyGoal = (id: string, updates: Partial) => {
- setWeeklyGoals((prev) =>
- prev.map((goal) => (goal.id === id ? { ...goal, ...updates } : goal))
- );
+ /**
+ * Save goals to server
+ */
+ const saveGoals = async (goals: List>) => {
+ if (!userId || !currentWeekId) return;
+
+ const goalsArray = goals.toArray().map(g => g.toObject());
+ const result = await saveCurrentWeek({ userId, weekId: currentWeekId, goals: goalsArray });
+
+ if (result.failed) {
+ console.error('Failed to save weekly goals:', result.errors);
+ return false;
+ }
+ return true;
};
- const deleteWeeklyGoal = (id: string) => {
- setWeeklyGoals((prev) => prev.filter((goal) => goal.id !== id));
+ /**
+ * Add a weekly goal with optimistic update and server persistence
+ */
+ const addWeeklyGoal = async (goal: WeeklyGoal) => {
+ if (!userId || !currentWeekId) return;
+
+ // Optimistic update
+ const goalRecord = WeeklyGoalRecord(goal);
+ const previousGoals = weeklyGoals;
+ setWeeklyGoals(weeklyGoals.push(goalRecord));
+
+ try {
+ const success = await saveGoals(weeklyGoals.push(goalRecord));
+ if (!success) {
+ // Rollback on failure
+ setWeeklyGoals(previousGoals);
+ }
+ } catch (error) {
+ // Rollback on error
+ setWeeklyGoals(previousGoals);
+ console.error('Error saving weekly goal:', error);
+ }
};
- const toggleWeeklyGoal = (id: string) => {
- setWeeklyGoals((prev) =>
- prev.map((goal) =>
- goal.id === id
- ? {
- ...goal,
- completed: !goal.completed,
- completedAt: !goal.completed ? new Date().toISOString() : undefined,
- }
- : goal
- )
- );
+ /**
+ * Update a weekly goal with optimistic update and server persistence
+ */
+ const updateWeeklyGoal = async (id: string, updates: Partial) => {
+ if (!userId || !currentWeekId) return;
+
+ // Optimistic update
+ const previousGoals = weeklyGoals;
+ const index = weeklyGoals.findIndex(g => g.get('id') === id);
+ if (index === -1) return;
+
+ const updatedGoal = weeklyGoals.get(index)!.merge(updates);
+ setWeeklyGoals(weeklyGoals.set(index, updatedGoal));
+
+ try {
+ const success = await saveGoals(weeklyGoals.set(index, updatedGoal));
+ if (!success) {
+ // Rollback on failure
+ setWeeklyGoals(previousGoals);
+ }
+ } catch (error) {
+ // Rollback on error
+ setWeeklyGoals(previousGoals);
+ console.error('Error updating weekly goal:', error);
+ }
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Delete a weekly goal with optimistic update and server persistence
+ */
+ const deleteWeeklyGoal = async (id: string) => {
+ if (!userId || !currentWeekId) return;
+
+ // Optimistic update
+ const previousGoals = weeklyGoals;
+ const index = weeklyGoals.findIndex(g => g.get('id') === id);
+ if (index === -1) return;
+
+ setWeeklyGoals(weeklyGoals.delete(index));
+
+ try {
+ const success = await saveGoals(weeklyGoals.delete(index));
+ if (!success) {
+ // Rollback on failure
+ setWeeklyGoals(previousGoals);
+ }
+ } catch (error) {
+ // Rollback on error
+ setWeeklyGoals(previousGoals);
+ console.error('Error deleting weekly goal:', error);
+ }
+ };
+
+ /**
+ * Toggle a weekly goal completion with optimistic update and server persistence
+ */
+ const toggleWeeklyGoal = async (id: string) => {
+ if (!userId || !currentWeekId) return;
+
+ // Optimistic update
+ const previousGoals = weeklyGoals;
+ const index = weeklyGoals.findIndex(g => g.get('id') === id);
+ if (index === -1) return;
+
+ const currentGoal = weeklyGoals.get(index)!;
+ const updatedGoal = currentGoal.merge({
+ completed: !currentGoal.get('completed'),
+ completedAt: !currentGoal.get('completed') ? new Date().toISOString() : undefined,
+ });
+ setWeeklyGoals(weeklyGoals.set(index, updatedGoal));
+
+ try {
+ const success = await saveGoals(weeklyGoals.set(index, updatedGoal));
+ if (!success) {
+ // Rollback on failure
+ setWeeklyGoals(previousGoals);
+ }
+ } catch (error) {
+ // Rollback on error
+ setWeeklyGoals(previousGoals);
+ console.error('Error toggling weekly goal:', error);
+ }
};
return (
@@ -79,12 +205,13 @@ export function GoalProvider({ children }: { children: ReactNode }) {
value={{
weeklyGoals,
isLoading,
- setWeeklyGoals,
+ userId,
+ currentWeekId,
+ loadWeeklyGoals,
addWeeklyGoal,
updateWeeklyGoal,
deleteWeeklyGoal,
toggleWeeklyGoal,
- setLoading,
}}
>
{children}
@@ -94,11 +221,17 @@ export function GoalProvider({ children }: { children: ReactNode }) {
/**
* Hook to use goal context
+ * Returns goals as plain JavaScript array for easier consumption
*/
export function useGoals() {
const context = useContext(GoalContext);
if (context === undefined) {
throw new Error('useGoals must be used within a GoalProvider');
}
- return context;
+
+ // Convert immutable List to plain array for component consumption
+ return {
+ ...context,
+ weeklyGoals: context.weeklyGoals.toArray().map(g => g.toObject()),
+ };
}
diff --git a/apps/web/lib/contexts/ScoringContext.tsx b/apps/web/lib/contexts/ScoringContext.tsx
index 117cc61..b09cbc3 100644
--- a/apps/web/lib/contexts/ScoringContext.tsx
+++ b/apps/web/lib/contexts/ScoringContext.tsx
@@ -1,6 +1,8 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { List, Record } from 'immutable';
+import { saveScoring } from '@/services/scoring';
/**
* Scoring entry data type
@@ -12,41 +14,133 @@ export type ScoringEntry = {
activity: string;
points: number;
category?: string;
+ source?: string;
+ dreamId?: string;
+ weekId?: string;
+ connectId?: string;
+ createdAt?: string;
};
+/**
+ * Immutable ScoringEntry record
+ */
+const ScoringEntryRecord = Record({
+ id: '',
+ date: '',
+ score: 0,
+ activity: '',
+ points: 0,
+ category: '',
+ source: '',
+ dreamId: '',
+ weekId: '',
+ connectId: '',
+ createdAt: '',
+});
+
/**
* Scoring context state
*/
type ScoringContextState = {
- scoringHistory: ScoringEntry[];
- allYearsScoring: ScoringEntry[];
+ scoringHistory: List>;
+ allYearsScoring: List>;
allTimeScore: number;
isLoading: boolean;
- setScoringHistory: (history: ScoringEntry[]) => void;
- setAllYearsScoring: (scoring: ScoringEntry[]) => void;
- setAllTimeScore: (score: number) => void;
- addScoringEntry: (entry: ScoringEntry) => void;
- setLoading: (loading: boolean) => void;
+ userId: string | null;
+ loadScoringData: (
+ userId: string,
+ initialHistory?: ScoringEntry[],
+ initialAllYears?: ScoringEntry[],
+ initialAllTimeScore?: number
+ ) => void;
+ addScoringEntry: (entry: ScoringEntry) => Promise;
};
const ScoringContext = createContext(undefined);
/**
* Scoring context provider
- * Manages scorecard and activity scoring state
+ * Manages scorecard and activity scoring state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function ScoringProvider({ children }: { children: ReactNode }) {
- const [scoringHistory, setScoringHistory] = useState([]);
- const [allYearsScoring, setAllYearsScoring] = useState([]);
+ const [scoringHistory, setScoringHistory] = useState>>(List());
+ const [allYearsScoring, setAllYearsScoring] = useState>>(List());
const [allTimeScore, setAllTimeScore] = useState(0);
const [isLoading, setIsLoading] = useState(false);
+ const [userId, setUserId] = useState(null);
- const addScoringEntry = (entry: ScoringEntry) => {
- setScoringHistory((prev) => [entry, ...prev]);
+ /**
+ * Load scoring data for a user
+ */
+ const loadScoringData = (
+ userId: string,
+ initialHistory?: ScoringEntry[],
+ initialAllYears?: ScoringEntry[],
+ initialAllTimeScore?: number
+ ) => {
+ setUserId(userId);
+ if (initialHistory) {
+ const historyRecords = List(initialHistory.map(e => ScoringEntryRecord(e)));
+ setScoringHistory(historyRecords);
+ }
+ if (initialAllYears) {
+ const allYearsRecords = List(initialAllYears.map(e => ScoringEntryRecord(e)));
+ setAllYearsScoring(allYearsRecords);
+ }
+ if (initialAllTimeScore !== undefined) {
+ setAllTimeScore(initialAllTimeScore);
+ }
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Add a scoring entry with optimistic update and server persistence
+ */
+ const addScoringEntry = async (entry: ScoringEntry) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const entryRecord = ScoringEntryRecord(entry);
+ const previousHistory = scoringHistory;
+ const previousAllYears = allYearsScoring;
+ const previousAllTimeScore = allTimeScore;
+
+ setScoringHistory(scoringHistory.unshift(entryRecord));
+ setAllYearsScoring(allYearsScoring.unshift(entryRecord));
+ setAllTimeScore(allTimeScore + entry.points);
+
+ try {
+ const year = new Date(entry.date).getFullYear();
+ const result = await saveScoring({
+ userId,
+ year,
+ entry: {
+ id: entry.id,
+ date: entry.date,
+ source: entry.source || 'manual',
+ dreamId: entry.dreamId,
+ weekId: entry.weekId,
+ connectId: entry.connectId,
+ points: entry.points,
+ activity: entry.activity,
+ createdAt: entry.createdAt,
+ },
+ });
+
+ if (result.failed) {
+ // Rollback on failure
+ setScoringHistory(previousHistory);
+ setAllYearsScoring(previousAllYears);
+ setAllTimeScore(previousAllTimeScore);
+ console.error('Failed to save scoring entry:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setScoringHistory(previousHistory);
+ setAllYearsScoring(previousAllYears);
+ setAllTimeScore(previousAllTimeScore);
+ console.error('Error saving scoring entry:', error);
+ }
};
return (
@@ -56,11 +150,9 @@ export function ScoringProvider({ children }: { children: ReactNode }) {
allYearsScoring,
allTimeScore,
isLoading,
- setScoringHistory,
- setAllYearsScoring,
- setAllTimeScore,
+ userId,
+ loadScoringData,
addScoringEntry,
- setLoading,
}}
>
{children}
@@ -70,11 +162,18 @@ export function ScoringProvider({ children }: { children: ReactNode }) {
/**
* Hook to use scoring context
+ * Returns data as plain JavaScript arrays for easier consumption
*/
export function useScoring() {
const context = useContext(ScoringContext);
if (context === undefined) {
throw new Error('useScoring must be used within a ScoringProvider');
}
- return context;
+
+ // Convert immutable Lists to plain arrays for component consumption
+ return {
+ ...context,
+ scoringHistory: context.scoringHistory.toArray().map(e => e.toObject()),
+ allYearsScoring: context.allYearsScoring.toArray().map(e => e.toObject()),
+ };
}
diff --git a/apps/web/lib/contexts/TeamContext.tsx b/apps/web/lib/contexts/TeamContext.tsx
index 76842ba..d31e721 100644
--- a/apps/web/lib/contexts/TeamContext.tsx
+++ b/apps/web/lib/contexts/TeamContext.tsx
@@ -1,6 +1,8 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { List, Record } from 'immutable';
+import { updateTeamInfo, saveMeetingAttendance } from '@/services/teams';
/**
* Team member data type
@@ -37,52 +39,156 @@ export type TeamInfo = {
members?: TeamMember[];
};
+/**
+ * Immutable records
+ */
+const TeamInfoRecord = Record({
+ id: '',
+ name: '',
+ mission: '',
+ coachId: '',
+ members: [],
+});
+
+const MeetingRecord = Record({
+ id: '',
+ date: '',
+ attendees: [],
+ notes: '',
+ teamId: '',
+});
+
/**
* Team context state
*/
type TeamContextState = {
- teamInfo: TeamInfo | null;
- meetings: Meeting[];
+ teamInfo: Record | null;
+ meetings: List>;
isLoading: boolean;
- setTeamInfo: (info: TeamInfo | null) => void;
- updateTeamInfo: (updates: Partial) => void;
- setMeetings: (meetings: Meeting[]) => void;
- addMeeting: (meeting: Meeting) => void;
- updateMeeting: (id: string, updates: Partial) => void;
- setLoading: (loading: boolean) => void;
+ userId: string | null;
+ loadTeamData: (userId: string, teamInfo?: TeamInfo, meetings?: Meeting[]) => void;
+ updateTeamInfo: (updates: Partial) => Promise;
+ addMeeting: (meeting: Meeting) => Promise;
+ updateMeeting: (id: string, updates: Partial) => Promise;
};
const TeamContext = createContext(undefined);
/**
* Team context provider
- * Manages team collaboration state
+ * Manages team collaboration state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function TeamProvider({ children }: { children: ReactNode }) {
- const [teamInfo, setTeamInfo] = useState(null);
- const [meetings, setMeetings] = useState([]);
+ const [teamInfo, setTeamInfo] = useState | null>(null);
+ const [meetings, setMeetings] = useState>>(List());
const [isLoading, setIsLoading] = useState(false);
+ const [userId, setUserId] = useState(null);
- const updateTeamInfo = (updates: Partial) => {
- if (teamInfo) {
- setTeamInfo({ ...teamInfo, ...updates });
+ /**
+ * Load team data for a user
+ */
+ const loadTeamData = (userId: string, initialTeamInfo?: TeamInfo, initialMeetings?: Meeting[]) => {
+ setUserId(userId);
+ if (initialTeamInfo) {
+ setTeamInfo(TeamInfoRecord(initialTeamInfo));
+ }
+ if (initialMeetings) {
+ const meetingRecords = List(initialMeetings.map(m => MeetingRecord(m)));
+ setMeetings(meetingRecords);
}
};
- const addMeeting = (meeting: Meeting) => {
- setMeetings((prev) => [...prev, meeting]);
+ /**
+ * Update team info with optimistic update and server persistence
+ */
+ const updateTeamInfoAction = async (updates: Partial) => {
+ if (!teamInfo || !userId) return;
+
+ // Optimistic update
+ const previousTeamInfo = teamInfo;
+ const updatedTeamInfo = teamInfo.merge(updates);
+ setTeamInfo(updatedTeamInfo);
+
+ try {
+ const result = await updateTeamInfo({
+ managerId: userId,
+ ...updates,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setTeamInfo(previousTeamInfo);
+ console.error('Failed to update team info:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setTeamInfo(previousTeamInfo);
+ console.error('Error updating team info:', error);
+ }
};
- const updateMeeting = (id: string, updates: Partial) => {
- setMeetings((prev) =>
- prev.map((meeting) =>
- meeting.id === id ? { ...meeting, ...updates } : meeting
- )
- );
+ /**
+ * Add a meeting with optimistic update and server persistence
+ */
+ const addMeeting = async (meeting: Meeting) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const meetingRecord = MeetingRecord(meeting);
+ const previousMeetings = meetings;
+ setMeetings(meetings.push(meetingRecord));
+
+ try {
+ const result = await saveMeetingAttendance({
+ userId,
+ date: meeting.date,
+ attendees: meeting.attendees,
+ notes: meeting.notes,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setMeetings(previousMeetings);
+ console.error('Failed to save meeting:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setMeetings(previousMeetings);
+ console.error('Error saving meeting:', error);
+ }
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Update a meeting with optimistic update and server persistence
+ */
+ const updateMeeting = async (id: string, updates: Partial) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousMeetings = meetings;
+ const index = meetings.findIndex(m => m.get('id') === id);
+ if (index === -1) return;
+
+ const updatedMeeting = meetings.get(index)!.merge(updates);
+ setMeetings(meetings.set(index, updatedMeeting));
+
+ try {
+ const meetingData = updatedMeeting.toObject();
+ const result = await saveMeetingAttendance({
+ userId,
+ date: meetingData.date,
+ attendees: meetingData.attendees,
+ notes: meetingData.notes,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setMeetings(previousMeetings);
+ console.error('Failed to update meeting:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setMeetings(previousMeetings);
+ console.error('Error updating meeting:', error);
+ }
};
return (
@@ -91,12 +197,11 @@ export function TeamProvider({ children }: { children: ReactNode }) {
teamInfo,
meetings,
isLoading,
- setTeamInfo,
- updateTeamInfo,
- setMeetings,
+ userId,
+ loadTeamData,
+ updateTeamInfo: updateTeamInfoAction,
addMeeting,
updateMeeting,
- setLoading,
}}
>
{children}
@@ -106,11 +211,18 @@ export function TeamProvider({ children }: { children: ReactNode }) {
/**
* Hook to use team context
+ * Returns data as plain JavaScript objects for easier consumption
*/
export function useTeam() {
const context = useContext(TeamContext);
if (context === undefined) {
throw new Error('useTeam must be used within a TeamProvider');
}
- return context;
+
+ // Convert immutable data to plain objects for component consumption
+ return {
+ ...context,
+ teamInfo: context.teamInfo ? context.teamInfo.toObject() : null,
+ meetings: context.meetings.toArray().map(m => m.toObject()),
+ };
}
diff --git a/apps/web/lib/contexts/UserContext.tsx b/apps/web/lib/contexts/UserContext.tsx
index b25e504..19311f7 100644
--- a/apps/web/lib/contexts/UserContext.tsx
+++ b/apps/web/lib/contexts/UserContext.tsx
@@ -1,6 +1,8 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
+import { Record } from 'immutable';
+import { saveUserData } from '@/services/users';
/**
* User data type
@@ -22,42 +24,106 @@ export type User = {
dreamCategories?: string[];
};
+/**
+ * Immutable User record
+ */
+const UserRecord = Record({
+ id: '',
+ email: '',
+ name: '',
+ displayName: '',
+ office: '',
+ avatar: '',
+ jobTitle: '',
+ department: '',
+ role: 'user',
+ isCoach: false,
+ score: 0,
+ dreamsCount: 0,
+ connectsCount: 0,
+ dreamCategories: [],
+});
+
/**
* User context state
*/
type UserContextState = {
- currentUser: User | null;
+ currentUser: Record | null;
isLoading: boolean;
- setCurrentUser: (user: User | null) => void;
- updateUserProfile: (updates: Partial) => void;
- updateScore: (score: number) => void;
- setLoading: (loading: boolean) => void;
+ loadUser: (user: User) => void;
+ updateUserProfile: (updates: Partial) => Promise;
+ updateScore: (score: number) => Promise;
};
const UserContext = createContext(undefined);
/**
* User context provider
- * Manages current user profile state
+ * Manages current user profile state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
*/
export function UserProvider({ children }: { children: ReactNode }) {
- const [currentUser, setCurrentUser] = useState(null);
+ const [currentUser, setCurrentUser] = useState | null>(null);
const [isLoading, setIsLoading] = useState(false);
- const updateUserProfile = (updates: Partial) => {
- if (currentUser) {
- setCurrentUser({ ...currentUser, ...updates });
- }
+ /**
+ * Load user data
+ */
+ const loadUser = (user: User) => {
+ const userRecord = UserRecord(user);
+ setCurrentUser(userRecord);
};
- const updateScore = (score: number) => {
- if (currentUser) {
- setCurrentUser({ ...currentUser, score });
+ /**
+ * Update user profile with optimistic update and server persistence
+ */
+ const updateUserProfile = async (updates: Partial) => {
+ if (!currentUser) return;
+
+ // Optimistic update
+ const previousUser = currentUser;
+ const updatedUser = currentUser.merge(updates);
+ setCurrentUser(updatedUser);
+
+ try {
+ const userId = currentUser.get('id');
+ const result = await saveUserData({ userId, ...updatedUser.toObject() });
+ if (result.failed) {
+ // Rollback on failure
+ setCurrentUser(previousUser);
+ console.error('Failed to update user profile:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setCurrentUser(previousUser);
+ console.error('Error updating user profile:', error);
}
};
- const setLoading = (loading: boolean) => {
- setIsLoading(loading);
+ /**
+ * Update score with optimistic update and server persistence
+ */
+ const updateScore = async (score: number) => {
+ if (!currentUser) return;
+
+ // Optimistic update
+ const previousUser = currentUser;
+ const updatedUser = currentUser.set('score', score);
+ setCurrentUser(updatedUser);
+
+ try {
+ const userId = currentUser.get('id');
+ const result = await saveUserData({ userId, score });
+ if (result.failed) {
+ // Rollback on failure
+ setCurrentUser(previousUser);
+ console.error('Failed to update score:', result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setCurrentUser(previousUser);
+ console.error('Error updating score:', error);
+ }
};
return (
@@ -65,10 +131,9 @@ export function UserProvider({ children }: { children: ReactNode }) {
value={{
currentUser,
isLoading,
- setCurrentUser,
+ loadUser,
updateUserProfile,
updateScore,
- setLoading,
}}
>
{children}
@@ -78,11 +143,17 @@ export function UserProvider({ children }: { children: ReactNode }) {
/**
* Hook to use user context
+ * Returns user as plain JavaScript object for easier consumption
*/
export function useUser() {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('useUser must be used within a UserProvider');
}
- return context;
+
+ // Convert immutable Record to plain object for component consumption
+ return {
+ ...context,
+ currentUser: context.currentUser ? context.currentUser.toObject() : null,
+ };
}
diff --git a/apps/web/package.json b/apps/web/package.json
index 2f22411..0245c34 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -17,6 +17,7 @@
"@dreamspace/shared": "workspace:*",
"@microsoft/applicationinsights-web": "^3.0.5",
"canvas-confetti": "^1.9.4",
+ "immutable": "^5.1.4",
"lucide-react": "^0.451.0",
"next": "15.2.9",
"next-auth": "^5.0.0-beta.25",
@@ -31,6 +32,7 @@
"@eslint/js": "^9.39.2",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/canvas-confetti": "^1.6.4",
+ "@types/immutable": "^3.8.7",
"@types/node": "^20.10.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4b9c5e8..a0d5bf5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,6 +44,9 @@ importers:
canvas-confetti:
specifier: ^1.9.4
version: 1.9.4
+ immutable:
+ specifier: ^5.1.4
+ version: 5.1.4
lucide-react:
specifier: ^0.451.0
version: 0.451.0(react@18.3.1)
@@ -81,6 +84,9 @@ importers:
'@types/canvas-confetti':
specifier: ^1.6.4
version: 1.9.0
+ '@types/immutable':
+ specifier: ^3.8.7
+ version: 3.8.7
'@types/node':
specifier: ^20.10.0
version: 20.19.30
@@ -885,6 +891,10 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/immutable@3.8.7':
+ resolution: {integrity: sha512-nsHFDX48Tl3RaP4BF47HHe5njx40Pcp+0a8CIqzJata80Fp7JzkcuGB7UhZBGjH9aA1fMEahIqvPQQNmro5YLg==}
+ deprecated: This is a stub types definition for Facebook's Immutable (https://github.com/facebook/immutable-js). Facebook's Immutable provides its own type definitions, so you don't need @types/immutable installed!
+
'@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
@@ -1646,7 +1656,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globals@13.24.0:
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
@@ -1710,6 +1720,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ immutable@5.1.4:
+ resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -3336,6 +3349,10 @@ snapshots:
'@types/estree@1.0.8': {}
+ '@types/immutable@3.8.7':
+ dependencies:
+ immutable: 5.1.4
+
'@types/json5@0.0.29': {}
'@types/node@20.19.30':
@@ -3976,8 +3993,8 @@ snapshots:
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -3996,7 +4013,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -4007,22 +4024,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.54.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -4033,7 +4050,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -4361,6 +4378,8 @@ snapshots:
ignore@7.0.5: {}
+ immutable@5.1.4: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
From 2b36bdf7b718c83fcc2af3c9ca993d8375818ad9 Mon Sep 17 00:00:00 2001
From: Matt Lynam
Date: Fri, 6 Feb 2026 09:04:22 -0700
Subject: [PATCH 06/12] wip: dream context patterning
---
apps/web/lib/contexts/DreamContext.tsx | 289 ------------------
apps/web/lib/contexts/dreams/DreamContext.tsx | 231 ++++++++++++++
apps/web/lib/contexts/dreams/index.ts | 3 +
apps/web/lib/contexts/dreams/types.ts | 53 ++++
apps/web/lib/contexts/index.ts | 2 +-
apps/web/services/admin/getCoachingAlerts.ts | 21 +-
apps/web/services/prompts/getPrompts.ts | 18 +-
7 files changed, 312 insertions(+), 305 deletions(-)
delete mode 100644 apps/web/lib/contexts/DreamContext.tsx
create mode 100644 apps/web/lib/contexts/dreams/DreamContext.tsx
create mode 100644 apps/web/lib/contexts/dreams/index.ts
create mode 100644 apps/web/lib/contexts/dreams/types.ts
diff --git a/apps/web/lib/contexts/DreamContext.tsx b/apps/web/lib/contexts/DreamContext.tsx
deleted file mode 100644
index 0d0909c..0000000
--- a/apps/web/lib/contexts/DreamContext.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
-import { List, Record } from 'immutable';
-import { saveDreams, saveYearVision } from '@/services/dreams';
-
-/**
- * Dream data type
- */
-export type Dream = {
- id: string;
- title: string;
- category: string;
- description?: string;
- progress: number;
- image?: string;
- goals?: Goal[];
- notes?: Note[];
- coachNotes?: CoachNote[];
- history?: HistoryEntry[];
- createdAt?: string;
- updatedAt?: string;
-};
-
-export type Goal = {
- id: string;
- dreamId: string;
- title: string;
- description?: string;
- type: 'consistency' | 'deadline';
- recurrence?: 'weekly' | 'once';
- targetWeeks?: number;
- targetDate?: string;
- startDate?: string;
- weekId?: string;
- active: boolean;
- completed: boolean;
- completedAt?: string;
-};
-
-export type Note = {
- id: string;
- text: string;
- createdAt: string;
-};
-
-export type CoachNote = {
- id: string;
- text: string;
- sender: string;
- createdAt: string;
-};
-
-export type HistoryEntry = {
- id: string;
- action: string;
- details: string;
- timestamp: string;
-};
-
-/**
- * Immutable Dream record
- */
-const DreamRecord = Record({
- id: '',
- title: '',
- category: '',
- description: '',
- progress: 0,
- image: '',
- goals: [],
- notes: [],
- coachNotes: [],
- history: [],
- createdAt: '',
- updatedAt: '',
-});
-
-/**
- * Dream context state
- */
-type DreamContextState = {
- dreams: List>;
- yearVision: string;
- isLoading: boolean;
- userId: string | null;
- loadDreams: (userId: string, initialDreams?: Dream[], initialVision?: string) => void;
- setYearVision: (vision: string) => Promise;
- addDream: (dream: Dream) => Promise;
- updateDream: (id: string, updates: Partial) => Promise;
- deleteDream: (id: string) => Promise;
- reorderDreams: (dreams: Dream[]) => Promise;
-};
-
-const DreamContext = createContext(undefined);
-
-/**
- * Dream context provider
- * Manages dream book state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function DreamProvider({ children }: { children: ReactNode }) {
- const [dreams, setDreams] = useState>>(List());
- const [yearVision, setYearVisionState] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const [userId, setUserId] = useState(null);
-
- /**
- * Load dreams data for a user
- */
- const loadDreams = (userId: string, initialDreams?: Dream[], initialVision?: string) => {
- setUserId(userId);
- if (initialDreams) {
- const dreamRecords = List(initialDreams.map(d => DreamRecord(d)));
- setDreams(dreamRecords);
- }
- if (initialVision !== undefined) {
- setYearVisionState(initialVision);
- }
- };
-
- /**
- * Update year vision with optimistic update and server persistence
- */
- const setYearVision = async (vision: string) => {
- if (!userId) return;
-
- // Optimistic update
- const previousVision = yearVision;
- setYearVisionState(vision);
-
- try {
- const result = await saveYearVision({ userId, yearVision: vision });
- if (result.failed) {
- // Rollback on failure
- setYearVisionState(previousVision);
- console.error('Failed to save year vision:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setYearVisionState(previousVision);
- console.error('Error saving year vision:', error);
- }
- };
-
- /**
- * Add a dream with optimistic update and server persistence
- */
- const addDream = async (dream: Dream) => {
- if (!userId) return;
-
- // Optimistic update
- const dreamRecord = DreamRecord(dream);
- const previousDreams = dreams;
- setDreams(dreams.push(dreamRecord));
-
- try {
- const dreamsArray = dreams.push(dreamRecord).toArray().map(d => d.toObject());
- const result = await saveDreams({ userId, dreams: dreamsArray });
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error('Failed to save dream:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error('Error saving dream:', error);
- }
- };
-
- /**
- * Update a dream with optimistic update and server persistence
- */
- const updateDream = async (id: string, updates: Partial) => {
- if (!userId) return;
-
- // Optimistic update
- const previousDreams = dreams;
- const index = dreams.findIndex(d => d.get('id') === id);
- if (index === -1) return;
-
- const updatedDream = dreams.get(index)!.merge(updates);
- setDreams(dreams.set(index, updatedDream));
-
- try {
- const dreamsArray = dreams.set(index, updatedDream).toArray().map(d => d.toObject());
- const result = await saveDreams({ userId, dreams: dreamsArray });
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error('Failed to update dream:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error('Error updating dream:', error);
- }
- };
-
- /**
- * Delete a dream with optimistic update and server persistence
- */
- const deleteDream = async (id: string) => {
- if (!userId) return;
-
- // Optimistic update
- const previousDreams = dreams;
- const index = dreams.findIndex(d => d.get('id') === id);
- if (index === -1) return;
-
- setDreams(dreams.delete(index));
-
- try {
- const dreamsArray = dreams.delete(index).toArray().map(d => d.toObject());
- const result = await saveDreams({ userId, dreams: dreamsArray });
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error('Failed to delete dream:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error('Error deleting dream:', error);
- }
- };
-
- /**
- * Reorder dreams with optimistic update and server persistence
- */
- const reorderDreams = async (newDreams: Dream[]) => {
- if (!userId) return;
-
- // Optimistic update
- const previousDreams = dreams;
- const dreamRecords = List(newDreams.map(d => DreamRecord(d)));
- setDreams(dreamRecords);
-
- try {
- const result = await saveDreams({ userId, dreams: newDreams });
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error('Failed to reorder dreams:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error('Error reordering dreams:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use dream context
- * Returns dreams as plain JavaScript array for easier consumption
- */
-export function useDreams() {
- const context = useContext(DreamContext);
- if (context === undefined) {
- throw new Error('useDreams must be used within a DreamProvider');
- }
-
- // Convert immutable List to plain array for component consumption
- return {
- ...context,
- dreams: context.dreams.toArray().map(d => d.toObject()),
- };
-}
diff --git a/apps/web/lib/contexts/dreams/DreamContext.tsx b/apps/web/lib/contexts/dreams/DreamContext.tsx
new file mode 100644
index 0000000..c862359
--- /dev/null
+++ b/apps/web/lib/contexts/dreams/DreamContext.tsx
@@ -0,0 +1,231 @@
+"use client";
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ ReactNode,
+ useOptimistic,
+ useCallback,
+ useTransition,
+} from "react";
+import { List } from "immutable";
+import * as DreamsService from "@/services/dreams";
+import { Dream } from "./types";
+import { useSession } from "next-auth/react";
+
+/**
+ * Dream context state
+ */
+type DreamContextState = {
+ dreams: List;
+ yearVision: string;
+ pending: boolean;
+ setYearVision: (vision: string) => Promise;
+ add: (dream: Dream) => Promise;
+ update: (id: string, updates: Partial) => Promise;
+ $delete: (id: string) => Promise;
+ reorder: (dreams: Dream[]) => Promise;
+};
+
+const DreamContext = createContext(undefined);
+
+interface DreamsProviderProps {
+ children: ReactNode;
+ data: Dream[];
+}
+
+/**
+ * Dream context provider
+ * Manages dream book state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
+ */
+export function DreamProvider({ children, data }: DreamsProviderProps) {
+ const session = useSession();
+ const [state, setState] = useState(List(data));
+ const [dreams, setDreams] = useOptimistic(state);
+ const [yearVision, setYearVisionState] = useState("");
+ const [pending, startTransition] = useTransition();
+
+ if (!session.data?.user?.id) {
+ return null;
+ }
+
+ const userId = session.data.user.id;
+
+ /**
+ * Update year vision with optimistic update and server persistence
+ */
+ const setYearVision = async (vision: string) => {
+ // Optimistic update
+ const previousVision = yearVision;
+ setYearVisionState(vision);
+
+ try {
+ const result = await DreamsService.saveYearVision({
+ userId,
+ yearVision: vision,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setYearVisionState(previousVision);
+ console.error("Failed to save year vision:", result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setYearVisionState(previousVision);
+ console.error("Error saving year vision:", error);
+ }
+ };
+
+ /**
+ * Add a dream with optimistic update and server persistence
+ */
+ const add = useCallback(
+ async (dream: Dream) => {
+ // Optimistic update
+ const next = dreams.push(dream);
+ setDreams(next);
+
+ try {
+ startTransition(async () => {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: next.toArray().map((d) => d),
+ });
+ if (result.failed) {
+ console.error("Failed to save dream:", result.errors);
+ } else {
+ setState(next);
+ }
+ });
+ } catch (error) {
+ console.error("Unexpected error saving dream:", error);
+ }
+ },
+ [setState, setDreams, dreams],
+ );
+
+ /**
+ * Update a dream with optimistic update and server persistence
+ */
+ const updateDream = async (id: string, updates: Partial) => {
+ // Optimistic update
+ const index = dreams.findIndex((d) => d.id === id);
+ if (index === -1) return;
+
+ // const updatedDream = dreams.get(index)!.merge(updates);
+ const next = dreams.update(index, (d) => ({ ...d!, ...updates }));
+
+ setDreams(next);
+
+ try {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: next.toArray(),
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error("Failed to update dream:", result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error("Error updating dream:", error);
+ }
+ };
+
+ /**
+ * Delete a dream with optimistic update and server persistence
+ */
+ const deleteDream = async (id: string) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousDreams = dreams;
+ const index = dreams.findIndex((d) => d.get("id") === id);
+ if (index === -1) return;
+
+ setDreams(dreams.delete(index));
+
+ try {
+ const dreamsArray = dreams
+ .delete(index)
+ .toArray()
+ .map((d) => d.toObject());
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: dreamsArray,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error("Failed to delete dream:", result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error("Error deleting dream:", error);
+ }
+ };
+
+ /**
+ * Reorder dreams with optimistic update and server persistence
+ */
+ const reorderDreams = async (newDreams: Dream[]) => {
+ if (!userId) return;
+
+ // Optimistic update
+ const previousDreams = dreams;
+ const dreamRecords = List(newDreams.map((d) => DreamRecord(d)));
+ setDreams(dreamRecords);
+
+ try {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: newDreams,
+ });
+ if (result.failed) {
+ // Rollback on failure
+ setDreams(previousDreams);
+ console.error("Failed to reorder dreams:", result.errors);
+ }
+ } catch (error) {
+ // Rollback on error
+ setDreams(previousDreams);
+ console.error("Error reordering dreams:", error);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use dream context
+ */
+export function useDreams() {
+ const context = useContext(DreamContext);
+ if (context === undefined) {
+ throw new Error("useDreams must be used within a DreamProvider");
+ }
+
+ return context;
+}
diff --git a/apps/web/lib/contexts/dreams/index.ts b/apps/web/lib/contexts/dreams/index.ts
new file mode 100644
index 0000000..2dd4f31
--- /dev/null
+++ b/apps/web/lib/contexts/dreams/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./DreamContext";
diff --git a/apps/web/lib/contexts/dreams/types.ts b/apps/web/lib/contexts/dreams/types.ts
new file mode 100644
index 0000000..eae8c1e
--- /dev/null
+++ b/apps/web/lib/contexts/dreams/types.ts
@@ -0,0 +1,53 @@
+/**
+ * Dream data type
+ */
+export type Dream = {
+ id: string;
+ title: string;
+ category: string;
+ description?: string;
+ progress: number;
+ image?: string;
+ goals?: Goal[];
+ notes?: Note[];
+ coachNotes?: CoachNote[];
+ history?: HistoryEntry[];
+ createdAt?: string;
+ updatedAt?: string;
+};
+
+export type Goal = {
+ id: string;
+ dreamId: string;
+ title: string;
+ description?: string;
+ type: "consistency" | "deadline";
+ recurrence?: "weekly" | "once";
+ targetWeeks?: number;
+ targetDate?: string;
+ startDate?: string;
+ weekId?: string;
+ active: boolean;
+ completed: boolean;
+ completedAt?: string;
+};
+
+export type Note = {
+ id: string;
+ text: string;
+ createdAt: string;
+};
+
+export type CoachNote = {
+ id: string;
+ text: string;
+ sender: string;
+ createdAt: string;
+};
+
+export type HistoryEntry = {
+ id: string;
+ action: string;
+ details: string;
+ timestamp: string;
+};
diff --git a/apps/web/lib/contexts/index.ts b/apps/web/lib/contexts/index.ts
index b829bc0..10f9293 100644
--- a/apps/web/lib/contexts/index.ts
+++ b/apps/web/lib/contexts/index.ts
@@ -3,7 +3,7 @@
* Barrel export for all context providers and hooks
*/
-export * from './DreamContext';
+export * from './dreams/DreamContext';
export * from './GoalContext';
export * from './UserContext';
export * from './ConnectContext';
diff --git a/apps/web/services/admin/getCoachingAlerts.ts b/apps/web/services/admin/getCoachingAlerts.ts
index 8b4ece4..089361a 100644
--- a/apps/web/services/admin/getCoachingAlerts.ts
+++ b/apps/web/services/admin/getCoachingAlerts.ts
@@ -1,25 +1,30 @@
-'use server';
+"use server";
-import { withAdminAuth, createActionSuccess, handleActionError } from '@/lib/actions';
-import { getDatabaseClient } from '@dreamspace/database';
+import {
+ withAdminAuth,
+ createActionSuccess,
+ handleActionError,
+} from "@/lib/actions";
+import { getDatabaseClient } from "@dreamspace/database";
/**
* Gets coaching alerts for administrators.
- *
+ *
* @returns Coaching alerts
*/
export const getCoachingAlerts = withAdminAuth(async (user) => {
try {
const db = getDatabaseClient();
+
// TODO: Implement coaching alerts query
// This would analyze user/team data for alerts
-
+
return createActionSuccess({
alerts: [],
- message: 'Coaching alerts not yet implemented'
+ message: "Coaching alerts not yet implemented",
});
} catch (error) {
- console.error('Failed to get coaching alerts:', error);
- return handleActionError(error, 'Failed to get coaching alerts');
+ console.error("Failed to get coaching alerts:", error);
+ return handleActionError(error, "Failed to get coaching alerts");
}
});
diff --git a/apps/web/services/prompts/getPrompts.ts b/apps/web/services/prompts/getPrompts.ts
index aea74cf..284a5ea 100644
--- a/apps/web/services/prompts/getPrompts.ts
+++ b/apps/web/services/prompts/getPrompts.ts
@@ -1,21 +1,25 @@
-'use server';
+"use server";
-import { withAuth, createActionSuccess, handleActionError } from '@/lib/actions';
-import { getDatabaseClient } from '@dreamspace/database';
+import {
+ withAuth,
+ createActionSuccess,
+ handleActionError,
+} from "@/lib/actions";
+import { getDatabaseClient } from "@dreamspace/database";
/**
* Gets AI prompts configuration from database.
- *
+ *
* @returns Prompts configuration
*/
export const getPrompts = withAuth(async (user) => {
try {
const db = getDatabaseClient();
const prompts = await db.prompts.getPrompts();
-
+
return createActionSuccess({ prompts });
} catch (error) {
- console.error('Failed to get prompts:', error);
- return handleActionError(error, 'Failed to get prompts');
+ console.error("Failed to get prompts:", error);
+ return handleActionError(error, "Failed to get prompts");
}
});
From 0e99ea0b0eccd216b7e4fb3d09ded91ad8cc6463 Mon Sep 17 00:00:00 2001
From: Matt Lynam
Date: Sat, 7 Feb 2026 01:43:05 -0700
Subject: [PATCH 07/12] feat: add context pattern
---
apps/web/lib/contexts/ErrorsContext.tsx | 20 ++
apps/web/lib/contexts/dreams/DreamContext.tsx | 195 ++++++++----------
apps/web/services/dreams/saveYearVision.ts | 92 +++++----
3 files changed, 154 insertions(+), 153 deletions(-)
create mode 100644 apps/web/lib/contexts/ErrorsContext.tsx
diff --git a/apps/web/lib/contexts/ErrorsContext.tsx b/apps/web/lib/contexts/ErrorsContext.tsx
new file mode 100644
index 0000000..afd7eed
--- /dev/null
+++ b/apps/web/lib/contexts/ErrorsContext.tsx
@@ -0,0 +1,20 @@
+import React, { useContext } from "react";
+
+export type ErrorContextType = {
+ dispatch(error: string): void;
+ reset(): void;
+};
+
+const DEFAULT_ERROR_CONTEXT: ErrorContextType = {
+ dispatch: () => {},
+ reset: () => {},
+};
+
+export const ErrorContext = React.createContext(
+ DEFAULT_ERROR_CONTEXT,
+);
+
+export function useErrors() {
+ const context = useContext(ErrorContext);
+ return context;
+}
diff --git a/apps/web/lib/contexts/dreams/DreamContext.tsx b/apps/web/lib/contexts/dreams/DreamContext.tsx
index c862359..91e3c10 100644
--- a/apps/web/lib/contexts/dreams/DreamContext.tsx
+++ b/apps/web/lib/contexts/dreams/DreamContext.tsx
@@ -13,6 +13,7 @@ import { List } from "immutable";
import * as DreamsService from "@/services/dreams";
import { Dream } from "./types";
import { useSession } from "next-auth/react";
+import { useErrors } from "../ErrorsContext";
/**
* Dream context state
@@ -21,11 +22,11 @@ type DreamContextState = {
dreams: List;
yearVision: string;
pending: boolean;
- setYearVision: (vision: string) => Promise;
- add: (dream: Dream) => Promise;
- update: (id: string, updates: Partial) => Promise;
- $delete: (id: string) => Promise;
- reorder: (dreams: Dream[]) => Promise;
+ setYearVision: (vision: string) => void;
+ add: (dream: Dream) => void;
+ update: (id: string, updates: Partial) => void;
+ $delete: (id: string) => void;
+ reorder: (dreams: Dream[]) => void;
};
const DreamContext = createContext(undefined);
@@ -42,9 +43,11 @@ interface DreamsProviderProps {
*/
export function DreamProvider({ children, data }: DreamsProviderProps) {
const session = useSession();
+ const errors = useErrors();
const [state, setState] = useState(List(data));
const [dreams, setDreams] = useOptimistic(state);
- const [yearVision, setYearVisionState] = useState("");
+ const [visionState, setVisionState] = useState("");
+ const [vision, setVision] = useOptimistic(visionState);
const [pending, startTransition] = useTransition();
if (!session.data?.user?.id) {
@@ -56,52 +59,43 @@ export function DreamProvider({ children, data }: DreamsProviderProps) {
/**
* Update year vision with optimistic update and server persistence
*/
- const setYearVision = async (vision: string) => {
+ const setYearVision = (vision: string) => {
// Optimistic update
- const previousVision = yearVision;
- setYearVisionState(vision);
+ setVision(vision);
- try {
+ startTransition(async () => {
const result = await DreamsService.saveYearVision({
userId,
yearVision: vision,
});
if (result.failed) {
- // Rollback on failure
- setYearVisionState(previousVision);
- console.error("Failed to save year vision:", result.errors);
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setVisionState(vision);
}
- } catch (error) {
- // Rollback on error
- setYearVisionState(previousVision);
- console.error("Error saving year vision:", error);
- }
+ });
};
/**
* Add a dream with optimistic update and server persistence
*/
- const add = useCallback(
- async (dream: Dream) => {
+ const addDream = useCallback(
+ (dream: Dream) => {
// Optimistic update
const next = dreams.push(dream);
setDreams(next);
- try {
- startTransition(async () => {
- const result = await DreamsService.saveDreams({
- userId,
- dreams: next.toArray().map((d) => d),
- });
- if (result.failed) {
- console.error("Failed to save dream:", result.errors);
- } else {
- setState(next);
- }
+ startTransition(async () => {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: next.toArray(),
});
- } catch (error) {
- console.error("Unexpected error saving dream:", error);
- }
+ if (result.failed) {
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setState(next);
+ }
+ });
},
[setState, setDreams, dreams],
);
@@ -109,103 +103,84 @@ export function DreamProvider({ children, data }: DreamsProviderProps) {
/**
* Update a dream with optimistic update and server persistence
*/
- const updateDream = async (id: string, updates: Partial) => {
- // Optimistic update
- const index = dreams.findIndex((d) => d.id === id);
- if (index === -1) return;
+ const updateDream = useCallback(
+ (id: string, updates: Partial) => {
+ const index = dreams.findIndex((d) => d.id === id);
+ if (index === -1) return;
- // const updatedDream = dreams.get(index)!.merge(updates);
- const next = dreams.update(index, (d) => ({ ...d!, ...updates }));
-
- setDreams(next);
+ const next = dreams.update(index, (d) => ({ ...d!, ...updates }));
+ setDreams(next);
- try {
- const result = await DreamsService.saveDreams({
- userId,
- dreams: next.toArray(),
+ startTransition(async () => {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: next.toArray(),
+ });
+ if (result.failed) {
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setState(next);
+ }
});
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error("Failed to update dream:", result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error("Error updating dream:", error);
- }
- };
+ },
+ [setState, setDreams, dreams],
+ );
/**
* Delete a dream with optimistic update and server persistence
*/
- const deleteDream = async (id: string) => {
- if (!userId) return;
+ const deleteDream = useCallback(
+ (id: string) => {
+ const index = dreams.findIndex((d) => d.id === id);
+ if (index === -1) return;
- // Optimistic update
- const previousDreams = dreams;
- const index = dreams.findIndex((d) => d.get("id") === id);
- if (index === -1) return;
-
- setDreams(dreams.delete(index));
-
- try {
- const dreamsArray = dreams
- .delete(index)
- .toArray()
- .map((d) => d.toObject());
- const result = await DreamsService.saveDreams({
- userId,
- dreams: dreamsArray,
+ const next = dreams.delete(index);
+ setDreams(next);
+
+ startTransition(async () => {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: next.toArray(),
+ });
+ if (result.failed) {
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setState(next);
+ }
});
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error("Failed to delete dream:", result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error("Error deleting dream:", error);
- }
- };
+ },
+ [setState, setDreams, dreams],
+ );
/**
* Reorder dreams with optimistic update and server persistence
*/
- const reorderDreams = async (newDreams: Dream[]) => {
- if (!userId) return;
-
- // Optimistic update
- const previousDreams = dreams;
- const dreamRecords = List(newDreams.map((d) => DreamRecord(d)));
- setDreams(dreamRecords);
+ const reorderDreams = useCallback(
+ (newDreams: Dream[]) => {
+ const next = List(newDreams);
+ setDreams(next);
- try {
- const result = await DreamsService.saveDreams({
- userId,
- dreams: newDreams,
+ startTransition(async () => {
+ const result = await DreamsService.saveDreams({
+ userId,
+ dreams: newDreams,
+ });
+ if (result.failed) {
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setState(next);
+ }
});
- if (result.failed) {
- // Rollback on failure
- setDreams(previousDreams);
- console.error("Failed to reorder dreams:", result.errors);
- }
- } catch (error) {
- // Rollback on error
- setDreams(previousDreams);
- console.error("Error reordering dreams:", error);
- }
- };
+ },
+ [setState, setDreams],
+ );
return (
{
- try {
- const { userId, yearVision } = input;
-
- if (!userId) {
- throw new Error('userId is required');
- }
-
- // Only allow users to save their own year vision
- if (user.id !== userId) {
- throw new Error('Forbidden');
+export const saveYearVision = withAuth(
+ async (user, input: SaveYearVisionInput) => {
+ try {
+ const { userId, yearVision } = input;
+
+ if (!userId) {
+ throw new Error("userId is required");
+ }
+
+ // Only allow users to save their own year vision
+ if (user.id !== userId) {
+ throw new Error("Forbidden");
+ }
+
+ const db = getDatabaseClient();
+
+ // Get existing dreams document or create new one
+ const existingDoc = await db.dreams.getDreamsDocument(userId);
+
+ const document = {
+ ...existingDoc,
+ id: userId,
+ userId: userId,
+ dreams: existingDoc?.dreams || [],
+ weeklyGoalTemplates: existingDoc?.weeklyGoalTemplates || [],
+ yearVision: yearVision,
+ createdAt: existingDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.dreams.upsertDreamsDocument(userId, document);
+
+ return createActionSuccess({
+ id: userId,
+ message: "Year vision saved successfully",
+ });
+ } catch (error) {
+ console.error("Failed to save year vision:", error);
+ return handleActionError(error, "Failed to save year vision");
}
-
- const db = getDatabaseClient();
-
- // Get existing dreams document or create new one
- const existingDoc = await db.dreams.getDreamsDocument(userId);
-
- const document = {
- ...existingDoc,
- id: userId,
- userId: userId,
- dreams: existingDoc?.dreams || [],
- weeklyGoalTemplates: existingDoc?.weeklyGoalTemplates || [],
- yearVision: yearVision,
- createdAt: existingDoc?.createdAt || new Date().toISOString(),
- updatedAt: new Date().toISOString()
- };
-
- await db.dreams.upsertDreamsDocument(userId, document);
-
- return createActionSuccess({
- id: userId,
- message: 'Year vision saved successfully'
- });
- } catch (error) {
- console.error('Failed to save year vision:', error);
- return handleActionError(error, 'Failed to save year vision');
- }
-});
+ },
+);
From 2017b6ba8dd974775d74568598c799d206fcbc0d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Feb 2026 08:50:08 +0000
Subject: [PATCH 08/12] Refactor contexts to match DreamContext pattern
- Restructure all contexts into subdirectories (goals/, connects/, users/, teams/, scoring/)
- Use useOptimistic and useTransition for all state updates
- Implement consistent optimistic update pattern across all contexts
- Add useErrors context for centralized error handling
- Remove old Record-based immutable wrappers in favor of List with plain objects
- Simplify context API with consistent naming (add, update, $delete, toggle)
- Make contexts require data prop instead of load methods
- Remove conversion to plain JS in hooks - return immutable Lists directly
Co-authored-by: mlynam <2816612+mlynam@users.noreply.github.com>
---
apps/web/lib/contexts/ConnectContext.tsx | 194 --------------
apps/web/lib/contexts/GoalContext.tsx | 237 ------------------
apps/web/lib/contexts/ScoringContext.tsx | 179 -------------
apps/web/lib/contexts/TeamContext.tsx | 228 -----------------
apps/web/lib/contexts/UserContext.tsx | 159 ------------
.../lib/contexts/connects/ConnectContext.tsx | 157 ++++++++++++
apps/web/lib/contexts/connects/index.ts | 3 +
apps/web/lib/contexts/connects/types.ts | 18 ++
apps/web/lib/contexts/goals/GoalContext.tsx | 191 ++++++++++++++
apps/web/lib/contexts/goals/index.ts | 3 +
apps/web/lib/contexts/goals/types.ts | 15 ++
apps/web/lib/contexts/index.ts | 15 +-
.../lib/contexts/scoring/ScoringContext.tsx | 124 +++++++++
apps/web/lib/contexts/scoring/index.ts | 3 +
apps/web/lib/contexts/scoring/types.ts | 16 ++
apps/web/lib/contexts/teams/TeamContext.tsx | 167 ++++++++++++
apps/web/lib/contexts/teams/index.ts | 3 +
apps/web/lib/contexts/teams/types.ts | 34 +++
apps/web/lib/contexts/users/UserContext.tsx | 126 ++++++++++
apps/web/lib/contexts/users/index.ts | 3 +
apps/web/lib/contexts/users/types.ts | 19 ++
21 files changed, 891 insertions(+), 1003 deletions(-)
delete mode 100644 apps/web/lib/contexts/ConnectContext.tsx
delete mode 100644 apps/web/lib/contexts/GoalContext.tsx
delete mode 100644 apps/web/lib/contexts/ScoringContext.tsx
delete mode 100644 apps/web/lib/contexts/TeamContext.tsx
delete mode 100644 apps/web/lib/contexts/UserContext.tsx
create mode 100644 apps/web/lib/contexts/connects/ConnectContext.tsx
create mode 100644 apps/web/lib/contexts/connects/index.ts
create mode 100644 apps/web/lib/contexts/connects/types.ts
create mode 100644 apps/web/lib/contexts/goals/GoalContext.tsx
create mode 100644 apps/web/lib/contexts/goals/index.ts
create mode 100644 apps/web/lib/contexts/goals/types.ts
create mode 100644 apps/web/lib/contexts/scoring/ScoringContext.tsx
create mode 100644 apps/web/lib/contexts/scoring/index.ts
create mode 100644 apps/web/lib/contexts/scoring/types.ts
create mode 100644 apps/web/lib/contexts/teams/TeamContext.tsx
create mode 100644 apps/web/lib/contexts/teams/index.ts
create mode 100644 apps/web/lib/contexts/teams/types.ts
create mode 100644 apps/web/lib/contexts/users/UserContext.tsx
create mode 100644 apps/web/lib/contexts/users/index.ts
create mode 100644 apps/web/lib/contexts/users/types.ts
diff --git a/apps/web/lib/contexts/ConnectContext.tsx b/apps/web/lib/contexts/ConnectContext.tsx
deleted file mode 100644
index 784593d..0000000
--- a/apps/web/lib/contexts/ConnectContext.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { List, Record } from 'immutable';
-import { saveConnect, deleteConnect } from '@/services/connects';
-
-/**
- * Connect data type
- */
-export type Connect = {
- id: string;
- userId: string;
- dreamId?: string;
- withWhom: string;
- withWhomId: string;
- when?: string;
- notes?: string;
- status?: 'pending' | 'completed';
- agenda?: string;
- proposedWeeks?: string[];
- schedulingMethod?: string;
- createdAt?: string;
- updatedAt?: string;
-};
-
-/**
- * Immutable Connect record
- */
-const ConnectRecord = Record({
- id: '',
- userId: '',
- dreamId: '',
- withWhom: '',
- withWhomId: '',
- when: '',
- notes: '',
- status: 'pending',
- agenda: '',
- proposedWeeks: [],
- schedulingMethod: '',
- createdAt: '',
- updatedAt: '',
-});
-
-/**
- * Connect context state
- */
-type ConnectContextState = {
- connects: List>;
- isLoading: boolean;
- userId: string | null;
- loadConnects: (userId: string, initialConnects?: Connect[]) => void;
- addConnect: (connect: Connect) => Promise;
- updateConnect: (id: string, updates: Partial) => Promise;
- deleteConnect: (id: string) => Promise;
-};
-
-const ConnectContext = createContext(undefined);
-
-/**
- * Connect context provider
- * Manages dream connect/networking state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function ConnectProvider({ children }: { children: ReactNode }) {
- const [connects, setConnects] = useState>>(List());
- const [isLoading, setIsLoading] = useState(false);
- const [userId, setUserId] = useState(null);
-
- /**
- * Load connects for a user
- */
- const loadConnects = (userId: string, initialConnects?: Connect[]) => {
- setUserId(userId);
- if (initialConnects) {
- const connectRecords = List(initialConnects.map(c => ConnectRecord(c)));
- setConnects(connectRecords);
- }
- };
-
- /**
- * Add a connect with optimistic update and server persistence
- */
- const addConnect = async (connect: Connect) => {
- if (!userId) return;
-
- // Optimistic update
- const connectRecord = ConnectRecord(connect);
- const previousConnects = connects;
- setConnects(connects.push(connectRecord));
-
- try {
- const result = await saveConnect({ userId, connectData: connect });
- if (result.failed) {
- // Rollback on failure
- setConnects(previousConnects);
- console.error('Failed to save connect:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setConnects(previousConnects);
- console.error('Error saving connect:', error);
- }
- };
-
- /**
- * Update a connect with optimistic update and server persistence
- */
- const updateConnect = async (id: string, updates: Partial) => {
- if (!userId) return;
-
- // Optimistic update
- const previousConnects = connects;
- const index = connects.findIndex(c => c.get('id') === id);
- if (index === -1) return;
-
- const updatedConnect = connects.get(index)!.merge(updates);
- setConnects(connects.set(index, updatedConnect));
-
- try {
- const connectData = updatedConnect.toObject();
- const result = await saveConnect({ userId, connectData });
- if (result.failed) {
- // Rollback on failure
- setConnects(previousConnects);
- console.error('Failed to update connect:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setConnects(previousConnects);
- console.error('Error updating connect:', error);
- }
- };
-
- /**
- * Delete a connect with optimistic update and server persistence
- */
- const deleteConnectAction = async (id: string) => {
- if (!userId) return;
-
- // Optimistic update
- const previousConnects = connects;
- const index = connects.findIndex(c => c.get('id') === id);
- if (index === -1) return;
-
- setConnects(connects.delete(index));
-
- try {
- const result = await deleteConnect({ userId, connectId: id });
- if (result.failed) {
- // Rollback on failure
- setConnects(previousConnects);
- console.error('Failed to delete connect:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setConnects(previousConnects);
- console.error('Error deleting connect:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use connect context
- * Returns connects as plain JavaScript array for easier consumption
- */
-export function useConnects() {
- const context = useContext(ConnectContext);
- if (context === undefined) {
- throw new Error('useConnects must be used within a ConnectProvider');
- }
-
- // Convert immutable List to plain array for component consumption
- return {
- ...context,
- connects: context.connects.toArray().map(c => c.toObject()),
- };
-}
diff --git a/apps/web/lib/contexts/GoalContext.tsx b/apps/web/lib/contexts/GoalContext.tsx
deleted file mode 100644
index b468e2c..0000000
--- a/apps/web/lib/contexts/GoalContext.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { List, Record } from 'immutable';
-import { saveCurrentWeek } from '@/services/weeks';
-
-/**
- * Weekly goal data type
- */
-export type WeeklyGoal = {
- id: string;
- title: string;
- dreamId?: string;
- goalId?: string;
- completed: boolean;
- completedAt?: string;
- weekId: string;
- recurrence: 'weekly' | 'once';
- active: boolean;
- weekLog?: Record;
-};
-
-/**
- * Immutable WeeklyGoal record
- */
-const WeeklyGoalRecord = Record({
- id: '',
- title: '',
- dreamId: '',
- goalId: '',
- completed: false,
- completedAt: '',
- weekId: '',
- recurrence: 'weekly',
- active: true,
- weekLog: {},
-});
-
-/**
- * Goal context state
- */
-type GoalContextState = {
- weeklyGoals: List>;
- isLoading: boolean;
- userId: string | null;
- currentWeekId: string | null;
- loadWeeklyGoals: (userId: string, weekId: string, initialGoals?: WeeklyGoal[]) => void;
- addWeeklyGoal: (goal: WeeklyGoal) => Promise;
- updateWeeklyGoal: (id: string, updates: Partial) => Promise;
- deleteWeeklyGoal: (id: string) => Promise;
- toggleWeeklyGoal: (id: string) => Promise;
-};
-
-const GoalContext = createContext(undefined);
-
-/**
- * Goal context provider
- * Manages weekly goals state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function GoalProvider({ children }: { children: ReactNode }) {
- const [weeklyGoals, setWeeklyGoals] = useState>>(List());
- const [isLoading, setIsLoading] = useState(false);
- const [userId, setUserId] = useState(null);
- const [currentWeekId, setCurrentWeekId] = useState(null);
-
- /**
- * Load weekly goals for a user and week
- */
- const loadWeeklyGoals = (userId: string, weekId: string, initialGoals?: WeeklyGoal[]) => {
- setUserId(userId);
- setCurrentWeekId(weekId);
- if (initialGoals) {
- const goalRecords = List(initialGoals.map(g => WeeklyGoalRecord(g)));
- setWeeklyGoals(goalRecords);
- }
- };
-
- /**
- * Save goals to server
- */
- const saveGoals = async (goals: List>) => {
- if (!userId || !currentWeekId) return;
-
- const goalsArray = goals.toArray().map(g => g.toObject());
- const result = await saveCurrentWeek({ userId, weekId: currentWeekId, goals: goalsArray });
-
- if (result.failed) {
- console.error('Failed to save weekly goals:', result.errors);
- return false;
- }
- return true;
- };
-
- /**
- * Add a weekly goal with optimistic update and server persistence
- */
- const addWeeklyGoal = async (goal: WeeklyGoal) => {
- if (!userId || !currentWeekId) return;
-
- // Optimistic update
- const goalRecord = WeeklyGoalRecord(goal);
- const previousGoals = weeklyGoals;
- setWeeklyGoals(weeklyGoals.push(goalRecord));
-
- try {
- const success = await saveGoals(weeklyGoals.push(goalRecord));
- if (!success) {
- // Rollback on failure
- setWeeklyGoals(previousGoals);
- }
- } catch (error) {
- // Rollback on error
- setWeeklyGoals(previousGoals);
- console.error('Error saving weekly goal:', error);
- }
- };
-
- /**
- * Update a weekly goal with optimistic update and server persistence
- */
- const updateWeeklyGoal = async (id: string, updates: Partial) => {
- if (!userId || !currentWeekId) return;
-
- // Optimistic update
- const previousGoals = weeklyGoals;
- const index = weeklyGoals.findIndex(g => g.get('id') === id);
- if (index === -1) return;
-
- const updatedGoal = weeklyGoals.get(index)!.merge(updates);
- setWeeklyGoals(weeklyGoals.set(index, updatedGoal));
-
- try {
- const success = await saveGoals(weeklyGoals.set(index, updatedGoal));
- if (!success) {
- // Rollback on failure
- setWeeklyGoals(previousGoals);
- }
- } catch (error) {
- // Rollback on error
- setWeeklyGoals(previousGoals);
- console.error('Error updating weekly goal:', error);
- }
- };
-
- /**
- * Delete a weekly goal with optimistic update and server persistence
- */
- const deleteWeeklyGoal = async (id: string) => {
- if (!userId || !currentWeekId) return;
-
- // Optimistic update
- const previousGoals = weeklyGoals;
- const index = weeklyGoals.findIndex(g => g.get('id') === id);
- if (index === -1) return;
-
- setWeeklyGoals(weeklyGoals.delete(index));
-
- try {
- const success = await saveGoals(weeklyGoals.delete(index));
- if (!success) {
- // Rollback on failure
- setWeeklyGoals(previousGoals);
- }
- } catch (error) {
- // Rollback on error
- setWeeklyGoals(previousGoals);
- console.error('Error deleting weekly goal:', error);
- }
- };
-
- /**
- * Toggle a weekly goal completion with optimistic update and server persistence
- */
- const toggleWeeklyGoal = async (id: string) => {
- if (!userId || !currentWeekId) return;
-
- // Optimistic update
- const previousGoals = weeklyGoals;
- const index = weeklyGoals.findIndex(g => g.get('id') === id);
- if (index === -1) return;
-
- const currentGoal = weeklyGoals.get(index)!;
- const updatedGoal = currentGoal.merge({
- completed: !currentGoal.get('completed'),
- completedAt: !currentGoal.get('completed') ? new Date().toISOString() : undefined,
- });
- setWeeklyGoals(weeklyGoals.set(index, updatedGoal));
-
- try {
- const success = await saveGoals(weeklyGoals.set(index, updatedGoal));
- if (!success) {
- // Rollback on failure
- setWeeklyGoals(previousGoals);
- }
- } catch (error) {
- // Rollback on error
- setWeeklyGoals(previousGoals);
- console.error('Error toggling weekly goal:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use goal context
- * Returns goals as plain JavaScript array for easier consumption
- */
-export function useGoals() {
- const context = useContext(GoalContext);
- if (context === undefined) {
- throw new Error('useGoals must be used within a GoalProvider');
- }
-
- // Convert immutable List to plain array for component consumption
- return {
- ...context,
- weeklyGoals: context.weeklyGoals.toArray().map(g => g.toObject()),
- };
-}
diff --git a/apps/web/lib/contexts/ScoringContext.tsx b/apps/web/lib/contexts/ScoringContext.tsx
deleted file mode 100644
index b09cbc3..0000000
--- a/apps/web/lib/contexts/ScoringContext.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { List, Record } from 'immutable';
-import { saveScoring } from '@/services/scoring';
-
-/**
- * Scoring entry data type
- */
-export type ScoringEntry = {
- id: string;
- date: string;
- score: number;
- activity: string;
- points: number;
- category?: string;
- source?: string;
- dreamId?: string;
- weekId?: string;
- connectId?: string;
- createdAt?: string;
-};
-
-/**
- * Immutable ScoringEntry record
- */
-const ScoringEntryRecord = Record({
- id: '',
- date: '',
- score: 0,
- activity: '',
- points: 0,
- category: '',
- source: '',
- dreamId: '',
- weekId: '',
- connectId: '',
- createdAt: '',
-});
-
-/**
- * Scoring context state
- */
-type ScoringContextState = {
- scoringHistory: List>;
- allYearsScoring: List>;
- allTimeScore: number;
- isLoading: boolean;
- userId: string | null;
- loadScoringData: (
- userId: string,
- initialHistory?: ScoringEntry[],
- initialAllYears?: ScoringEntry[],
- initialAllTimeScore?: number
- ) => void;
- addScoringEntry: (entry: ScoringEntry) => Promise;
-};
-
-const ScoringContext = createContext(undefined);
-
-/**
- * Scoring context provider
- * Manages scorecard and activity scoring state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function ScoringProvider({ children }: { children: ReactNode }) {
- const [scoringHistory, setScoringHistory] = useState>>(List());
- const [allYearsScoring, setAllYearsScoring] = useState>>(List());
- const [allTimeScore, setAllTimeScore] = useState(0);
- const [isLoading, setIsLoading] = useState(false);
- const [userId, setUserId] = useState(null);
-
- /**
- * Load scoring data for a user
- */
- const loadScoringData = (
- userId: string,
- initialHistory?: ScoringEntry[],
- initialAllYears?: ScoringEntry[],
- initialAllTimeScore?: number
- ) => {
- setUserId(userId);
- if (initialHistory) {
- const historyRecords = List(initialHistory.map(e => ScoringEntryRecord(e)));
- setScoringHistory(historyRecords);
- }
- if (initialAllYears) {
- const allYearsRecords = List(initialAllYears.map(e => ScoringEntryRecord(e)));
- setAllYearsScoring(allYearsRecords);
- }
- if (initialAllTimeScore !== undefined) {
- setAllTimeScore(initialAllTimeScore);
- }
- };
-
- /**
- * Add a scoring entry with optimistic update and server persistence
- */
- const addScoringEntry = async (entry: ScoringEntry) => {
- if (!userId) return;
-
- // Optimistic update
- const entryRecord = ScoringEntryRecord(entry);
- const previousHistory = scoringHistory;
- const previousAllYears = allYearsScoring;
- const previousAllTimeScore = allTimeScore;
-
- setScoringHistory(scoringHistory.unshift(entryRecord));
- setAllYearsScoring(allYearsScoring.unshift(entryRecord));
- setAllTimeScore(allTimeScore + entry.points);
-
- try {
- const year = new Date(entry.date).getFullYear();
- const result = await saveScoring({
- userId,
- year,
- entry: {
- id: entry.id,
- date: entry.date,
- source: entry.source || 'manual',
- dreamId: entry.dreamId,
- weekId: entry.weekId,
- connectId: entry.connectId,
- points: entry.points,
- activity: entry.activity,
- createdAt: entry.createdAt,
- },
- });
-
- if (result.failed) {
- // Rollback on failure
- setScoringHistory(previousHistory);
- setAllYearsScoring(previousAllYears);
- setAllTimeScore(previousAllTimeScore);
- console.error('Failed to save scoring entry:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setScoringHistory(previousHistory);
- setAllYearsScoring(previousAllYears);
- setAllTimeScore(previousAllTimeScore);
- console.error('Error saving scoring entry:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use scoring context
- * Returns data as plain JavaScript arrays for easier consumption
- */
-export function useScoring() {
- const context = useContext(ScoringContext);
- if (context === undefined) {
- throw new Error('useScoring must be used within a ScoringProvider');
- }
-
- // Convert immutable Lists to plain arrays for component consumption
- return {
- ...context,
- scoringHistory: context.scoringHistory.toArray().map(e => e.toObject()),
- allYearsScoring: context.allYearsScoring.toArray().map(e => e.toObject()),
- };
-}
diff --git a/apps/web/lib/contexts/TeamContext.tsx b/apps/web/lib/contexts/TeamContext.tsx
deleted file mode 100644
index d31e721..0000000
--- a/apps/web/lib/contexts/TeamContext.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { List, Record } from 'immutable';
-import { updateTeamInfo, saveMeetingAttendance } from '@/services/teams';
-
-/**
- * Team member data type
- */
-export type TeamMember = {
- id: string;
- name: string;
- email?: string;
- avatar?: string;
- role?: string;
- dreamsCompleted?: number;
- meetingAttendance?: number;
-};
-
-/**
- * Meeting data type
- */
-export type Meeting = {
- id: string;
- date: string;
- attendees: string[];
- notes?: string;
- teamId?: string;
-};
-
-/**
- * Team info data type
- */
-export type TeamInfo = {
- id: string;
- name: string;
- mission?: string;
- coachId?: string;
- members?: TeamMember[];
-};
-
-/**
- * Immutable records
- */
-const TeamInfoRecord = Record({
- id: '',
- name: '',
- mission: '',
- coachId: '',
- members: [],
-});
-
-const MeetingRecord = Record({
- id: '',
- date: '',
- attendees: [],
- notes: '',
- teamId: '',
-});
-
-/**
- * Team context state
- */
-type TeamContextState = {
- teamInfo: Record | null;
- meetings: List>;
- isLoading: boolean;
- userId: string | null;
- loadTeamData: (userId: string, teamInfo?: TeamInfo, meetings?: Meeting[]) => void;
- updateTeamInfo: (updates: Partial) => Promise;
- addMeeting: (meeting: Meeting) => Promise;
- updateMeeting: (id: string, updates: Partial) => Promise;
-};
-
-const TeamContext = createContext(undefined);
-
-/**
- * Team context provider
- * Manages team collaboration state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function TeamProvider({ children }: { children: ReactNode }) {
- const [teamInfo, setTeamInfo] = useState | null>(null);
- const [meetings, setMeetings] = useState>>(List());
- const [isLoading, setIsLoading] = useState(false);
- const [userId, setUserId] = useState(null);
-
- /**
- * Load team data for a user
- */
- const loadTeamData = (userId: string, initialTeamInfo?: TeamInfo, initialMeetings?: Meeting[]) => {
- setUserId(userId);
- if (initialTeamInfo) {
- setTeamInfo(TeamInfoRecord(initialTeamInfo));
- }
- if (initialMeetings) {
- const meetingRecords = List(initialMeetings.map(m => MeetingRecord(m)));
- setMeetings(meetingRecords);
- }
- };
-
- /**
- * Update team info with optimistic update and server persistence
- */
- const updateTeamInfoAction = async (updates: Partial) => {
- if (!teamInfo || !userId) return;
-
- // Optimistic update
- const previousTeamInfo = teamInfo;
- const updatedTeamInfo = teamInfo.merge(updates);
- setTeamInfo(updatedTeamInfo);
-
- try {
- const result = await updateTeamInfo({
- managerId: userId,
- ...updates,
- });
- if (result.failed) {
- // Rollback on failure
- setTeamInfo(previousTeamInfo);
- console.error('Failed to update team info:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setTeamInfo(previousTeamInfo);
- console.error('Error updating team info:', error);
- }
- };
-
- /**
- * Add a meeting with optimistic update and server persistence
- */
- const addMeeting = async (meeting: Meeting) => {
- if (!userId) return;
-
- // Optimistic update
- const meetingRecord = MeetingRecord(meeting);
- const previousMeetings = meetings;
- setMeetings(meetings.push(meetingRecord));
-
- try {
- const result = await saveMeetingAttendance({
- userId,
- date: meeting.date,
- attendees: meeting.attendees,
- notes: meeting.notes,
- });
- if (result.failed) {
- // Rollback on failure
- setMeetings(previousMeetings);
- console.error('Failed to save meeting:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setMeetings(previousMeetings);
- console.error('Error saving meeting:', error);
- }
- };
-
- /**
- * Update a meeting with optimistic update and server persistence
- */
- const updateMeeting = async (id: string, updates: Partial) => {
- if (!userId) return;
-
- // Optimistic update
- const previousMeetings = meetings;
- const index = meetings.findIndex(m => m.get('id') === id);
- if (index === -1) return;
-
- const updatedMeeting = meetings.get(index)!.merge(updates);
- setMeetings(meetings.set(index, updatedMeeting));
-
- try {
- const meetingData = updatedMeeting.toObject();
- const result = await saveMeetingAttendance({
- userId,
- date: meetingData.date,
- attendees: meetingData.attendees,
- notes: meetingData.notes,
- });
- if (result.failed) {
- // Rollback on failure
- setMeetings(previousMeetings);
- console.error('Failed to update meeting:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setMeetings(previousMeetings);
- console.error('Error updating meeting:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use team context
- * Returns data as plain JavaScript objects for easier consumption
- */
-export function useTeam() {
- const context = useContext(TeamContext);
- if (context === undefined) {
- throw new Error('useTeam must be used within a TeamProvider');
- }
-
- // Convert immutable data to plain objects for component consumption
- return {
- ...context,
- teamInfo: context.teamInfo ? context.teamInfo.toObject() : null,
- meetings: context.meetings.toArray().map(m => m.toObject()),
- };
-}
diff --git a/apps/web/lib/contexts/UserContext.tsx b/apps/web/lib/contexts/UserContext.tsx
deleted file mode 100644
index 19311f7..0000000
--- a/apps/web/lib/contexts/UserContext.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-'use client';
-
-import React, { createContext, useContext, useState, ReactNode } from 'react';
-import { Record } from 'immutable';
-import { saveUserData } from '@/services/users';
-
-/**
- * User data type
- */
-export type User = {
- id: string;
- email: string;
- name: string;
- displayName?: string;
- office?: string;
- avatar?: string;
- jobTitle?: string;
- department?: string;
- role?: 'user' | 'coach' | 'admin';
- isCoach?: boolean;
- score?: number;
- dreamsCount?: number;
- connectsCount?: number;
- dreamCategories?: string[];
-};
-
-/**
- * Immutable User record
- */
-const UserRecord = Record({
- id: '',
- email: '',
- name: '',
- displayName: '',
- office: '',
- avatar: '',
- jobTitle: '',
- department: '',
- role: 'user',
- isCoach: false,
- score: 0,
- dreamsCount: 0,
- connectsCount: 0,
- dreamCategories: [],
-});
-
-/**
- * User context state
- */
-type UserContextState = {
- currentUser: Record | null;
- isLoading: boolean;
- loadUser: (user: User) => void;
- updateUserProfile: (updates: Partial) => Promise;
- updateScore: (score: number) => Promise;
-};
-
-const UserContext = createContext(undefined);
-
-/**
- * User context provider
- * Manages current user profile state with immutable data structures
- * Loads data on initialization and saves optimistically via services
- */
-export function UserProvider({ children }: { children: ReactNode }) {
- const [currentUser, setCurrentUser] = useState | null>(null);
- const [isLoading, setIsLoading] = useState(false);
-
- /**
- * Load user data
- */
- const loadUser = (user: User) => {
- const userRecord = UserRecord(user);
- setCurrentUser(userRecord);
- };
-
- /**
- * Update user profile with optimistic update and server persistence
- */
- const updateUserProfile = async (updates: Partial) => {
- if (!currentUser) return;
-
- // Optimistic update
- const previousUser = currentUser;
- const updatedUser = currentUser.merge(updates);
- setCurrentUser(updatedUser);
-
- try {
- const userId = currentUser.get('id');
- const result = await saveUserData({ userId, ...updatedUser.toObject() });
- if (result.failed) {
- // Rollback on failure
- setCurrentUser(previousUser);
- console.error('Failed to update user profile:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setCurrentUser(previousUser);
- console.error('Error updating user profile:', error);
- }
- };
-
- /**
- * Update score with optimistic update and server persistence
- */
- const updateScore = async (score: number) => {
- if (!currentUser) return;
-
- // Optimistic update
- const previousUser = currentUser;
- const updatedUser = currentUser.set('score', score);
- setCurrentUser(updatedUser);
-
- try {
- const userId = currentUser.get('id');
- const result = await saveUserData({ userId, score });
- if (result.failed) {
- // Rollback on failure
- setCurrentUser(previousUser);
- console.error('Failed to update score:', result.errors);
- }
- } catch (error) {
- // Rollback on error
- setCurrentUser(previousUser);
- console.error('Error updating score:', error);
- }
- };
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Hook to use user context
- * Returns user as plain JavaScript object for easier consumption
- */
-export function useUser() {
- const context = useContext(UserContext);
- if (context === undefined) {
- throw new Error('useUser must be used within a UserProvider');
- }
-
- // Convert immutable Record to plain object for component consumption
- return {
- ...context,
- currentUser: context.currentUser ? context.currentUser.toObject() : null,
- };
-}
diff --git a/apps/web/lib/contexts/connects/ConnectContext.tsx b/apps/web/lib/contexts/connects/ConnectContext.tsx
new file mode 100644
index 0000000..ac2acd7
--- /dev/null
+++ b/apps/web/lib/contexts/connects/ConnectContext.tsx
@@ -0,0 +1,157 @@
+"use client";
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ ReactNode,
+ useOptimistic,
+ useCallback,
+ useTransition,
+} from "react";
+import { List } from "immutable";
+import * as ConnectsService from "@/services/connects";
+import { Connect } from "./types";
+import { useSession } from "next-auth/react";
+import { useErrors } from "../ErrorsContext";
+
+/**
+ * Connect context state
+ */
+type ConnectContextState = {
+ connects: List;
+ pending: boolean;
+ add: (connect: Connect) => void;
+ update: (id: string, updates: Partial) => void;
+ $delete: (id: string) => void;
+};
+
+const ConnectContext = createContext(
+ undefined,
+);
+
+interface ConnectProviderProps {
+ children: ReactNode;
+ data: Connect[];
+}
+
+/**
+ * Connect context provider
+ * Manages dream connect/networking state with immutable data structures
+ * Loads data on initialization and saves optimistically via services
+ */
+export function ConnectProvider({ children, data }: ConnectProviderProps) {
+ const session = useSession();
+ const errors = useErrors();
+ const [state, setState] = useState(List(data));
+ const [connects, setConnects] = useOptimistic(state);
+ const [pending, startTransition] = useTransition();
+
+ if (!session.data?.user?.id) {
+ return null;
+ }
+
+ const userId = session.data.user.id;
+
+ /**
+ * Add a connect with optimistic update and server persistence
+ */
+ const addConnect = useCallback(
+ (connect: Connect) => {
+ const next = connects.push(connect);
+ setConnects(next);
+
+ startTransition(async () => {
+ const result = await ConnectsService.saveConnect({
+ userId,
+ connectData: connect,
+ });
+ if (result.failed) {
+ errors.dispatch(result.errors._errors.join(","));
+ } else {
+ setState(next);
+ }
+ });
+ },
+ [setState, setConnects, connects, userId],
+ );
+
+ /**
+ * Update a connect with optimistic update and server persistence
+ */
+ const updateConnect = useCallback(
+ (id: string, updates: Partial