'use client'; import Link from 'next/link'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useMediaCatalog, getSceneStageBackgroundStyle } from '@/entities/media'; import { usePlanTier } from '@/entities/plan'; import { getSceneById, SCENE_THEMES } from '@/entities/scene'; import { SOUND_PRESETS } from '@/entities/session'; import { PaywallSheetContent } from '@/features/paywall-sheet'; import { focusSessionApi, type FocusSession } from '@/features/focus-session/api/focusSessionApi'; import { useFocusStats, type ReviewCarryHint } from '@/features/stats'; import { copy } from '@/shared/i18n'; import { cn } from '@/shared/lib/cn'; import { ATMOSPHERE_OPTIONS, ENTRY_DURATION_SUGGESTIONS, findAtmosphereOptionForSelection, getAtmosphereOptionById, getRecommendedDurationMinutes, getTimerPresetMetaById, parseDurationMinutes, resolveNearestTimerPreset, sanitizeDurationDraft, } from '../model/atmosphereEntry'; import { AppAtmosphereEntryShell } from './AppAtmosphereEntryShell'; const DEFAULT_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id; const DEFAULT_SOUND_ID = SOUND_PRESETS.find((preset) => preset.id === 'forest-birds')?.id ?? SOUND_PRESETS[0].id; const DEFAULT_TIMER_ID = '50-10'; const REVIEW_ENTRY_PRESETS = { 'forest-50-10': { sceneId: DEFAULT_SCENE_ID, soundPresetId: DEFAULT_SOUND_ID, timerPresetId: DEFAULT_TIMER_ID, label: '숲 · Forest Birds', }, } as const; const DEFAULT_ATMOSPHERE = findAtmosphereOptionForSelection(DEFAULT_SCENE_ID, DEFAULT_SOUND_ID) ?? ATMOSPHERE_OPTIONS[0]; const entryCopy = { eyebrow: 'VibeRoom', goalPlaceholder: 'e.g. Write the first draft', durationLabel: 'Estimated Time', durationPlaceholder: 'e.g. 70', durationHelper: 'Set a realistic time to accomplish this goal.', startNow: 'Begin Session', startLoading: 'Entering...', atmosphereTitle: 'Atmosphere', atmosphereBody: 'Background and sound play together. Choose an atmosphere to dive into deep focus.', loadFailed: 'Failed to load session state. You can still start a new one.', reviewEyebrow: 'Weekly Review', reviewTitle: 'Take a quick look at your weekly review?', reviewCta: 'View Review', reviewHelper: 'A brief look back before you start.', reviewTitlePro: 'Revisit a flow that worked well for you?', reviewCtaPro: 'View My Flow', reviewHelperPro: 'Check your best rituals and carry-forwards.', reviewReturnEyebrow: 'From your recent review', reviewReturnTitleSteady: 'Keep the rhythm that worked well this week.', reviewReturnTitleSmaller: 'Try setting a smaller goal this time.', reviewReturnTitleClosure: 'Decide where to wrap up before you start.', reviewReturnTitleStart: 'Your focus right now: just start one more session.', reviewReturnBodySteady: 'Set your own goal, but maintain the light entry rhythm.', reviewReturnBodySmaller: 'Instead of extending time, a smaller goal makes it easier to keep going.', reviewReturnBodyClosure: 'Think about where to close the block first to finish strong.', reviewReturnBodyStart: 'Just aim to open one more short session to build momentum.', reviewReturnRitualLabel: 'Recommended Ritual · Forest · Forest Birds', paywallLead: 'Calm Session OS PRO', paywallBody: 'Pro enables faster rituals and deeper reviews for seamless entry and return.', }; const reviewCarryCopyByHint: Record< ReviewCarryHint, { title: string; body: string } > = { steady: { title: entryCopy.reviewReturnTitleSteady, body: entryCopy.reviewReturnBodySteady, }, smaller: { title: entryCopy.reviewReturnTitleSmaller, body: entryCopy.reviewReturnBodySmaller, }, closure: { title: entryCopy.reviewReturnTitleClosure, body: entryCopy.reviewReturnBodyClosure, }, start: { title: entryCopy.reviewReturnTitleStart, body: entryCopy.reviewReturnBodyStart, }, }; export const FocusDashboardWidget = () => { const router = useRouter(); const searchParams = useSearchParams(); const { plan, isPro, setPlan } = usePlanTier(); const { sceneAssetMap } = useMediaCatalog(); const { summary: weeklySummary } = useFocusStats(); const reviewEntryPreset = searchParams.get('entryPreset'); const reviewEntryPresetConfig = useMemo(() => { if (!reviewEntryPreset) { return null; } return REVIEW_ENTRY_PRESETS[reviewEntryPreset as keyof typeof REVIEW_ENTRY_PRESETS] ?? null; }, [reviewEntryPreset]); const initialAtmosphere = useMemo(() => { return ( findAtmosphereOptionForSelection( reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID, reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID, ) ?? DEFAULT_ATMOSPHERE ); }, [reviewEntryPresetConfig]); const initialDurationMinutes = useMemo(() => { if (reviewEntryPresetConfig) { return getTimerPresetMetaById(reviewEntryPresetConfig.timerPresetId).focusMinutes; } return getRecommendedDurationMinutes(initialAtmosphere); }, [initialAtmosphere, reviewEntryPresetConfig]); const [goalDraft, setGoalDraft] = useState(''); const [durationDraft, setDurationDraft] = useState(() => String(initialDurationMinutes)); const [selectedAtmosphereId, setSelectedAtmosphereId] = useState(initialAtmosphere.id); const [hasEditedDuration, setHasEditedDuration] = useState(false); const [isStartingSession, setIsStartingSession] = useState(false); const [paywallSource, setPaywallSource] = useState(null); const [currentSession, setCurrentSession] = useState(null); const [isCheckingSession, setIsCheckingSession] = useState(true); const [sessionLookupError, setSessionLookupError] = useState(null); const goalInputRef = useRef(null); const selectedAtmosphere = useMemo( () => getAtmosphereOptionById(selectedAtmosphereId), [selectedAtmosphereId], ); const rawDurationValue = useMemo(() => { const digitsOnly = durationDraft.replace(/[^\d]/g, ''); if (!digitsOnly) { return null; } const parsed = Number(digitsOnly); return Number.isFinite(parsed) ? parsed : null; }, [durationDraft]); const parsedDurationMinutes = parseDurationMinutes(durationDraft); const resolvedTimerPreset = useMemo(() => { const targetMinutes = parsedDurationMinutes ?? getRecommendedDurationMinutes(selectedAtmosphere); return resolveNearestTimerPreset(targetMinutes); }, [parsedDurationMinutes, selectedAtmosphere]); const activeScene = useMemo(() => { return getSceneById(currentSession?.sceneId ?? selectedAtmosphere.sceneId) ?? SCENE_THEMES[0]; }, [currentSession?.sceneId, selectedAtmosphere.sceneId]); const trimmedGoal = goalDraft.trim(); const canStart = trimmedGoal.length > 0 && parsedDurationMinutes !== null && !isStartingSession && !currentSession; const hasEnoughWeeklyData = weeklySummary.last7Days.startedSessions >= 3 && (weeklySummary.last7Days.completedSessions >= 2 || weeklySummary.recovery.pausedSessions > 0); const reviewSource = searchParams.get('review'); const reviewCarryHint = searchParams.get('carryHint'); const normalizedReviewCarryHint: ReviewCarryHint | null = reviewCarryHint === 'steady' || reviewCarryHint === 'smaller' || reviewCarryHint === 'closure' || reviewCarryHint === 'start' ? reviewCarryHint : null; const isReviewReturn = reviewSource === 'weekly' && normalizedReviewCarryHint !== null; const reviewReturnCopy = normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null; const reviewReturnRitualLabel = isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null; const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle; const durationHelper = rawDurationValue !== null && rawDurationValue < 5 ? 'Please enter at least 5 minutes.' : parsedDurationMinutes === null ? 'Please enter the estimated duration in minutes.' : entryCopy.durationHelper; const hasCurrentSession = Boolean(currentSession); useEffect(() => { let cancelled = false; const loadCurrentSession = async () => { setIsCheckingSession(true); try { const session = await focusSessionApi.getCurrentSession(); if (cancelled) { return; } setCurrentSession(session); setSessionLookupError(null); } catch (error) { if (cancelled) { return; } setCurrentSession(null); setSessionLookupError(error instanceof Error ? error.message : entryCopy.loadFailed); } finally { if (!cancelled) { setIsCheckingSession(false); } } }; void loadCurrentSession(); return () => { cancelled = true; }; }, []); useEffect(() => { if (!isCheckingSession && hasCurrentSession) { router.replace('/space'); } }, [hasCurrentSession, isCheckingSession, router]); const openPaywall = () => { if (!isPro) { setPaywallSource('app-entry-plan-pill'); } }; const handleDurationChange = (value: string) => { setDurationDraft(sanitizeDurationDraft(value)); setHasEditedDuration(true); }; const handleSelectDuration = (minutes: number) => { setDurationDraft(String(minutes)); setHasEditedDuration(true); }; const handleSelectAtmosphere = (atmosphereId: string) => { const nextAtmosphere = getAtmosphereOptionById(atmosphereId); setSelectedAtmosphereId(nextAtmosphere.id); if (!hasEditedDuration) { setDurationDraft(String(getRecommendedDurationMinutes(nextAtmosphere))); } }; const handleStartSession = async () => { if (!trimmedGoal || isStartingSession || currentSession) { if (!trimmedGoal) { goalInputRef.current?.focus(); } return; } if (parsedDurationMinutes === null) { return; } setIsStartingSession(true); try { await focusSessionApi.startSession({ goal: trimmedGoal, microStep: null, sceneId: selectedAtmosphere.sceneId, soundPresetId: selectedAtmosphere.soundPresetId, timerPresetId: resolvedTimerPreset.id, entryPoint: 'space-setup', }); router.push('/space'); return; } catch (error) { const message = error instanceof Error ? error.message : entryCopy.loadFailed; setSessionLookupError(message); try { const session = await focusSessionApi.getCurrentSession(); if (session) { setCurrentSession(session); } } catch (syncError) { console.error('Failed to sync current session after /app start failure', syncError); } console.error('Failed to start focus session from /app', error); } setIsStartingSession(false); }; const shouldShowWeeklyReviewTeaser = !isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn; return (
{/* Background Media */}
{/* Immersive Overlay Gradients */}
{/* Header */}

{entryCopy.eyebrow}

{plan === 'pro' && ( PRO )}
{plan !== 'pro' && ( )}
{/* Main Content Area */}
{isCheckingSession ? (

Loading session...

) : (
{entryCopy.reviewReturnEyebrow}

{reviewReturnCopy.title}

{reviewReturnCopy.body}

{reviewReturnRitualLabel && (

{reviewReturnRitualLabel}

)}
) : shouldShowWeeklyReviewTeaser ? ( {reviewTeaserTitle} ) : undefined } errorAccessory={ sessionLookupError ? (
{sessionLookupError}
) : undefined } selectedAtmosphere={selectedAtmosphere} startButtonLabel={entryCopy.startNow} startButtonLoadingLabel={entryCopy.startLoading} atmosphereOptions={ATMOSPHERE_OPTIONS} atmosphereTitle={entryCopy.atmosphereTitle} atmosphereBody={entryCopy.atmosphereBody} onDurationChange={handleDurationChange} onGoalChange={setGoalDraft} onSelectAtmosphere={handleSelectAtmosphere} onSelectDuration={handleSelectDuration} onStartSession={() => { void handleStartSession(); }} />
)} {/* Paywall Overlay */} {paywallSource && (
)}
); };