'use client'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useEffect, useMemo, useRef, useState } from 'react'; import { type FocusPlanItem, type FocusPlanToday, useFocusPlan, } from '@/entities/focus-plan'; import { usePlanTier } from '@/entities/plan'; import { SCENE_THEMES, getSceneById } from '@/entities/scene'; import { useMediaCatalog, getSceneStageBackgroundStyle } from '@/entities/media'; import { SOUND_PRESETS } from '@/entities/session'; import { PaywallSheetContent } from '@/features/paywall-sheet'; import { PlanPill } from '@/features/plan-pill'; import { useFocusStats } from '@/features/stats'; import { focusSessionApi } from '@/features/focus-session/api/focusSessionApi'; import { copy } from '@/shared/i18n'; import { cn } from '@/shared/lib/cn'; import { useDragScroll } from '@/shared/lib/useDragScroll'; import { FocusPlanManageSheet, type FocusPlanEditingState } from './FocusPlanManageSheet'; const FREE_MAX_ITEMS = 1; const PRO_MAX_ITEMS = 5; const focusEntryCopy = { eyebrow: 'VibeRoom', title: '오늘의 깊은 몰입을 위한 단 하나의 목표', description: '지금 당장 시작할 딱 하나만 남겨두세요.', inputLabel: '첫 블록', inputPlaceholder: '예: 제안서 첫 문단만 다듬기', helper: '아주 작게 잡아도 괜찮아요.', startNow: '바로 몰입하기', nextStep: '환경 세팅', manageBlocks: '내 계획에서 가져오기', previewTitle: '이어갈 블록', previewDescription: '다음 후보는 가볍게만 두고, 시작은 위 버튼 하나로 끝냅니다.', reviewLinkLabel: 'stats', reviewFallback: '최근 7일 흐름을 불러오는 중이에요.', ritualMeta: '기본 설정으로 들어갑니다. 공간 안에서 언제든 바꿀 수 있어요.', apiUnavailableNote: '계획 연결이 잠시 느려요. 지금은 첫 블록부터 바로 시작할 수 있어요.', freeUpgradeLabel: '두 번째 블록부터는 PRO', paywallSource: 'focus-entry-manage-sheet', paywallLead: 'Calm Session OS PRO', paywallBody: '여러 블록을 이어서 정리하는 manage sheet는 PRO에서 열립니다.', microStepTitle: '가장 작은 첫 단계 (선택)', microStepHelper: '이 목표를 위해 당장 할 수 있는 5분짜리 행동은 무엇인가요?', microStepPlaceholder: '예: 폴더 열기, 노션 켜기', ritualTitle: '어떤 환경에서 몰입하시겠어요?', ritualHelper: '오늘의 무드를 선택하세요.', }; const ENTRY_SUGGESTIONS = [ { id: 'tidy-10m', label: '정리 10분', goal: '정리 10분만 하기' }, { id: 'mail-3', label: '메일 3개', goal: '메일 3개 정리' }, { id: 'doc-1p', label: '문서 1p', goal: '문서 1p 다듬기' }, ] as const; type EntrySource = 'starter' | 'plan' | 'custom'; type DashboardStep = 'goal' | 'ritual'; const getVisiblePlanItems = ( currentItem: FocusPlanItem | null, nextItems: FocusPlanItem[], limit: number, ) => { return [currentItem, ...nextItems] .filter((item): item is FocusPlanItem => Boolean(item)) .slice(0, limit); }; const resolveVisiblePlanItems = (nextPlan: FocusPlanToday | null, limit: number) => { return getVisiblePlanItems(nextPlan?.currentItem ?? null, nextPlan?.nextItems ?? [], limit); }; // Premium Glassmorphism UI Classes const glassInputClass = 'w-full rounded-full border border-white/20 bg-black/20 px-8 py-5 text-center text-lg md:text-xl font-light tracking-wide text-white placeholder:text-white/40 shadow-2xl backdrop-blur-xl outline-none transition-all focus:border-white/40 focus:bg-black/30 focus:ring-4 focus:ring-white/10'; const primaryGlassBtnClass = 'inline-flex items-center justify-center rounded-full border border-white/20 bg-white/20 px-8 py-4 text-base font-medium text-white shadow-xl backdrop-blur-xl transition-all hover:bg-white/30 hover:scale-[1.02] active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed'; const secondaryGlassBtnClass = 'inline-flex items-center justify-center rounded-full border border-white/10 bg-transparent px-8 py-4 text-base font-medium text-white/80 transition-all hover:bg-white/10 hover:text-white active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed'; const panelGlassClass = 'rounded-[2rem] border border-white/10 bg-black/40 p-6 md:p-8 shadow-2xl backdrop-blur-2xl'; const itemCardGlassClass = 'relative flex flex-col items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/5 p-4 text-white transition-all hover:bg-white/10 active:scale-95 cursor-pointer'; const itemCardGlassSelectedClass = 'border-white/40 bg-white/20 shadow-[0_0_20px_rgba(255,255,255,0.1)]'; export const FocusDashboardWidget = () => { const router = useRouter(); const { plan: planTier, isPro, setPlan } = usePlanTier(); const { plan, isLoading, isSaving, error, source, createItem, updateItem, deleteItem } = useFocusPlan(); const { sceneAssetMap } = useMediaCatalog(); const [step, setStep] = useState('goal'); const [paywallSource, setPaywallSource] = useState(null); const [manageSheetOpen, setManageSheetOpen] = useState(false); const [editingState, setEditingState] = useState(null); const [entryDraft, setEntryDraft] = useState(''); const [selectedPlanItemId, setSelectedPlanItemId] = useState(null); const [entrySource, setEntrySource] = useState('starter'); const [microStepDraft, setMicroStepDraft] = useState(''); // Use user's last preference or default to first const [selectedSceneId, setSelectedSceneId] = useState(SCENE_THEMES[0].id); const [selectedSoundId, setSelectedSoundId] = useState(SOUND_PRESETS[0].id); const [selectedTimerId, setSelectedTimerId] = useState('50-10'); const [isStartingSession, setIsStartingSession] = useState(false); const entryInputRef = useRef(null); const microStepInputRef = useRef(null); const inputRef = useRef(null); const { containerRef: sceneContainerRef, events: sceneDragEvents, isDragging: isSceneDragging, shouldSuppressClick: shouldSuppressSceneClick, } = useDragScroll(); const selectedScene = useMemo(() => getSceneById(selectedSceneId) ?? SCENE_THEMES[0], [selectedSceneId]); const maxItems = isPro ? PRO_MAX_ITEMS : FREE_MAX_ITEMS; const planItems = useMemo(() => { return getVisiblePlanItems(plan.currentItem, plan.nextItems, maxItems); }, [maxItems, plan.currentItem, plan.nextItems]); const currentItem = planItems[0] ?? null; const hasPendingEdit = editingState !== null; const canAddMore = planItems.length < maxItems; const canManagePlan = source === 'api' && !isLoading; const trimmedEntryGoal = entryDraft.trim(); const isGoalReady = trimmedEntryGoal.length > 0; useEffect(() => { if (!editingState) return; const rafId = window.requestAnimationFrame(() => { inputRef.current?.focus(); inputRef.current?.select(); }); return () => window.cancelAnimationFrame(rafId); }, [editingState]); useEffect(() => { if (!currentItem) return; if (entrySource === 'starter' || (entrySource === 'plan' && !selectedPlanItemId)) { setEntryDraft(currentItem.title); setSelectedPlanItemId(currentItem.id); setEntrySource('plan'); } }, [currentItem, entryDraft, entrySource, selectedPlanItemId]); const openPaywall = () => setPaywallSource(focusEntryCopy.paywallSource); const handleSelectPlanItem = (item: FocusPlanItem) => { const isCurrentSelection = currentItem?.id === item.id; setEntryDraft(item.title); setSelectedPlanItemId(isCurrentSelection ? item.id : null); setEntrySource(isCurrentSelection ? 'plan' : 'custom'); setManageSheetOpen(false); }; const handleSelectSuggestion = (goal: string) => { setEntryDraft(goal); setSelectedPlanItemId(null); setEntrySource('starter'); }; const handleEntryDraftChange = (value: string) => { setEntryDraft(value); setEntrySource('custom'); setSelectedPlanItemId(null); }; const handleAddBlock = () => { if (hasPendingEdit || isSaving || !canManagePlan) return; if (!canAddMore) { if (!isPro) openPaywall(); return; } setEditingState({ mode: 'new', value: '' }); }; const handleEditRow = (item: FocusPlanItem) => { if (hasPendingEdit || isSaving) return; setEditingState({ mode: 'edit', itemId: item.id, value: item.title }); }; const handleManageDraftChange = (value: string) => { setEditingState((current) => current ? { ...current, value } : current); }; const handleCancelEdit = () => { if (!isSaving) setEditingState(null); }; const handleSaveEdit = async () => { if (!editingState) return; const trimmedTitle = editingState.value.trim(); if (!trimmedTitle) return; if (editingState.mode === 'new') { const nextPlan = await createItem({ title: trimmedTitle }); if (!nextPlan) return; setEditingState(null); if (!currentItem) { const nextVisiblePlanItems = resolveVisiblePlanItems(nextPlan, maxItems); const nextCurrentItem = nextVisiblePlanItems[0] ?? null; if (nextCurrentItem) { setEntryDraft(nextCurrentItem.title); setSelectedPlanItemId(nextCurrentItem.id); setEntrySource('plan'); } } return; } const currentRow = planItems.find((item) => item.id === editingState.itemId); if (!currentRow) return; if (currentRow.title === trimmedTitle) { setEditingState(null); return; } const nextPlan = await updateItem(editingState.itemId, { title: trimmedTitle }); if (!nextPlan) return; setEditingState(null); if (selectedPlanItemId === editingState.itemId) { setEntryDraft(trimmedTitle); setEntrySource('plan'); } }; const handleDeleteRow = async (itemId: string) => { const nextPlan = await deleteItem(itemId); if (!nextPlan) return; if (editingState?.mode === 'edit' && editingState.itemId === itemId) { setEditingState(null); } if (selectedPlanItemId === itemId) { const nextVisiblePlanItems = resolveVisiblePlanItems(nextPlan, maxItems); const nextCurrentItem = nextVisiblePlanItems[0] ?? null; if (nextCurrentItem) { setEntryDraft(nextCurrentItem.title); setSelectedPlanItemId(nextCurrentItem.id); setEntrySource('plan'); return; } setEntryDraft(''); setSelectedPlanItemId(null); setEntrySource('custom'); } }; const handleNextStep = () => { if (!isGoalReady) { entryInputRef.current?.focus(); return; } if (step === 'goal') setStep('ritual'); }; const handleStartSession = async () => { if (isStartingSession) return; setIsStartingSession(true); try { await focusSessionApi.startSession({ goal: trimmedEntryGoal, microStep: microStepDraft.trim() || null, sceneId: selectedSceneId, soundPresetId: selectedSoundId, timerPresetId: selectedTimerId, focusPlanItemId: selectedPlanItemId || undefined, entryPoint: 'space-setup' }); router.push('/space'); } catch (err) { console.error('Failed to start session', err); setIsStartingSession(false); } }; return (
{/* Premium Cinematic Background */}
{/* Global Gradient Overlay for text readability */}
{/* Header */}

{focusEntryCopy.eyebrow}

{ if (!isPro) openPaywall(); }} />
{/* Main Content Area */}
{/* Step 1: Goal Setup */}

{focusEntryCopy.title}

handleEntryDraftChange(event.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleNextStep()} placeholder={focusEntryCopy.inputPlaceholder} className={glassInputClass} autoFocus />
{/* Suggestions / Manage - very minimal */}
{ENTRY_SUGGESTIONS.map((suggestion) => { const isActive = selectedPlanItemId === null && trimmedEntryGoal === suggestion.goal; return ( ); })}
{/* Step 2: Ritual Setup */}

Today's Focus

{trimmedEntryGoal}

{/* Microstep */}

{focusEntryCopy.microStepHelper}

{/* Timer */}

몰입 리듬

{[{id: '25-5', label: '25 / 5'}, {id: '50-10', label: '50 / 10'}, {id: '90-20', label: '90 / 20'}].map(timer => ( ))}
{/* Scene */}

배경 공간

{SCENE_THEMES.map(scene => ( ))}
{/* Sound */}

사운드

{SOUND_PRESETS.map(sound => ( ))}
{/* Plan Sheet & Paywall */} { if (!isSaving) { setManageSheetOpen(false); setEditingState(null); } }} onAddBlock={handleAddBlock} onDraftChange={handleManageDraftChange} onSelect={handleSelectPlanItem} onEdit={handleEditRow} onDelete={(itemId) => { void handleDeleteRow(itemId); }} onSave={() => { void handleSaveEdit(); }} onCancel={handleCancelEdit} /> {paywallSource ? (
) : null}
); };