From c2cfc66222bb79f34c656e17b72be9a156edcc59 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Dec 2025 00:00:30 +0000 Subject: [PATCH] feat: Add session flow management and tab Co-authored-by: johnarpaul --- app/components/MainSlideover.tsx | 630 ++++++++++++++++++++++++++++++- instant.schema.ts | 14 + 2 files changed, 642 insertions(+), 2 deletions(-) diff --git a/app/components/MainSlideover.tsx b/app/components/MainSlideover.tsx index bfba1ec..6fc03fc 100644 --- a/app/components/MainSlideover.tsx +++ b/app/components/MainSlideover.tsx @@ -60,6 +60,23 @@ interface Session { rewardsClaimedAt?: string | number; timeRemaining?: number; type?: "focus" | "break"; + sessionFlowId?: string; + flowIndex?: number; +} + +interface SessionFlowTimer { + type: "focus" | "break"; + minutes: number; +} + +interface SessionFlow { + id: string; + name: string; + timers: SessionFlowTimer[]; + createdAt: string | number; + lastUsedAt?: string | number; + currentTimerIndex?: number; + isActive?: boolean; } interface Block { @@ -377,6 +394,7 @@ SupporterTab.displayName = "SupporterTab"; // Tab type definition type TabType = | "timer" + | "session" | "sessions" | "blocks" | "packs" @@ -474,6 +492,15 @@ const Tabs = memo( setIsExpanded(true); }} /> + { + setActiveTab("session"); + setIsExpanded(true); + }} + /> (""); const [activeTab, setActiveTab] = useState< | "timer" + | "session" | "sessions" | "blocks" | "packs" @@ -612,6 +640,14 @@ const MainSlideover = memo(function MainSlideover({ useState(null); const [pausedAt, setPausedAt] = useState(null); const [lowAnimationMode, setLowAnimationMode] = useState(false); + + // Session flow state + const [sessionFlows, setSessionFlows] = useState([]); + const [activeSessionFlow, setActiveSessionFlow] = useState(null); + const [currentFlowIndex, setCurrentFlowIndex] = useState(0); + const [editingFlow, setEditingFlow] = useState(null); + const [newFlowName, setNewFlowName] = useState(""); + const [newFlowTimers, setNewFlowTimers] = useState([]); const { user, profile, sessionId } = useAuth(); const effectiveSessionId = user?.id || sessionId || browserSessionId; @@ -699,12 +735,36 @@ const MainSlideover = memo(function MainSlideover({ }, }, }, + sessionFlows: user + ? { + $: { + where: { + "user.id": user.id, + }, + }, + } + : undefined, } : null ); const sessions = data?.sessions || []; const unplacedBlocks = data?.blocks || []; + const queriedSessionFlows = data?.sessionFlows || []; + + // Update sessionFlows state when data changes + useEffect(() => { + if (queriedSessionFlows) { + setSessionFlows(queriedSessionFlows as SessionFlow[]); + + // Check if there's an active session flow and restore it + const activeFlow = queriedSessionFlows.find((f: any) => f.isActive); + if (activeFlow && !activeSessionFlow) { + setActiveSessionFlow(activeFlow as SessionFlow); + setCurrentFlowIndex(activeFlow.currentTimerIndex || 0); + } + } + }, [queriedSessionFlows]); // Memoize expensive calculations const sessionsWithUnclaimedRewards = useMemo(() => { @@ -909,7 +969,9 @@ const MainSlideover = memo(function MainSlideover({ const startTimer = async ( type?: "focus" | "break", - durationMinutes?: number + durationMinutes?: number, + sessionFlowId?: string, + flowIndex?: number ) => { const timerType = type || timerMode; @@ -927,7 +989,7 @@ const MainSlideover = memo(function MainSlideover({ const minutes = durationMinutes || timerMinutes; const newSessionId = id(); - const newSession = { + const newSession: any = { sessionId: effectiveSessionId, createdAt: Date.now(), timeInSeconds: minutes * 60, @@ -935,6 +997,12 @@ const MainSlideover = memo(function MainSlideover({ type: timerType as "focus" | "break", }; + // Add session flow tracking if provided + if (sessionFlowId) { + newSession.sessionFlowId = sessionFlowId; + newSession.flowIndex = flowIndex; + } + // Create session with user link if authenticated if (user) { await db.transact( @@ -1005,6 +1073,155 @@ const MainSlideover = memo(function MainSlideover({ setIsPaused(false); }; + // Session flow functions + const calculateSessionRewards = (timers: SessionFlowTimer[]) => { + const focusTimers = timers.filter(t => t.type === "focus"); + const totalPacks = focusTimers.length; + const totalBlocks = totalPacks * 3; // Each pack has 3 blocks + const totalMinutes = timers.reduce((sum, t) => sum + t.minutes, 0); + const totalFocusMinutes = focusTimers.reduce((sum, t) => sum + t.minutes, 0); + + return { + totalPacks, + totalBlocks, + totalMinutes, + totalFocusMinutes, + focusTimerCount: focusTimers.length, + breakTimerCount: timers.filter(t => t.type === "break").length, + }; + }; + + const createSessionFlow = async () => { + if (!user || !newFlowName.trim() || newFlowTimers.length === 0) { + alert("Please provide a flow name and add at least one timer!"); + return; + } + + const newFlowId = id(); + const newFlow = { + name: newFlowName.trim(), + timers: newFlowTimers, + createdAt: Date.now(), + }; + + await db.transact( + db.tx.sessionFlows[newFlowId].update(newFlow).link({ + user: user.id, + }) + ); + + // Reset form + setNewFlowName(""); + setNewFlowTimers([]); + setEditingFlow(null); + }; + + const updateSessionFlow = async (flowId: string, updates: Partial) => { + if (!user) return; + + await db.transact( + db.tx.sessionFlows[flowId].update(updates) + ); + }; + + const deleteSessionFlow = async (flowId: string) => { + if (!user) return; + + await db.transact( + db.tx.sessionFlows[flowId].delete() + ); + }; + + const startSessionFlow = async (flow: SessionFlow) => { + if (!user || !profile) { + alert("Please complete your profile setup first!"); + return; + } + + if (flow.timers.length === 0) { + alert("This session flow has no timers!"); + return; + } + + // Update flow to mark as active and reset index + await updateSessionFlow(flow.id, { + isActive: true, + currentTimerIndex: 0, + lastUsedAt: Date.now(), + }); + + setActiveSessionFlow(flow); + setCurrentFlowIndex(0); + + // Start the first timer + const firstTimer = flow.timers[0]; + await startTimer(firstTimer.type, firstTimer.minutes, flow.id, 0); + }; + + const nextTimerInFlow = async () => { + if (!activeSessionFlow || !user) return; + + const nextIndex = currentFlowIndex + 1; + + if (nextIndex >= activeSessionFlow.timers.length) { + // Session flow complete! + await updateSessionFlow(activeSessionFlow.id, { + isActive: false, + currentTimerIndex: 0, + }); + + setActiveSessionFlow(null); + setCurrentFlowIndex(0); + setActiveSession(null); + setRemainingTime(0); + + alert("Session flow complete! Great work! 🎉"); + return; + } + + // Update flow index + await updateSessionFlow(activeSessionFlow.id, { + currentTimerIndex: nextIndex, + }); + + setCurrentFlowIndex(nextIndex); + + // Start next timer + const nextTimer = activeSessionFlow.timers[nextIndex]; + await startTimer(nextTimer.type, nextTimer.minutes, activeSessionFlow.id, nextIndex); + }; + + const cancelSessionFlow = async () => { + if (!activeSessionFlow || !user) return; + + await updateSessionFlow(activeSessionFlow.id, { + isActive: false, + currentTimerIndex: 0, + }); + + setActiveSessionFlow(null); + setCurrentFlowIndex(0); + cancelTimer(); + }; + + const addTimerToNewFlow = (type: "focus" | "break", minutes: number) => { + setNewFlowTimers([...newFlowTimers, { type, minutes }]); + }; + + const removeTimerFromNewFlow = (index: number) => { + setNewFlowTimers(newFlowTimers.filter((_, i) => i !== index)); + }; + + const moveTimerInNewFlow = (index: number, direction: "up" | "down") => { + const newTimers = [...newFlowTimers]; + const targetIndex = direction === "up" ? index - 1 : index + 1; + + if (targetIndex < 0 || targetIndex >= newTimers.length) return; + + [newTimers[index], newTimers[targetIndex]] = [newTimers[targetIndex], newTimers[index]]; + setNewFlowTimers(newTimers); + }; + const claimReward = async (session: Session) => { if (session.rewardsClaimedAt) return; @@ -1582,6 +1799,415 @@ const MainSlideover = memo(function MainSlideover({ )} + {/* Session Flow Tab */} + {activeTab === "session" && ( +
+ {!user ? ( +
+

+ Sign In Required +

+

+ Sign in to create and use session flows +

+
+ ) : activeSessionFlow ? ( + /* Active Session Flow */ +
+
+

+ {activeSessionFlow.name} +

+
+
+ + Progress: + + + {currentFlowIndex + 1} / {activeSessionFlow.timers.length} + +
+
+
+
+
+
+ + {!activeSession?.completedAt && remainingTime > 0 ? ( +
+
+ {activeSessionFlow.timers[currentFlowIndex]?.type === "break" ? "Break Timer" : "Focus Timer"} ({currentFlowIndex + 1}/{activeSessionFlow.timers.length}) +
+ +
+ + +
+
+ ) : activeSession?.completedAt ? ( +
+ +

+ Timer Complete! +

+ {currentFlowIndex < activeSessionFlow.timers.length - 1 ? ( + <> +

+ Ready for the next timer in your flow? +

+
+ {activeSession.type === "focus" && !activeSession.rewardsClaimedAt && ( + + )} + +
+ + ) : ( + <> +

+ You've completed the entire session flow! 🎉 +

+
+ {activeSession.type === "focus" && !activeSession.rewardsClaimedAt && ( + + )} + +
+ + )} +
+ ) : null} + + {/* Upcoming timers */} +
+

+ Remaining Timers +

+
+ {activeSessionFlow.timers.slice(currentFlowIndex + 1).map((timer, idx) => ( +
+ + {timer.type} + + + {timer.minutes} min + +
+ ))} +
+
+
+ ) : ( + /* Session Flow Management */ +
+ {/* Create New Flow Section */} +
+

+ Create New Session Flow +

+ +
+
+ + setNewFlowName(e.target.value)} + placeholder="e.g., Deep Work Session" + className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 dark:bg-neutral-900 dark:text-neutral-200" + /> +
+ +
+ +
+ + +
+
+ + {newFlowTimers.length > 0 && ( +
+ +
+ {newFlowTimers.map((timer, idx) => ( +
+ + {idx + 1}. + + + {timer.type} + + + {timer.minutes} min + +
+ + + +
+
+ ))} +
+ + {/* Reward Calculation */} + {(() => { + const rewards = calculateSessionRewards(newFlowTimers); + return ( +
+

+ Potential Rewards +

+
+
+ Total Time: +

{rewards.totalMinutes} min

+
+
+ Focus Time: +

{rewards.totalFocusMinutes} min

+
+
+ Packs: +

{rewards.totalPacks} packs

+
+
+ Blocks: +

{rewards.totalBlocks} blocks

+
+
+
+ ); + })()} + + +
+ )} +
+
+ + {/* Saved Session Flows */} + {sessionFlows.length > 0 && ( +
+

+ Your Session Flows +

+
+ {sessionFlows.map((flow) => { + const rewards = calculateSessionRewards(flow.timers); + return ( +
+
+

+ {flow.name} +

+ +
+ +
+ {flow.timers.length} timers • {rewards.totalMinutes} min total • {rewards.totalPacks} packs +
+ +
+ {flow.timers.map((timer, idx) => ( + + {timer.minutes}m + + ))} +
+ + +
+ ); + })} +
+
+ )} +
+ )} +
+ )} + {/* Sessions Tab */} {/* {activeTab === 'sessions' && (
diff --git a/instant.schema.ts b/instant.schema.ts index ac9c77c..e8e40f3 100644 --- a/instant.schema.ts +++ b/instant.schema.ts @@ -38,6 +38,16 @@ const _schema = i.schema({ rewardsClaimedAt: i.date().optional(), cancelledAt: i.date().optional().indexed(), type: i.string().optional().indexed(), // 'focus' or 'break' + sessionFlowId: i.string().optional().indexed(), // Reference to sessionFlow if part of a flow + flowIndex: i.number().optional(), // Index in the flow sequence + }), + sessionFlows: i.entity({ + name: i.string(), + timers: i.json(), // Array of {type: 'focus' | 'break', minutes: number} + createdAt: i.date(), + lastUsedAt: i.date().optional(), + currentTimerIndex: i.number().optional(), // Track progress through the flow + isActive: i.boolean().optional(), // Whether this flow is currently running }), }, links: { @@ -53,6 +63,10 @@ const _schema = i.schema({ forward: { on: 'blocks', has: 'one', label: 'user' }, reverse: { on: '$users', has: 'many', label: 'blocks' } }, + userSessionFlows: { + forward: { on: 'sessionFlows', has: 'one', label: 'user' }, + reverse: { on: '$users', has: 'many', label: 'sessionFlows' } + }, }, rooms: {}, });