-
Current Week
-
Your weekly goals will appear here
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
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
+
+
+
+
+
+
+ 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
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+ 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
+
+
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!
+ ) : (
+
+ )}
+
+
+
+ );
+}
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 (
+
+ );
+}
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/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/connects/ConnectContext.tsx b/apps/web/lib/contexts/connects/ConnectContext.tsx
new file mode 100644
index 0000000..48899bd
--- /dev/null
+++ b/apps/web/lib/contexts/connects/ConnectContext.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { List } from "immutable";
+import { Connect } from "./types";
+
+/**
+ * Connect context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type ConnectContextState = {
+ connects: List;
+};
+
+const ConnectContext = createContext(
+ undefined,
+);
+
+interface ConnectProviderProps {
+ children: ReactNode;
+ data: Connect[];
+}
+
+/**
+ * Connect context provider - READ ONLY
+ * Provides dream connect/networking data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function ConnectProvider({ children, data }: ConnectProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use connect context - READ ONLY
+ * For mutations, use server actions from @/services/connects
+ */
+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/connects/index.ts b/apps/web/lib/contexts/connects/index.ts
new file mode 100644
index 0000000..8cbc108
--- /dev/null
+++ b/apps/web/lib/contexts/connects/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./ConnectContext";
diff --git a/apps/web/lib/contexts/connects/types.ts b/apps/web/lib/contexts/connects/types.ts
new file mode 100644
index 0000000..9040df4
--- /dev/null
+++ b/apps/web/lib/contexts/connects/types.ts
@@ -0,0 +1,18 @@
+/**
+ * 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;
+};
diff --git a/apps/web/lib/contexts/dreams/DreamContext.tsx b/apps/web/lib/contexts/dreams/DreamContext.tsx
new file mode 100644
index 0000000..a1dcf93
--- /dev/null
+++ b/apps/web/lib/contexts/dreams/DreamContext.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { List } from "immutable";
+import { Dream } from "./types";
+
+/**
+ * Dream context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type DreamContextState = {
+ dreams: List;
+ yearVision: string;
+};
+
+const DreamContext = createContext(undefined);
+
+interface DreamsProviderProps {
+ children: ReactNode;
+ data: Dream[];
+ yearVision?: string;
+}
+
+/**
+ * Dream context provider - READ ONLY
+ * Provides dream book data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function DreamProvider({
+ children,
+ data,
+ yearVision = "",
+}: DreamsProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use dream context - READ ONLY
+ * For mutations, use server actions from @/services/dreams
+ */
+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/goals/GoalContext.tsx b/apps/web/lib/contexts/goals/GoalContext.tsx
new file mode 100644
index 0000000..3b01fe0
--- /dev/null
+++ b/apps/web/lib/contexts/goals/GoalContext.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { List } from "immutable";
+import { WeeklyGoal } from "./types";
+
+/**
+ * Goal context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type GoalContextState = {
+ goals: List;
+ weekId: string;
+};
+
+const GoalContext = createContext(undefined);
+
+interface GoalProviderProps {
+ children: ReactNode;
+ data: WeeklyGoal[];
+ weekId: string;
+}
+
+/**
+ * Goal context provider - READ ONLY
+ * Provides weekly goals data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function GoalProvider({ children, data, weekId }: GoalProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use goal context - READ ONLY
+ * For mutations, use server actions from @/services/weeks
+ */
+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/goals/index.ts b/apps/web/lib/contexts/goals/index.ts
new file mode 100644
index 0000000..27eb054
--- /dev/null
+++ b/apps/web/lib/contexts/goals/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./GoalContext";
diff --git a/apps/web/lib/contexts/goals/types.ts b/apps/web/lib/contexts/goals/types.ts
new file mode 100644
index 0000000..82e0498
--- /dev/null
+++ b/apps/web/lib/contexts/goals/types.ts
@@ -0,0 +1,15 @@
+/**
+ * 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;
+};
diff --git a/apps/web/lib/contexts/index.ts b/apps/web/lib/contexts/index.ts
new file mode 100644
index 0000000..1825e17
--- /dev/null
+++ b/apps/web/lib/contexts/index.ts
@@ -0,0 +1,14 @@
+/**
+ * Context exports
+ * Barrel export for all context providers and hooks
+ */
+
+export * from "./dreams";
+export * from "./goals";
+export * from "./users";
+export * from "./connects";
+export * from "./teams";
+export * from "./scoring";
+export * from "./ErrorsContext";
+export * from "./AppProviders";
+
diff --git a/apps/web/lib/contexts/scoring/ScoringContext.tsx b/apps/web/lib/contexts/scoring/ScoringContext.tsx
new file mode 100644
index 0000000..a33e841
--- /dev/null
+++ b/apps/web/lib/contexts/scoring/ScoringContext.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { List } from "immutable";
+import { ScoringEntry } from "./types";
+
+/**
+ * Scoring context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type ScoringContextState = {
+ entries: List;
+ allTimeScore: number;
+};
+
+const ScoringContext = createContext(
+ undefined,
+);
+
+interface ScoringProviderProps {
+ children: ReactNode;
+ data: ScoringEntry[];
+ allTimeScore: number;
+}
+
+/**
+ * Scoring context provider - READ ONLY
+ * Provides scorecard and activity scoring data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function ScoringProvider({
+ children,
+ data,
+ allTimeScore,
+}: ScoringProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use scoring context - READ ONLY
+ * For mutations, use server actions from @/services/scoring
+ */
+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/scoring/index.ts b/apps/web/lib/contexts/scoring/index.ts
new file mode 100644
index 0000000..465aa8d
--- /dev/null
+++ b/apps/web/lib/contexts/scoring/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./ScoringContext";
diff --git a/apps/web/lib/contexts/scoring/types.ts b/apps/web/lib/contexts/scoring/types.ts
new file mode 100644
index 0000000..88b97af
--- /dev/null
+++ b/apps/web/lib/contexts/scoring/types.ts
@@ -0,0 +1,16 @@
+/**
+ * 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;
+};
diff --git a/apps/web/lib/contexts/teams/TeamContext.tsx b/apps/web/lib/contexts/teams/TeamContext.tsx
new file mode 100644
index 0000000..bd61498
--- /dev/null
+++ b/apps/web/lib/contexts/teams/TeamContext.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { List } from "immutable";
+import { TeamInfo, Meeting } from "./types";
+
+/**
+ * Team context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type TeamContextState = {
+ teamInfo: TeamInfo | null;
+ meetings: List;
+};
+
+const TeamContext = createContext(undefined);
+
+interface TeamProviderProps {
+ children: ReactNode;
+ teamData: TeamInfo | null;
+ meetingsData: Meeting[];
+}
+
+/**
+ * Team context provider - READ ONLY
+ * Provides team collaboration data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function TeamProvider({
+ children,
+ teamData,
+ meetingsData,
+}: TeamProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use team context - READ ONLY
+ * For mutations, use server actions from @/services/teams
+ */
+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/teams/index.ts b/apps/web/lib/contexts/teams/index.ts
new file mode 100644
index 0000000..731502d
--- /dev/null
+++ b/apps/web/lib/contexts/teams/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./TeamContext";
diff --git a/apps/web/lib/contexts/teams/types.ts b/apps/web/lib/contexts/teams/types.ts
new file mode 100644
index 0000000..7727786
--- /dev/null
+++ b/apps/web/lib/contexts/teams/types.ts
@@ -0,0 +1,34 @@
+/**
+ * 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[];
+};
diff --git a/apps/web/lib/contexts/users/UserContext.tsx b/apps/web/lib/contexts/users/UserContext.tsx
new file mode 100644
index 0000000..cc9e188
--- /dev/null
+++ b/apps/web/lib/contexts/users/UserContext.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import React, { createContext, useContext, ReactNode } from "react";
+import { User } from "./types";
+
+/**
+ * User context state - READ ONLY
+ * Mutations should be handled by server actions that trigger revalidation
+ */
+type UserContextState = {
+ user: User | null;
+};
+
+const UserContext = createContext(undefined);
+
+interface UserProviderProps {
+ children: ReactNode;
+ data: User | null;
+}
+
+/**
+ * User context provider - READ ONLY
+ * Provides current user profile data to components.
+ * Mutations are handled via server actions (not context methods).
+ * Server actions should use revalidatePath/revalidateTag to refresh this data.
+ */
+export function UserProvider({ children, data }: UserProviderProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use user context - READ ONLY
+ * For mutations, use server actions from @/services/users
+ */
+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/users/index.ts b/apps/web/lib/contexts/users/index.ts
new file mode 100644
index 0000000..5f951aa
--- /dev/null
+++ b/apps/web/lib/contexts/users/index.ts
@@ -0,0 +1,3 @@
+export * from "./types";
+
+export * from "./UserContext";
diff --git a/apps/web/lib/contexts/users/types.ts b/apps/web/lib/contexts/users/types.ts
new file mode 100644
index 0000000..9fa5a2e
--- /dev/null
+++ b/apps/web/lib/contexts/users/types.ts
@@ -0,0 +1,19 @@
+/**
+ * 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[];
+};
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/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/connects/index.ts b/apps/web/services/connects/index.ts
index dddefea..68ee257 100644
--- a/apps/web/services/connects/index.ts
+++ b/apps/web/services/connects/index.ts
@@ -2,6 +2,12 @@
* Connects service exports
* Barrel export for all connect-related server actions
*/
+
+// Get operations
export * from './getConnects';
+
+// Form actions (useActionState compatible)
export * from './saveConnect';
+
+// Non-form mutations (deletes)
export * from './deleteConnect';
diff --git a/apps/web/services/connects/saveConnect.ts b/apps/web/services/connects/saveConnect.ts
index ee7ee72..047d609 100644
--- a/apps/web/services/connects/saveConnect.ts
+++ b/apps/web/services/connects/saveConnect.ts
@@ -1,89 +1,103 @@
'use server';
-import { withAuth, createActionSuccess, handleActionError } from '@/lib/actions';
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth, createActionSuccess } from '@/lib/actions';
import { getDatabaseClient } from '@dreamspace/database';
-interface SaveConnectInput {
- userId: string;
- connectData: {
- id?: string;
- userId?: string;
- type?: string;
- withWhom: string;
- withWhomId: string;
- when: string;
- notes?: string;
- status?: string;
- agenda?: string;
- proposedWeeks?: string[];
- schedulingMethod?: string;
- dreamId?: string;
- name?: string;
- category?: string;
- avatar?: string;
- office?: string;
- createdAt?: string;
+/**
+ * Form action state for connect save
+ */
+export type SaveConnectState = {
+ success: boolean;
+ errors?: {
+ connectType?: string[];
+ connectDate?: string[];
+ recipientName?: string[];
+ _form?: string[];
+ };
+ data?: {
+ id: string;
};
-}
+};
+
+/**
+ * Schema for connect form data
+ */
+const connectFormSchema = zfd.formData({
+ id: zfd.text(z.string().optional()),
+ connectType: zfd.text(z.string().min(1, 'Connect type is required')),
+ connectDate: zfd.text(z.string().min(1, 'Date is required')),
+ notes: zfd.text(z.string().optional()),
+ recipientUserId: zfd.text(z.string().optional()),
+ recipientName: zfd.text(z.string().optional()),
+ teamId: zfd.text(z.string().optional()),
+});
/**
- * Saves a connect (Dream Connect) for a user.
- * Uses sender's userId as partition key to keep connects in correct partition.
+ * Create or update a connect via form submission
+ * Compatible with useActionState/useFormState
*
- * @param input - Contains userId and connectData
- * @returns Saved connect document
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
*/
-export const saveConnect = withAuth(async (user, input: SaveConnectInput) => {
+export const saveConnect = withAuth(async (user, prevState: SaveConnectState | null, formData: FormData): Promise => {
try {
- const { userId, connectData } = input;
-
- if (!connectData) {
- throw new Error('connectData is required');
- }
-
- // Use the connect's userId (sender's ID) as partition key
- const partitionUserId = connectData.userId || userId;
-
- if (!partitionUserId) {
- throw new Error('userId is required in connectData');
- }
+ // Validate form data
+ const validatedData = connectFormSchema.parse(formData);
+ const userId = user.id;
const db = getDatabaseClient();
- // Create the connect document
- const connectId = connectData.id
- ? String(connectData.id)
- : `connect_${partitionUserId}_${Date.now()}`;
+ // Create connect ID
+ const connectId = validatedData.id || `connect_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ // Save to database - match ConnectDocument structure
const document = {
id: connectId,
- userId: partitionUserId, // Always use sender's userId as partition key
- type: connectData.type || 'connect',
- withWhom: connectData.withWhom,
- withWhomId: connectData.withWhomId,
- when: connectData.when,
- notes: connectData.notes || '',
- status: connectData.status || 'pending',
- agenda: connectData.agenda,
- proposedWeeks: connectData.proposedWeeks || [],
- schedulingMethod: connectData.schedulingMethod,
- dreamId: connectData.dreamId || undefined,
- name: connectData.name,
- category: connectData.category,
- avatar: connectData.avatar,
- office: connectData.office,
- createdAt: connectData.createdAt || new Date().toISOString(),
- updatedAt: new Date().toISOString()
+ userId: userId,
+ connectType: validatedData.connectType,
+ connectDate: validatedData.connectDate,
+ notes: validatedData.notes,
+ recipientUserId: validatedData.recipientUserId,
+ recipientName: validatedData.recipientName,
+ teamId: validatedData.teamId,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
};
- await db.connects.upsertConnect(partitionUserId, document);
+ await db.connects.upsertConnect(userId, document);
- return createActionSuccess({
- id: connectId,
- connect: document
- });
+ // Revalidate to refresh context data
+ revalidatePath('/dream-connect');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { id: connectId },
+ };
} catch (error) {
console.error('Failed to save connect:', error);
- return handleActionError(error, 'Failed to save connect');
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ connectType: error.formErrors.fieldErrors.connectType as string[],
+ connectDate: error.formErrors.fieldErrors.connectDate as string[],
+ recipientName: error.formErrors.fieldErrors.recipientName as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
}
});
diff --git a/apps/web/services/dreams/deleteDream.ts b/apps/web/services/dreams/deleteDream.ts
new file mode 100644
index 0000000..0fc4201
--- /dev/null
+++ b/apps/web/services/dreams/deleteDream.ts
@@ -0,0 +1,56 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { withAuth } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+
+/**
+ * Delete a dream
+ * Non-form mutation with simple signature
+ *
+ * @param dreamId - ID of the dream to delete
+ * @returns Success response or throws error
+ */
+export async function deleteDream(dreamId: string) {
+ try {
+ const result = await withAuth(async (user) => {
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get existing dreams
+ const dreamsDoc = await db.dreams.getDreamsDocument(userId);
+ const existingDreams = dreamsDoc?.dreams || [];
+
+ // Filter out the dream
+ const updatedDreams = existingDreams.filter((d: any) => d.id !== dreamId);
+
+ // Save to database
+ const document = {
+ id: userId,
+ userId: userId,
+ dreams: updatedDreams,
+ weeklyGoalTemplates: dreamsDoc?.weeklyGoalTemplates || [],
+ yearVision: dreamsDoc?.yearVision || '',
+ createdAt: dreamsDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.dreams.upsertDreamsDocument(userId, document);
+
+ return { success: true, id: dreamId };
+ })({});
+
+ if (result.failed) {
+ throw new Error(result.errors._errors?.join(', ') || 'Failed to delete dream');
+ }
+
+ // Revalidate to refresh context data
+ revalidatePath('/dream-book');
+ revalidatePath('/dashboard');
+
+ return result;
+ } catch (error) {
+ console.error('Failed to delete dream:', error);
+ throw error;
+ }
+}
diff --git a/apps/web/services/dreams/index.ts b/apps/web/services/dreams/index.ts
index 758b20a..2f637f4 100644
--- a/apps/web/services/dreams/index.ts
+++ b/apps/web/services/dreams/index.ts
@@ -2,6 +2,15 @@
* Dreams service exports
* Barrel export for all dreams-related server actions
*/
+
+// Legacy bulk operations
export * from './saveDreams';
export * from './uploadDreamPicture';
+
+// Form actions (useActionState compatible)
+export * from './saveDream';
export * from './saveYearVision';
+
+// Non-form mutations (deletes, reorders)
+export * from './deleteDream';
+export * from './mutations';
diff --git a/apps/web/services/dreams/mutations.ts b/apps/web/services/dreams/mutations.ts
new file mode 100644
index 0000000..5db28f7
--- /dev/null
+++ b/apps/web/services/dreams/mutations.ts
@@ -0,0 +1,53 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { withAuth, createActionSuccess, handleActionError } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+import { Dream } from '@/lib/contexts/dreams/types';
+
+/**
+ * Reorder dreams (drag and drop)
+ * Non-form mutation with simple signature
+ *
+ * @param dreams - Reordered array of dreams
+ * @returns Success response
+ */
+export async function reorderDreams(dreams: Dream[]) {
+ try {
+ const result = await withAuth(async (user) => {
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get existing document to preserve other fields
+ const dreamsDoc = await db.dreams.getDreamsDocument(userId);
+
+ // Update document with reordered dreams
+ const document = {
+ id: userId,
+ userId: userId,
+ dreams: dreams,
+ weeklyGoalTemplates: dreamsDoc?.weeklyGoalTemplates || [],
+ yearVision: dreamsDoc?.yearVision || '',
+ createdAt: dreamsDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.dreams.upsertDreamsDocument(userId, document);
+
+ return createActionSuccess({ count: dreams.length });
+ })({});
+
+ if (result.failed) {
+ throw new Error(result.errors._errors?.join(', ') || 'Failed to reorder dreams');
+ }
+
+ // Revalidate to refresh context data
+ revalidatePath('/dream-book');
+ revalidatePath('/dashboard');
+
+ return result;
+ } catch (error) {
+ console.error('Failed to reorder dreams:', error);
+ return handleActionError(error, 'Failed to reorder dreams');
+ }
+}
diff --git a/apps/web/services/dreams/saveDream.ts b/apps/web/services/dreams/saveDream.ts
new file mode 100644
index 0000000..1a1fd22
--- /dev/null
+++ b/apps/web/services/dreams/saveDream.ts
@@ -0,0 +1,127 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth, createActionSuccess } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+import type { DreamBookEntry } from '@dreamspace/shared';
+
+/**
+ * Form action state for dream save
+ */
+export type SaveDreamState = {
+ success: boolean;
+ errors?: {
+ title?: string[];
+ category?: string[];
+ description?: string[];
+ _form?: string[];
+ };
+ data?: {
+ id: string;
+ };
+};
+
+/**
+ * Schema for dream form data
+ */
+const dreamFormSchema = zfd.formData({
+ id: zfd.text(z.string().optional()),
+ title: zfd.text(z.string().min(1, 'Title is required')),
+ category: zfd.text(z.string().optional()),
+ description: zfd.text(z.string().optional()),
+ imageUrl: zfd.text(z.string().optional()),
+ imagePrompt: zfd.text(z.string().optional()),
+ targetDate: zfd.text(z.string().optional()),
+});
+
+/**
+ * Create or update a dream via form submission
+ * Compatible with useActionState/useFormState
+ *
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
+ */
+export const saveDream = withAuth(async (user, prevState: SaveDreamState | null, formData: FormData): Promise => {
+ try {
+ // Validate form data
+ const validatedData = dreamFormSchema.parse(formData);
+
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get existing dreams document
+ const dreamsDoc = await db.dreams.getDreamsDocument(userId);
+ const existingDreams = dreamsDoc?.dreamBook || [];
+
+ // Create or update dream
+ const dreamId = validatedData.id || `dream_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ const dreamIndex = existingDreams.findIndex((d: DreamBookEntry) => d.id === dreamId);
+
+ const dreamData: DreamBookEntry = {
+ id: dreamId,
+ title: validatedData.title,
+ category: validatedData.category,
+ description: validatedData.description,
+ imageUrl: validatedData.imageUrl,
+ imagePrompt: validatedData.imagePrompt,
+ targetDate: validatedData.targetDate,
+ isCompleted: dreamIndex >= 0 ? existingDreams[dreamIndex].isCompleted : false,
+ createdAt: dreamIndex >= 0 ? existingDreams[dreamIndex].createdAt : new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ // Update dreams array
+ let updatedDreams: DreamBookEntry[];
+ if (dreamIndex >= 0) {
+ updatedDreams = [...existingDreams];
+ updatedDreams[dreamIndex] = dreamData;
+ } else {
+ updatedDreams = [...existingDreams, dreamData];
+ }
+
+ // Save to database - match DreamsDocument structure
+ const document = {
+ id: userId,
+ userId: userId,
+ dreamBook: updatedDreams,
+ weeklyGoalTemplates: dreamsDoc?.weeklyGoalTemplates || [],
+ createdAt: dreamsDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.dreams.upsertDreamsDocument(userId, document);
+
+ // Revalidate to refresh context data
+ revalidatePath('/dream-book');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { id: dreamId },
+ };
+ } catch (error) {
+ console.error('Failed to save dream:', error);
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ title: error.formErrors.fieldErrors.title as string[],
+ category: error.formErrors.fieldErrors.category as string[],
+ description: error.formErrors.fieldErrors.description as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
+ }
+});
diff --git a/apps/web/services/dreams/saveYearVision.ts b/apps/web/services/dreams/saveYearVision.ts
index 45d74be..efaa1e0 100644
--- a/apps/web/services/dreams/saveYearVision.ts
+++ b/apps/web/services/dreams/saveYearVision.ts
@@ -1,57 +1,91 @@
'use server';
-import { withAuth, createActionSuccess, handleActionError } from '@/lib/actions';
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth } from '@/lib/actions';
import { getDatabaseClient } from '@dreamspace/database';
-interface SaveYearVisionInput {
- userId: string;
- yearVision: string;
-}
+/**
+ * Form action state for year vision save
+ */
+export type SaveYearVisionState = {
+ success: boolean;
+ errors?: {
+ yearVision?: string[];
+ _form?: string[];
+ };
+ data?: {
+ yearVision: string;
+ };
+};
/**
- * Saves the year vision for a user.
- * Year vision is stored in the dreams document.
+ * Schema for year vision form data
+ */
+const yearVisionFormSchema = zfd.formData({
+ yearVision: zfd.text(z.string()),
+});
+
+/**
+ * Save year vision via form submission
+ * Compatible with useActionState/useFormState
*
- * @param input - Contains userId and yearVision text
- * @returns Success response
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
*/
-export const saveYearVision = withAuth(async (user, input: SaveYearVisionInput) => {
+export const saveYearVision = withAuth(async (user, prevState: SaveYearVisionState | null, formData: FormData): Promise => {
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');
- }
+ // Validate form data
+ const validatedData = yearVisionFormSchema.parse(formData);
+ const userId = user.id;
const db = getDatabaseClient();
- // Get existing dreams document or create new one
- const existingDoc = await db.dreams.getDreamsDocument(userId);
+ // Get existing document
+ const dreamsDoc = await db.dreams.getDreamsDocument(userId);
+ // Update document with new vision
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()
+ dreams: dreamsDoc?.dreams || [],
+ weeklyGoalTemplates: dreamsDoc?.weeklyGoalTemplates || [],
+ yearVision: validatedData.yearVision,
+ createdAt: dreamsDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
};
await db.dreams.upsertDreamsDocument(userId, document);
- return createActionSuccess({
- id: userId,
- message: 'Year vision saved successfully'
- });
+ // Revalidate to refresh context data
+ revalidatePath('/dream-book');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { yearVision: validatedData.yearVision },
+ };
} catch (error) {
console.error('Failed to save year vision:', error);
- return handleActionError(error, 'Failed to save year vision');
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ yearVision: error.formErrors.fieldErrors.yearVision as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
}
});
+
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");
}
});
diff --git a/apps/web/services/scoring/index.ts b/apps/web/services/scoring/index.ts
index 865cb65..eddb98a 100644
--- a/apps/web/services/scoring/index.ts
+++ b/apps/web/services/scoring/index.ts
@@ -2,6 +2,13 @@
* Scoring service exports
* Barrel export for all scoring-related server actions
*/
+
+// Get operations
export * from './getScoring';
-export * from './saveScoring';
export * from './getAllYearsScoring';
+
+// Form actions (useActionState compatible)
+export * from './saveScore';
+
+// Legacy operations
+export * from './saveScoring';
diff --git a/apps/web/services/scoring/saveScore.ts b/apps/web/services/scoring/saveScore.ts
new file mode 100644
index 0000000..6fc5074
--- /dev/null
+++ b/apps/web/services/scoring/saveScore.ts
@@ -0,0 +1,131 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth, createActionSuccess } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+import type { QuarterScore } from '@dreamspace/shared';
+
+/**
+ * Form action state for quarter score save
+ */
+export type SaveScoreState = {
+ success: boolean;
+ errors?: {
+ quarter?: string[];
+ score?: string[];
+ _form?: string[];
+ };
+ data?: {
+ id: string;
+ quarter: number;
+ };
+};
+
+/**
+ * Schema for quarter score form data
+ */
+const scoreFormSchema = zfd.formData({
+ year: zfd.numeric(z.number().int().min(2020)),
+ quarter: zfd.numeric(z.number().int().min(1).max(4)),
+ score: zfd.numeric(z.number().optional()),
+ notes: zfd.text(z.string().optional()),
+});
+
+/**
+ * Save or update a quarter score via form submission
+ * Compatible with useActionState/useFormState
+ *
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
+ */
+export const saveScore = withAuth(async (user, prevState: SaveScoreState | null, formData: FormData): Promise => {
+ try {
+ // Validate form data
+ const validatedData = scoreFormSchema.parse(formData);
+
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get existing scoring document for the year
+ const documentId = `${userId}_${validatedData.year}_scoring`;
+ let scoringDoc;
+ try {
+ scoringDoc = await db.scoring.getScoringDocument(userId, validatedData.year);
+ } catch (error: any) {
+ if (error.code !== 404) {
+ throw error;
+ }
+ // Document doesn't exist yet, will create new one
+ }
+
+ const existingQuarters = scoringDoc?.quarters || [];
+ const quarterIndex = existingQuarters.findIndex((q: QuarterScore) => q.quarter === validatedData.quarter);
+
+ const quarterData: QuarterScore = {
+ quarter: validatedData.quarter,
+ score: validatedData.score,
+ notes: validatedData.notes,
+ scoredAt: new Date().toISOString(),
+ };
+
+ // Update quarters array
+ let updatedQuarters: QuarterScore[];
+ if (quarterIndex >= 0) {
+ updatedQuarters = [...existingQuarters];
+ updatedQuarters[quarterIndex] = quarterData;
+ } else {
+ updatedQuarters = [...existingQuarters, quarterData];
+ }
+
+ // Calculate annual score (average of quarters)
+ const scores = updatedQuarters.filter(q => q.score !== undefined).map(q => q.score!);
+ const annualScore = scores.length > 0
+ ? scores.reduce((sum, s) => sum + s, 0) / scores.length
+ : undefined;
+
+ // Save to database - match ScoringDocument structure
+ const document = {
+ id: documentId,
+ userId: userId,
+ year: validatedData.year,
+ quarters: updatedQuarters,
+ annualScore,
+ createdAt: scoringDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.scoring.upsertScoring(userId, validatedData.year, document);
+
+ // Revalidate to refresh context data
+ revalidatePath('/scorecard');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { id: documentId, quarter: validatedData.quarter },
+ };
+ } catch (error) {
+ console.error('Failed to save score:', error);
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ quarter: error.formErrors.fieldErrors.quarter as string[],
+ score: error.formErrors.fieldErrors.score as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
+ }
+});
diff --git a/apps/web/services/teams/index.ts b/apps/web/services/teams/index.ts
index 24df1c1..b10925d 100644
--- a/apps/web/services/teams/index.ts
+++ b/apps/web/services/teams/index.ts
@@ -2,12 +2,18 @@
* Teams service exports
* Barrel export for all team-related server actions
*/
+
+// Get operations
export * from './getTeamMetrics';
export * from './getTeamRelationships';
-export * from './updateTeamInfo';
+export * from './getMeetingAttendance';
+
+// Form actions (useActionState compatible)
export * from './updateTeamName';
export * from './updateTeamMission';
+
+// Legacy operations
+export * from './updateTeamInfo';
export * from './updateTeamMeeting';
export * from './replaceTeamCoach';
-export * from './getMeetingAttendance';
export * from './saveMeetingAttendance';
diff --git a/apps/web/services/teams/updateTeamMission.ts b/apps/web/services/teams/updateTeamMission.ts
index cf8948e..c49c6ca 100644
--- a/apps/web/services/teams/updateTeamMission.ts
+++ b/apps/web/services/teams/updateTeamMission.ts
@@ -1,27 +1,49 @@
'use server';
-import { withCoachAuth, createActionSuccess, handleActionError } from '@/lib/actions';
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withCoachAuth, createActionSuccess } from '@/lib/actions';
import { getDatabaseClient } from '@dreamspace/database';
-interface UpdateTeamMissionInput {
- managerId: string;
- mission: string;
-}
+/**
+ * Form action state for team mission update
+ */
+export type UpdateTeamMissionState = {
+ success: boolean;
+ errors?: {
+ mission?: string[];
+ _form?: string[];
+ };
+ data?: {
+ managerId: string;
+ mission: string;
+ };
+};
/**
- * Updates a team's mission statement.
+ * Schema for team mission form data
+ */
+const teamMissionFormSchema = zfd.formData({
+ managerId: zfd.text(z.string()),
+ mission: zfd.text(z.string().min(1, 'Team mission is required')),
+});
+
+/**
+ * Update a team's mission statement via form submission
+ * Compatible with useActionState/useFormState
* Only coaches can update their own team mission.
*
- * @param input - Contains managerId and mission
- * @returns Updated team data
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
*/
-export const updateTeamMission = withCoachAuth(async (user, input: UpdateTeamMissionInput) => {
+export const updateTeamMission = withCoachAuth(async (user, prevState: UpdateTeamMissionState | null, formData: FormData): Promise => {
try {
- const { managerId, mission } = input;
+ // Validate form data
+ const validatedData = teamMissionFormSchema.parse(formData);
- if (!managerId) {
- throw new Error('Manager ID is required');
- }
+ const { managerId, mission } = validatedData;
// Verify the authenticated coach is modifying their own team
if (user.id !== managerId) {
@@ -38,20 +60,38 @@ export const updateTeamMission = withCoachAuth(async (user, input: UpdateTeamMis
const updatedTeam = {
...team,
- mission,
- lastModified: new Date().toISOString()
+ teamMission: mission,
+ updatedAt: new Date().toISOString(),
};
await db.teams.updateTeam(team.id, team.managerId, updatedTeam);
- return createActionSuccess({
- managerId,
- mission,
- teamName: team.teamName,
- lastModified: updatedTeam.lastModified
- });
+ // Revalidate to refresh context data
+ revalidatePath('/dream-team');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { managerId, mission },
+ };
} catch (error) {
console.error('Failed to update team mission:', error);
- return handleActionError(error, 'Failed to update team mission');
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ mission: error.formErrors.fieldErrors.mission as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
}
});
diff --git a/apps/web/services/teams/updateTeamName.ts b/apps/web/services/teams/updateTeamName.ts
index 692c1f5..d55797e 100644
--- a/apps/web/services/teams/updateTeamName.ts
+++ b/apps/web/services/teams/updateTeamName.ts
@@ -1,27 +1,49 @@
'use server';
-import { withCoachAuth, createActionSuccess, handleActionError } from '@/lib/actions';
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withCoachAuth, createActionSuccess } from '@/lib/actions';
import { getDatabaseClient } from '@dreamspace/database';
-interface UpdateTeamNameInput {
- managerId: string;
- teamName: string;
-}
+/**
+ * Form action state for team name update
+ */
+export type UpdateTeamNameState = {
+ success: boolean;
+ errors?: {
+ teamName?: string[];
+ _form?: string[];
+ };
+ data?: {
+ managerId: string;
+ teamName: string;
+ };
+};
/**
- * Updates a team's name.
+ * Schema for team name form data
+ */
+const teamNameFormSchema = zfd.formData({
+ managerId: zfd.text(z.string()),
+ teamName: zfd.text(z.string().min(1, 'Team name is required')),
+});
+
+/**
+ * Update a team's name via form submission
+ * Compatible with useActionState/useFormState
* Only coaches can update their own team name.
*
- * @param input - Contains managerId and new teamName
- * @returns Updated team data
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
*/
-export const updateTeamName = withCoachAuth(async (user, input: UpdateTeamNameInput) => {
+export const updateTeamName = withCoachAuth(async (user, prevState: UpdateTeamNameState | null, formData: FormData): Promise => {
try {
- const { managerId, teamName } = input;
+ // Validate form data
+ const validatedData = teamNameFormSchema.parse(formData);
- if (!managerId || !teamName) {
- throw new Error('Manager ID and team name are required');
- }
+ const { managerId, teamName } = validatedData;
// Verify the authenticated coach is modifying their own team
if (user.id !== managerId) {
@@ -39,18 +61,37 @@ export const updateTeamName = withCoachAuth(async (user, input: UpdateTeamNameIn
const updatedTeam = {
...team,
teamName,
- lastModified: new Date().toISOString()
+ updatedAt: new Date().toISOString(),
};
await db.teams.updateTeam(team.id, team.managerId, updatedTeam);
- return createActionSuccess({
- managerId,
- teamName,
- lastModified: updatedTeam.lastModified
- });
+ // Revalidate to refresh context data
+ revalidatePath('/dream-team');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { managerId, teamName },
+ };
} catch (error) {
console.error('Failed to update team name:', error);
- return handleActionError(error, 'Failed to update team name');
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ teamName: error.formErrors.fieldErrors.teamName as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
}
});
diff --git a/apps/web/services/users/index.ts b/apps/web/services/users/index.ts
index bddbdf3..d549fbb 100644
--- a/apps/web/services/users/index.ts
+++ b/apps/web/services/users/index.ts
@@ -2,11 +2,18 @@
* User service exports
* Barrel export for all user-related server actions
*/
+
+// Get operations
export * from './getUserProfile';
export * from './getUserData';
+export * from './getAllUsers';
+
+// Form actions (useActionState compatible)
+export * from './updateProfile';
+
+// Legacy operations
export * from './saveUserData';
export * from './updateUserProfile';
-export * from './getAllUsers';
export * from './assignUserToCoach';
export * from './promoteUserToCoach';
export * from './unassignUserFromTeam';
diff --git a/apps/web/services/users/updateProfile.ts b/apps/web/services/users/updateProfile.ts
new file mode 100644
index 0000000..0ccd12f
--- /dev/null
+++ b/apps/web/services/users/updateProfile.ts
@@ -0,0 +1,114 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth, createActionSuccess } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+
+/**
+ * Form action state for profile update
+ */
+export type UpdateProfileState = {
+ success: boolean;
+ errors?: {
+ name?: string[];
+ email?: string[];
+ _form?: string[];
+ };
+ data?: {
+ id: string;
+ };
+};
+
+/**
+ * Schema for profile form data
+ */
+const profileFormSchema = zfd.formData({
+ displayName: zfd.text(z.string().optional()),
+ name: zfd.text(z.string().optional()),
+ email: zfd.text(z.string().email().optional()),
+ region: zfd.text(z.string().optional()),
+ office: zfd.text(z.string().optional()),
+ title: zfd.text(z.string().optional()),
+ department: zfd.text(z.string().optional()),
+ cardBackgroundImage: zfd.text(z.string().optional()),
+});
+
+/**
+ * Update user profile via form submission
+ * Compatible with useActionState/useFormState
+ *
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
+ */
+export const updateProfile = withAuth(async (user, prevState: UpdateProfileState | null, formData: FormData): Promise => {
+ try {
+ // Validate form data
+ const validatedData = profileFormSchema.parse(formData);
+
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get existing user document
+ const existingDocument = await db.users.getUserProfile(userId);
+
+ // Create updated document with ONLY profile data (6-container architecture)
+ const updatedDocument = {
+ id: userId,
+ userId: userId,
+ // Basic profile fields
+ name: validatedData.displayName || validatedData.name || existingDocument?.name || 'Unknown User',
+ displayName: validatedData.displayName || validatedData.name || existingDocument?.displayName,
+ firstName: validatedData.displayName?.split(' ')[0] || existingDocument?.firstName,
+ lastName: validatedData.displayName?.split(' ').slice(1).join(' ') || existingDocument?.lastName,
+ email: validatedData.email || existingDocument?.email || '',
+ region: validatedData.region || existingDocument?.region,
+ photoUrl: existingDocument?.photoUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(validatedData.displayName || validatedData.name || 'User')}&background=6366f1&color=fff&size=100`,
+ // Additional profile fields
+ title: validatedData.title || existingDocument?.title || '',
+ department: validatedData.department || existingDocument?.department || '',
+ officeLocation: validatedData.office || existingDocument?.officeLocation,
+ // SECURITY: Never trust client-supplied roles
+ isCoach: existingDocument?.isCoach ?? false,
+ isActive: existingDocument?.isActive !== false,
+ teamId: existingDocument?.teamId,
+ onboardingComplete: existingDocument?.onboardingComplete ?? false,
+ lastLogin: existingDocument?.lastLogin,
+ createdAt: existingDocument?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.users.upsertUserProfile(userId, updatedDocument);
+
+ // Revalidate to refresh context data
+ revalidatePath('/people');
+ revalidatePath('/dashboard');
+
+ return {
+ success: true,
+ data: { id: userId },
+ };
+ } catch (error) {
+ console.error('Failed to update profile:', error);
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ name: error.formErrors.fieldErrors.name as string[],
+ email: error.formErrors.fieldErrors.email as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
+ }
+});
diff --git a/apps/web/services/weeks/index.ts b/apps/web/services/weeks/index.ts
index 25cc28e..f4f1492 100644
--- a/apps/web/services/weeks/index.ts
+++ b/apps/web/services/weeks/index.ts
@@ -2,8 +2,15 @@
* Weeks service exports
* Barrel export for all week-related server actions
*/
+
+// Get operations
export * from './getCurrentWeek';
export * from './getPastWeeks';
+
+// Form actions (useActionState compatible)
+export * from './saveGoal';
+
+// Legacy operations
export * from './saveCurrentWeek';
export * from './syncCurrentWeek';
export * from './archiveWeek';
diff --git a/apps/web/services/weeks/saveGoal.ts b/apps/web/services/weeks/saveGoal.ts
new file mode 100644
index 0000000..ea08610
--- /dev/null
+++ b/apps/web/services/weeks/saveGoal.ts
@@ -0,0 +1,152 @@
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { z } from 'zod';
+import { zfd } from 'zod-form-data';
+import { withAuth, createActionSuccess } from '@/lib/actions';
+import { getDatabaseClient } from '@dreamspace/database';
+import type { WeekGoal } from '@dreamspace/shared';
+
+/**
+ * Form action state for goal save
+ */
+export type SaveGoalState = {
+ success: boolean;
+ errors?: {
+ title?: string[];
+ category?: string[];
+ description?: string[];
+ _form?: string[];
+ };
+ data?: {
+ id: string;
+ };
+};
+
+/**
+ * Schema for goal form data
+ */
+const goalFormSchema = zfd.formData({
+ id: zfd.text(z.string().optional()),
+ weekStartDate: zfd.text(z.string()),
+ title: zfd.text(z.string().min(1, 'Title is required')),
+ category: zfd.text(z.string().min(1, 'Category is required')),
+ description: zfd.text(z.string().optional()),
+ templateId: zfd.text(z.string().optional()),
+ goalType: zfd.text(z.string().optional()),
+ targetValue: zfd.numeric(z.number().optional()),
+ currentValue: zfd.numeric(z.number().optional()),
+ unit: zfd.text(z.string().optional()),
+});
+
+/**
+ * Create or update a weekly goal via form submission
+ * Compatible with useActionState/useFormState
+ *
+ * @param prevState - Previous form state
+ * @param formData - Form data from submission
+ * @returns Form state with success/error information
+ */
+export const saveGoal = withAuth(async (user, prevState: SaveGoalState | null, formData: FormData): Promise => {
+ try {
+ // Validate form data
+ const validatedData = goalFormSchema.parse(formData);
+
+ const userId = user.id;
+ const db = getDatabaseClient();
+
+ // Get current week document
+ const weekDoc = await db.weeks.getCurrentWeek(userId);
+ const existingGoals = weekDoc?.goals || [];
+
+ // Create or update goal
+ const goalId = validatedData.id || `goal_${Date.now()}_${Math.random().toString(36).slice(2)}`;
+ const goalIndex = existingGoals.findIndex((g: WeekGoal) => g.id === goalId);
+
+ const goalData: WeekGoal = {
+ id: goalId,
+ title: validatedData.title,
+ category: validatedData.category,
+ description: validatedData.description,
+ templateId: validatedData.templateId,
+ goalType: validatedData.goalType,
+ targetValue: validatedData.targetValue,
+ currentValue: validatedData.currentValue ?? 0,
+ unit: validatedData.unit,
+ isCompleted: goalIndex >= 0 ? existingGoals[goalIndex].isCompleted : false,
+ completedAt: goalIndex >= 0 ? existingGoals[goalIndex].completedAt : undefined,
+ notes: goalIndex >= 0 ? existingGoals[goalIndex].notes : undefined,
+ dailyProgress: goalIndex >= 0 ? existingGoals[goalIndex].dailyProgress : [],
+ };
+
+ // Update goals array
+ let updatedGoals: WeekGoal[];
+ if (goalIndex >= 0) {
+ updatedGoals = [...existingGoals];
+ updatedGoals[goalIndex] = goalData;
+ } else {
+ updatedGoals = [...existingGoals, goalData];
+ }
+
+ // Calculate week number and year from weekStartDate
+ const weekStart = new Date(validatedData.weekStartDate);
+ const weekEnd = new Date(weekStart);
+ weekEnd.setDate(weekEnd.getDate() + 6);
+
+ // Save to database - match CurrentWeekDocument structure
+ const document = {
+ id: userId,
+ userId: userId,
+ weekStartDate: validatedData.weekStartDate,
+ weekEndDate: weekEnd.toISOString().split('T')[0],
+ goals: updatedGoals,
+ weekNumber: getWeekNumber(weekStart),
+ year: weekStart.getFullYear(),
+ createdAt: weekDoc?.createdAt || new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ };
+
+ await db.weeks.upsertCurrentWeek(userId, document);
+
+ // Revalidate to refresh context data
+ revalidatePath('/dashboard');
+ revalidatePath('/scorecard');
+
+ return {
+ success: true,
+ data: { id: goalId },
+ };
+ } catch (error) {
+ console.error('Failed to save goal:', error);
+
+ if (error instanceof z.ZodError) {
+ return {
+ success: false,
+ errors: {
+ title: error.formErrors.fieldErrors.title as string[],
+ category: error.formErrors.fieldErrors.category as string[],
+ description: error.formErrors.fieldErrors.description as string[],
+ _form: error.formErrors.formErrors,
+ },
+ };
+ }
+
+ return {
+ success: false,
+ errors: {
+ _form: ['An unexpected error occurred'],
+ },
+ };
+ }
+});
+
+/**
+ * Get ISO week number
+ */
+function getWeekNumber(date: Date): number {
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
+ const dayNum = d.getUTCDay() || 7;
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum);
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
+}
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