feat(flow): focus session api v2 웹 계약 전환

This commit is contained in:
2026-03-16 17:30:52 +09:00
parent f4910238a0
commit 38abc1e0c7
30 changed files with 390 additions and 702 deletions

View File

@@ -1,12 +1,6 @@
import { getSceneById, type SceneTheme } from '@/entities/scene';
import { SOUND_PRESETS } from '@/entities/session';
const TIMER_PRESETS = [
{ id: '25-5', label: '25/5', focusMinutes: 25 },
{ id: '50-10', label: '50/10', focusMinutes: 50 },
{ id: '90-20', label: '90/20', focusMinutes: 90 },
] as const;
const DURATION_SUGGESTIONS = [25, 45, 70, 90] as const;
export interface AtmosphereOption {
@@ -14,6 +8,7 @@ export interface AtmosphereOption {
name: string;
sceneId: string;
soundPresetId: string | null;
recommendedDurationMinutes: number;
description: string;
caption: string;
scene: SceneTheme;
@@ -25,6 +20,7 @@ const createAtmosphereOption = (
name: string,
sceneId: string,
soundPresetId: string | null,
recommendedDurationMinutes: number,
description: string,
caption: string,
): AtmosphereOption => {
@@ -38,6 +34,7 @@ const createAtmosphereOption = (
name,
sceneId,
soundPresetId,
recommendedDurationMinutes,
description,
caption,
scene,
@@ -52,6 +49,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Rain Window',
'rain-window',
'rain-focus',
45,
'비 소리 위로 조용히 문장을 붙잡기 좋은 흐름.',
'조용한 시작',
),
@@ -60,6 +58,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Quiet Library',
'quiet-library',
'deep-white',
70,
'소음 없이 길게 읽고 정리할 때 안정적인 조합.',
'길게 읽는 날',
),
@@ -68,6 +67,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Dawn Cafe',
'dawn-cafe',
'cafe-work',
25,
'가볍게 손을 움직이며 초안을 시작하기 좋은 온도.',
'워밍업용',
),
@@ -76,6 +76,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Forest Draft',
'forest',
'forest-birds',
50,
'딥워크 진입 전에 숨을 고르게 만드는 기본 조합.',
'기본 리듬',
),
@@ -84,6 +85,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Fireplace Glow',
'fireplace',
'fireplace',
70,
'밤에 닫히지 않는 일 하나를 끝까지 가져가고 싶을 때.',
'늦은 시간용',
),
@@ -92,6 +94,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Deep Night Desk',
'city-night',
'deep-white',
90,
'도시의 불빛은 멀리 두고 화면 안의 일만 남기는 조합.',
'몰입 유지',
),
@@ -100,6 +103,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Snow Light',
'snow-mountain',
'deep-white',
45,
'머리를 식히면서 구조를 정리해야 할 때 선명한 공기.',
'정리용',
),
@@ -108,6 +112,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Sun Window',
'sun-window',
'silent',
25,
'과하게 자극적이지 않게 아침 에너지를 가져오는 장면.',
'낮 시간용',
),
@@ -116,6 +121,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Ocean Still',
'wave-sound',
'ocean-calm',
70,
'넓은 생각이 필요한 기획이나 리서치에 어울리는 흐름.',
'넓게 생각하기',
),
@@ -124,6 +130,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Orbit Night',
'outer-space',
'deep-white',
90,
'길고 깊은 블록에 들어갈 때 외부 자극을 멀리 밀어낸다.',
'장시간 집중',
),
@@ -132,6 +139,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Rain Notes',
'rain-window',
'deep-white',
50,
'빗소리 대신 더 조용한 백색 소음으로 문장만 남긴 버전.',
'더 낮은 자극',
),
@@ -140,6 +148,7 @@ export const ATMOSPHERE_OPTIONS: AtmosphereOption[] = [
'Quiet Pages',
'quiet-library',
'silent',
45,
'읽기와 쓰기 사이를 오갈 때 가장 얇은 존재감으로 머문다.',
'완전 조용함',
),
@@ -179,29 +188,12 @@ export const sanitizeDurationDraft = (value: string) => {
return String(Math.min(180, parsed));
};
export const getTimerPresetMetaById = (timerPresetId: string) => {
return TIMER_PRESETS.find((preset) => preset.id === timerPresetId) ?? TIMER_PRESETS[1];
};
export const resolveNearestTimerPreset = (minutes: number) => {
return TIMER_PRESETS.reduce((best, candidate) => {
const bestDiff = Math.abs(best.focusMinutes - minutes);
const candidateDiff = Math.abs(candidate.focusMinutes - minutes);
if (candidateDiff < bestDiff) {
return candidate;
}
if (candidateDiff === bestDiff && candidate.focusMinutes > best.focusMinutes) {
return candidate;
}
return best;
});
export const formatDurationMinutesLabel = (minutes: number) => {
return `${minutes}m`;
};
export const getRecommendedDurationMinutes = (option: AtmosphereOption) => {
return getTimerPresetMetaById(option.scene.recommendedTimerPresetId).focusMinutes;
return option.recommendedDurationMinutes;
};
export const getAtmosphereOptionById = (id: string) => {

View File

@@ -7,6 +7,7 @@ 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 { preferencesApi } from '@/features/preferences/api/preferencesApi';
import { PaywallSheetContent } from '@/features/paywall-sheet';
import { focusSessionApi, type FocusSession } from '@/features/focus-session/api/focusSessionApi';
import { useFocusStats, type ReviewCarryHint } from '@/features/stats';
@@ -18,24 +19,13 @@ import {
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];
@@ -67,7 +57,7 @@ const entryCopy = {
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',
reviewReturnRitualLabel: 'Recommended Atmosphere',
paywallLead: 'Calm Session OS PRO',
paywallBody: 'Pro enables faster rituals and deeper reviews for seamless entry and return.',
};
@@ -101,29 +91,22 @@ export const FocusDashboardWidget = () => {
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 reviewEntryAtmosphereId = searchParams.get('entryAtmosphereId');
const reviewEntryDurationMinutes = searchParams.get('entryDurationMinutes');
const initialAtmosphere = useMemo(() => {
return (
findAtmosphereOptionForSelection(
reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID,
reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID,
) ?? DEFAULT_ATMOSPHERE
(reviewEntryAtmosphereId ? ATMOSPHERE_OPTIONS.find((option) => option.id === reviewEntryAtmosphereId) : null) ??
DEFAULT_ATMOSPHERE
);
}, [reviewEntryPresetConfig]);
}, [reviewEntryAtmosphereId]);
const initialDurationMinutes = useMemo(() => {
if (reviewEntryPresetConfig) {
return getTimerPresetMetaById(reviewEntryPresetConfig.timerPresetId).focusMinutes;
const parsed = Number(reviewEntryDurationMinutes);
if (Number.isFinite(parsed) && parsed >= 5 && parsed <= 180) {
return parsed;
}
return getRecommendedDurationMinutes(initialAtmosphere);
}, [initialAtmosphere, reviewEntryPresetConfig]);
}, [initialAtmosphere, reviewEntryDurationMinutes]);
const [goalDraft, setGoalDraft] = useState('');
const [durationDraft, setDurationDraft] = useState(() => String(initialDurationMinutes));
@@ -150,11 +133,6 @@ export const FocusDashboardWidget = () => {
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];
@@ -184,7 +162,9 @@ export const FocusDashboardWidget = () => {
const reviewReturnCopy =
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
const reviewReturnRitualLabel =
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
isPro && reviewEntryAtmosphereId
? `${entryCopy.reviewReturnRitualLabel} · ${initialAtmosphere.name} · ${initialDurationMinutes}m`
: null;
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
const durationHelper =
rawDurationValue !== null && rawDurationValue < 5
@@ -227,6 +207,43 @@ export const FocusDashboardWidget = () => {
};
}, []);
useEffect(() => {
if (isReviewReturn) {
return;
}
let cancelled = false;
void preferencesApi
.getFocusPreferences()
.then((preferences) => {
if (cancelled) {
return;
}
if (preferences.defaultAtmosphereId) {
const preferredAtmosphere = ATMOSPHERE_OPTIONS.find(
(option) => option.id === preferences.defaultAtmosphereId,
);
if (preferredAtmosphere) {
setSelectedAtmosphereId(preferredAtmosphere.id);
}
}
if (!hasEditedDuration && preferences.defaultDurationMinutes) {
setDurationDraft(String(preferences.defaultDurationMinutes));
}
})
.catch(() => {
// Preference hydration should not block entry.
});
return () => {
cancelled = true;
};
}, [hasEditedDuration, isReviewReturn]);
useEffect(() => {
if (!isCheckingSession && hasCurrentSession) {
router.replace('/space');
@@ -276,9 +293,10 @@ export const FocusDashboardWidget = () => {
await focusSessionApi.startSession({
goal: trimmedGoal,
microStep: null,
atmosphereId: selectedAtmosphere.id,
focusDurationMinutes: parsedDurationMinutes,
sceneId: selectedAtmosphere.sceneId,
soundPresetId: selectedAtmosphere.soundPresetId,
timerPresetId: resolvedTimerPreset.id,
entryPoint: 'space-setup',
});
router.push('/space');

View File

@@ -2,9 +2,12 @@
import Link from 'next/link';
import {
DEFAULT_PRESET_OPTIONS,
NOTIFICATION_INTENSITY_OPTIONS,
} from '@/shared/config/settingsOptions';
import {
ATMOSPHERE_OPTIONS,
ENTRY_DURATION_SUGGESTIONS,
} from '@/widgets/focus-dashboard/model/atmosphereEntry';
import { copy } from '@/shared/i18n';
import { useUserFocusPreferences } from '@/features/preferences';
import { cn } from '@/shared/lib/cn';
@@ -114,24 +117,55 @@ export const SettingsPanelWidget = () => {
</section>
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
<h2 className="text-base font-semibold text-brand-dark">{settings.defaultPresetTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">{settings.defaultPresetDescription}</p>
<h2 className="text-base font-semibold text-brand-dark">{settings.defaultDurationTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">{settings.defaultDurationDescription}</p>
<div className="mt-3 space-y-2">
{DEFAULT_PRESET_OPTIONS.map((preset) => (
<div className="flex flex-wrap gap-2">
{ENTRY_DURATION_SUGGESTIONS.map((minutes) => (
<button
key={minutes}
type="button"
onClick={() => {
void updatePreferences({ defaultDurationMinutes: minutes });
}}
className={cn(
'rounded-full border px-3 py-1.5 text-xs transition-colors',
preferences.defaultDurationMinutes === minutes
? 'border-brand-primary/45 bg-brand-soft/60 text-brand-dark'
: 'border-brand-dark/18 bg-white/75 text-brand-dark/78 hover:bg-white',
)}
>
{minutes}
</button>
))}
</div>
</div>
</section>
<section className="rounded-xl border border-brand-dark/12 bg-white/78 p-4 backdrop-blur-sm">
<h2 className="text-base font-semibold text-brand-dark">{settings.defaultAtmosphereTitle}</h2>
<p className="mt-1 text-sm text-brand-dark/64">{settings.defaultAtmosphereDescription}</p>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{ATMOSPHERE_OPTIONS.slice(0, 8).map((atmosphere) => (
<button
key={preset.id}
key={atmosphere.id}
type="button"
onClick={() => {
void updatePreferences({ defaultPresetId: preset.id });
void updatePreferences({
defaultAtmosphereId: atmosphere.id,
defaultSceneId: atmosphere.sceneId,
defaultSoundPresetId: atmosphere.soundPresetId,
});
}}
className={cn(
'w-full rounded-lg border px-3 py-2 text-left text-sm transition-colors',
preferences.defaultPresetId === preset.id
preferences.defaultAtmosphereId === atmosphere.id
? 'border-brand-primary/45 bg-brand-soft/58 text-brand-dark'
: 'border-brand-dark/16 bg-white/72 text-brand-dark/82 hover:bg-white',
)}
>
{preset.label}
<p className="font-medium">{atmosphere.name}</p>
<p className="mt-1 text-xs text-brand-dark/60">{atmosphere.caption}</p>
</button>
))}
</div>

View File

@@ -7,7 +7,6 @@ import type { SceneTheme } from '@/entities/scene';
import type {
GoalChip,
SoundPreset,
TimerPreset,
} from '@/entities/session';
import { copy } from '@/shared/i18n';
import { SceneSelectCarousel } from '@/features/scene-select';
@@ -22,16 +21,16 @@ interface SpaceSetupDrawerWidgetProps {
scenes: SceneTheme[];
sceneAssetMap?: SceneAssetMap;
selectedSceneId: string;
selectedTimerLabel: string;
selectedDurationLabel: string;
selectedSoundPresetId: string;
goalInput: string;
selectedGoalId: string | null;
goalChips: GoalChip[];
soundPresets: SoundPreset[];
timerPresets: TimerPreset[];
durationOptions: readonly number[];
canStart: boolean;
onSceneSelect: (sceneId: string) => void;
onTimerSelect: (timerLabel: string) => void;
onDurationSelect: (durationMinutes: number) => void;
onSoundSelect: (soundPresetId: string) => void;
onGoalChange: (value: string) => void;
onGoalChipSelect: (chip: GoalChip) => void;
@@ -81,16 +80,16 @@ export const SpaceSetupDrawerWidget = ({
scenes,
sceneAssetMap,
selectedSceneId,
selectedTimerLabel,
selectedDurationLabel,
selectedSoundPresetId,
goalInput,
selectedGoalId,
goalChips,
soundPresets,
timerPresets,
durationOptions,
canStart,
onSceneSelect,
onTimerSelect,
onDurationSelect,
onSoundSelect,
onGoalChange,
onGoalChipSelect,
@@ -208,7 +207,7 @@ export const SpaceSetupDrawerWidget = ({
/>
<SummaryChip
label={setup.timerLabel}
value={selectedTimerLabel}
value={selectedDurationLabel}
open={openPopover === 'timer'}
onClick={() => togglePopover('timer')}
/>
@@ -237,15 +236,15 @@ export const SpaceSetupDrawerWidget = ({
{openPopover === 'timer' ? (
<div className="absolute left-0 top-[calc(100%+0.5rem)] z-20 rounded-2xl border border-white/14 bg-slate-950/80 p-3 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none">
<div className="flex flex-wrap gap-1.5">
{timerPresets.slice(0, 3).map((preset) => {
const selected = preset.label === selectedTimerLabel;
{durationOptions.map((minutes) => {
const selected = `${minutes}m` === selectedDurationLabel;
return (
<button
key={preset.id}
key={minutes}
type="button"
onClick={() => {
onTimerSelect(preset.label);
onDurationSelect(minutes);
setOpenPopover(null);
}}
className={cn(
@@ -255,7 +254,7 @@ export const SpaceSetupDrawerWidget = ({
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
)}
>
{preset.label}
{minutes}m
</button>
);
})}

View File

@@ -3,12 +3,12 @@ export type SessionEntryPoint = 'space-setup' | 'goal-complete' | 'resume-restor
export type SelectionOverride = {
sound: boolean;
timer: boolean;
duration: boolean;
};
export interface StoredWorkspaceSelection {
sceneId?: string;
timerPresetId?: string;
durationMinutes?: number;
soundPresetId?: string;
goal?: string;
override?: Partial<SelectionOverride>;

View File

@@ -16,11 +16,16 @@ import type { FocusSession } from '@/features/focus-session';
import { preferencesApi } from '@/features/preferences/api/preferencesApi';
import { copy } from '@/shared/i18n';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import {
findAtmosphereOptionForSelection,
getAtmosphereOptionById,
getRecommendedDurationMinutes,
} from '@/widgets/focus-dashboard/model/atmosphereEntry';
import type { SelectionOverride } from './types';
import {
formatDurationMinutesLabel,
readStoredWorkspaceSelection,
resolveTimerLabelFromPresetId,
resolveTimerPresetIdFromLabel,
resolveInitialDurationMinutes,
} from './workspaceSelection';
import { useWorkspacePersistence } from './useWorkspacePersistence';
import { useWorkspaceMediaDiagnostics } from './useWorkspaceMediaDiagnostics';
@@ -29,12 +34,7 @@ interface UseSpaceWorkspaceSelectionParams {
initialSceneId: string;
initialGoal: string;
initialFocusPlanItemId: string | null;
initialTimerLabel: string;
sceneQuery: string | null;
goalQuery: string;
soundQuery: string | null;
timerQuery: string | null;
hasQueryOverrides: boolean;
initialDurationMinutes: number;
currentSession: FocusSession | null;
sceneAssetMap: SceneAssetMap;
selectedPresetId: string;
@@ -46,6 +46,7 @@ interface UseSpaceWorkspaceSelectionParams {
updateCurrentSelection: (payload: {
sceneId?: string;
soundPresetId?: string | null;
atmosphereId?: string | null;
}) => Promise<FocusSession | null>;
mediaCatalogError: string | null;
usedFallbackManifest: boolean;
@@ -66,12 +67,7 @@ export const useSpaceWorkspaceSelection = ({
initialSceneId,
initialGoal,
initialFocusPlanItemId,
initialTimerLabel,
sceneQuery,
goalQuery,
soundQuery,
timerQuery,
hasQueryOverrides,
initialDurationMinutes,
currentSession,
sceneAssetMap,
selectedPresetId,
@@ -86,7 +82,7 @@ export const useSpaceWorkspaceSelection = ({
hasResolvedManifest,
}: UseSpaceWorkspaceSelectionParams) => {
const [selectedSceneId, setSelectedSceneId] = useState(initialSceneId);
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
const [selectedDurationMinutes, setSelectedDurationMinutes] = useState(initialDurationMinutes);
const [goalInput, setGoalInput] = useState(initialGoal);
const [linkedFocusPlanItemId, setLinkedFocusPlanItemId] = useState<string | null>(initialFocusPlanItemId);
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
@@ -95,7 +91,7 @@ export const useSpaceWorkspaceSelection = ({
const [hasHydratedSelection, setHasHydratedSelection] = useState(false);
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
sound: false,
timer: false,
duration: false,
});
const didHydrateServerPreferencesRef = useRef(false);
@@ -110,6 +106,10 @@ export const useSpaceWorkspaceSelection = ({
}, [selectedScene]);
const canStart = goalInput.trim().length > 0;
const selectedDurationLabel = useMemo(
() => formatDurationMinutesLabel(selectedDurationMinutes),
[selectedDurationMinutes],
);
const applyRecommendedSelections = useCallback((
sceneId: string,
@@ -121,12 +121,17 @@ export const useSpaceWorkspaceSelection = ({
return;
}
if (!overrideState.timer) {
const recommendedTimerLabel = resolveTimerLabelFromPresetId(scene.recommendedTimerPresetId);
if (!overrideState.duration) {
const recommendedAtmosphere =
findAtmosphereOptionForSelection(sceneId, scene.recommendedSoundPresetId) ??
findAtmosphereOptionForSelection(sceneId);
if (recommendedTimerLabel) {
setSelectedTimerLabel(recommendedTimerLabel);
}
setSelectedDurationMinutes(
resolveInitialDurationMinutes(
undefined,
recommendedAtmosphere ? getRecommendedDurationMinutes(recommendedAtmosphere) : undefined,
),
);
}
if (
@@ -140,12 +145,16 @@ export const useSpaceWorkspaceSelection = ({
const persistSpaceSelection = useCallback((selection: {
sceneId?: string;
soundPresetId?: string | null;
durationMinutes?: number;
}) => {
const preferencePayload: {
defaultAtmosphereId?: string | null;
defaultDurationMinutes?: number | null;
defaultSceneId?: string | null;
defaultSoundPresetId?: string | null;
} = {};
const currentSessionPayload: {
atmosphereId?: string | null;
sceneId?: string;
soundPresetId?: string | null;
} = {};
@@ -160,6 +169,18 @@ export const useSpaceWorkspaceSelection = ({
currentSessionPayload.soundPresetId = selection.soundPresetId;
}
if (selection.durationMinutes !== undefined) {
preferencePayload.defaultDurationMinutes = selection.durationMinutes;
}
const nextSceneId = selection.sceneId ?? selectedSceneId;
const nextSoundPresetId =
selection.soundPresetId === undefined ? selectedPresetId : selection.soundPresetId;
const nextAtmosphere = findAtmosphereOptionForSelection(nextSceneId, nextSoundPresetId);
preferencePayload.defaultAtmosphereId = nextAtmosphere?.id ?? null;
currentSessionPayload.atmosphereId = nextAtmosphere?.id ?? null;
void (async () => {
const [preferencesResult, sessionResult] = await Promise.allSettled([
preferencesApi.updateFocusPreferences(preferencePayload),
@@ -181,7 +202,7 @@ export const useSpaceWorkspaceSelection = ({
});
}
})();
}, [currentSession, pushStatusLine, updateCurrentSelection]);
}, [currentSession, pushStatusLine, selectedPresetId, selectedSceneId, updateCurrentSelection]);
const handleSelectScene = useCallback((sceneId: string) => {
void (async () => {
@@ -215,21 +236,26 @@ export const useSpaceWorkspaceSelection = ({
unlockPlayback,
]);
const handleSelectTimer = useCallback((timerLabel: string, markOverride = false) => {
setSelectedTimerLabel(timerLabel);
const handleSelectDuration = useCallback((durationMinutes: number, markOverride = false) => {
if (!Number.isFinite(durationMinutes) || durationMinutes < 5) {
return;
}
setSelectedDurationMinutes(durationMinutes);
persistSpaceSelection({ durationMinutes });
if (!markOverride) {
return;
}
setSelectionOverride((current) => {
if (current.timer) {
if (current.duration) {
return current;
}
return { ...current, timer: true };
return { ...current, duration: true };
});
}, []);
}, [persistSpaceSelection]);
const handleSelectSound = useCallback((presetId: string, markOverride = false) => {
void (async () => {
@@ -287,17 +313,16 @@ export const useSpaceWorkspaceSelection = ({
const storedSelection = readStoredWorkspaceSelection();
const restoredSelectionOverride: SelectionOverride = {
sound: Boolean(storedSelection.override?.sound),
timer: Boolean(storedSelection.override?.timer),
duration: Boolean(storedSelection.override?.duration),
};
const restoredSceneId =
!sceneQuery && storedSelection.sceneId && getSceneById(storedSelection.sceneId)
storedSelection.sceneId && getSceneById(storedSelection.sceneId)
? normalizeSceneId(storedSelection.sceneId)
: null;
const restoredTimerLabel = !timerQuery
? resolveTimerLabelFromPresetId(storedSelection.timerPresetId)
: null;
const restoredDurationMinutes =
typeof storedSelection.durationMinutes === 'number' ? storedSelection.durationMinutes : null;
const restoredSoundPresetId =
!soundQuery && storedSelection.soundPresetId && SOUND_PRESETS.some((preset) => preset.id === storedSelection.soundPresetId)
storedSelection.soundPresetId && SOUND_PRESETS.some((preset) => preset.id === storedSelection.soundPresetId)
? storedSelection.soundPresetId
: null;
const restoredGoal = storedSelection.goal?.trim() ?? '';
@@ -308,15 +333,15 @@ export const useSpaceWorkspaceSelection = ({
setSelectedSceneId(restoredSceneId);
}
if (restoredTimerLabel) {
setSelectedTimerLabel(restoredTimerLabel);
if (restoredDurationMinutes) {
setSelectedDurationMinutes(restoredDurationMinutes);
}
if (restoredSoundPresetId) {
setSelectedPresetId(restoredSoundPresetId);
}
if (!goalQuery && restoredGoal.length > 0 && !hasQueryOverrides) {
if (restoredGoal.length > 0) {
setResumeGoal(restoredGoal);
setShowResumePrompt(true);
}
@@ -327,7 +352,7 @@ export const useSpaceWorkspaceSelection = ({
return () => {
window.cancelAnimationFrame(rafId);
};
}, [goalQuery, hasQueryOverrides, sceneQuery, setSelectedPresetId, soundQuery, timerQuery]);
}, [setSelectedPresetId]);
useEffect(() => {
if (!hasHydratedSelection || didHydrateServerPreferencesRef.current) {
@@ -340,19 +365,27 @@ export const useSpaceWorkspaceSelection = ({
void preferencesApi
.getFocusPreferences()
.then((preferences) => {
if (cancelled || currentSession || hasQueryOverrides) {
if (cancelled || currentSession) {
return;
}
const normalizedPreferredSceneId = normalizeSceneId(preferences.defaultSceneId);
const preferredAtmosphere =
preferences.defaultAtmosphereId
? getAtmosphereOptionById(preferences.defaultAtmosphereId)
: null;
const normalizedPreferredSceneId = normalizeSceneId(
preferredAtmosphere?.sceneId ?? preferences.defaultSceneId,
);
const nextSceneId =
normalizedPreferredSceneId && getSceneById(normalizedPreferredSceneId)
? normalizedPreferredSceneId
: null;
const nextSoundPresetId =
preferences.defaultSoundPresetId &&
SOUND_PRESETS.some((preset) => preset.id === preferences.defaultSoundPresetId)
? preferences.defaultSoundPresetId
(preferredAtmosphere?.soundPresetId ?? preferences.defaultSoundPresetId) &&
SOUND_PRESETS.some(
(preset) => preset.id === (preferredAtmosphere?.soundPresetId ?? preferences.defaultSoundPresetId),
)
? (preferredAtmosphere?.soundPresetId ?? preferences.defaultSoundPresetId)
: null;
if (nextSceneId) {
@@ -363,6 +396,11 @@ export const useSpaceWorkspaceSelection = ({
setSelectedPresetId(nextSoundPresetId);
setSelectionOverride((current) => ({ ...current, sound: true }));
}
if (preferences.defaultDurationMinutes) {
setSelectedDurationMinutes(preferences.defaultDurationMinutes);
setSelectionOverride((current) => ({ ...current, duration: true }));
}
})
.catch(() => {
// Focus preference load failure should not block entering the space.
@@ -371,15 +409,13 @@ export const useSpaceWorkspaceSelection = ({
return () => {
cancelled = true;
};
}, [currentSession, hasHydratedSelection, hasQueryOverrides, setSelectedPresetId]);
}, [currentSession, hasHydratedSelection, setSelectedPresetId]);
useEffect(() => {
if (!currentSession) {
return;
}
const nextTimerLabel =
resolveTimerLabelFromPresetId(currentSession.timerPresetId) ?? selectedTimerLabel;
const nextSoundPresetId =
currentSession.soundPresetId &&
SOUND_PRESETS.some((preset) => preset.id === currentSession.soundPresetId)
@@ -387,7 +423,7 @@ export const useSpaceWorkspaceSelection = ({
: selectedPresetId;
const rafId = window.requestAnimationFrame(() => {
setSelectedSceneId(normalizeSceneId(currentSession.sceneId) ?? currentSession.sceneId);
setSelectedTimerLabel(nextTimerLabel);
setSelectedDurationMinutes(Math.max(5, Math.round(currentSession.focusDurationSeconds / 60)));
setSelectedPresetId(nextSoundPresetId);
setGoalInput(currentSession.goal);
setLinkedFocusPlanItemId(currentSession.focusPlanItemId ?? null);
@@ -398,12 +434,12 @@ export const useSpaceWorkspaceSelection = ({
return () => {
window.cancelAnimationFrame(rafId);
};
}, [currentSession, selectedPresetId, selectedTimerLabel, setSelectedPresetId]);
}, [currentSession, selectedPresetId, setSelectedPresetId]);
useWorkspacePersistence({
hasHydratedSelection,
selectedScene,
selectedTimerPresetId: resolveTimerPresetIdFromLabel(selectedTimerLabel),
selectedDurationMinutes,
selectedPresetId,
goalInput,
showResumePrompt,
@@ -422,7 +458,8 @@ export const useSpaceWorkspaceSelection = ({
return {
selectedSceneId,
selectedTimerLabel,
selectedDurationMinutes,
selectedDurationLabel,
goalInput,
linkedFocusPlanItemId,
selectedGoalId,
@@ -440,7 +477,7 @@ export const useSpaceWorkspaceSelection = ({
setShowResumePrompt,
setResumeGoal,
handleSelectScene,
handleSelectTimer,
handleSelectDuration,
handleSelectSound,
handleGoalChipSelect,
handleGoalChange,

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from 'react';
import type { FocusSession } from '@/features/focus-session';
import { copy } from '@/shared/i18n';
import type { HudStatusLinePayload } from '@/shared/lib/useHudStatusLine';
import { resolveTimerPresetIdFromLabel } from './workspaceSelection';
import { findAtmosphereOptionForSelection } from '@/widgets/focus-dashboard/model/atmosphereEntry';
import type { SessionEntryPoint, WorkspaceMode } from './types';
interface UseSpaceWorkspaceSessionControlsParams {
@@ -19,16 +19,17 @@ interface UseSpaceWorkspaceSessionControlsParams {
goalInput: string;
linkedFocusPlanItemId: string | null;
selectedSceneId: string;
selectedTimerLabel: string;
selectedDurationMinutes: number;
selectedPresetId: string;
soundPlaybackError: string | null;
pushStatusLine: (payload: HudStatusLinePayload) => void;
unlockPlayback: (requestedUrl?: string | null) => Promise<boolean>;
resolveSoundPlaybackUrl: (presetId: string) => string | null;
startSession: (input: {
atmosphereId: string;
sceneId: string;
goal: string;
timerPresetId: string;
focusDurationMinutes: number;
soundPresetId: string | null;
focusPlanItemId?: string;
entryPoint: SessionEntryPoint;
@@ -50,8 +51,9 @@ interface UseSpaceWorkspaceSessionControlsParams {
advanceGoal: (input: {
completedGoal: string;
nextGoal: string;
sceneId: string;
timerPresetId: string;
sceneId?: string;
atmosphereId?: string | null;
focusDurationMinutes?: number;
soundPresetId: string;
focusPlanItemId?: string;
}) => Promise<{ nextSession: FocusSession } | null>;
@@ -74,7 +76,7 @@ export const useSpaceWorkspaceSessionControls = ({
goalInput,
linkedFocusPlanItemId,
selectedSceneId,
selectedTimerLabel,
selectedDurationMinutes,
selectedPresetId,
soundPlaybackError,
pushStatusLine,
@@ -122,16 +124,17 @@ export const useSpaceWorkspaceSessionControls = ({
const startFocusFlow = useCallback(async () => {
const trimmedGoal = goalInput.trim();
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
const selectedAtmosphere = findAtmosphereOptionForSelection(selectedSceneId, selectedPresetId);
if (!trimmedGoal || !timerPresetId) {
if (!trimmedGoal) {
return;
}
const startedSession = await startSession({
atmosphereId: selectedAtmosphere?.id ?? selectedSceneId,
sceneId: selectedSceneId,
goal: trimmedGoal,
timerPresetId,
focusDurationMinutes: selectedDurationMinutes,
soundPresetId: selectedPresetId,
focusPlanItemId: linkedFocusPlanItemId ?? undefined,
entryPoint: pendingSessionEntryPoint,
@@ -150,9 +153,9 @@ export const useSpaceWorkspaceSessionControls = ({
goalInput,
pendingSessionEntryPoint,
pushStatusLine,
selectedDurationMinutes,
selectedPresetId,
selectedSceneId,
selectedTimerLabel,
linkedFocusPlanItemId,
setPreviewPlaybackState,
startSession,
@@ -248,9 +251,11 @@ export const useSpaceWorkspaceSessionControls = ({
const handleGoalAdvance = useCallback(async (nextGoal: string) => {
const trimmedNextGoal = nextGoal.trim();
const trimmedCurrentGoal = goalInput.trim();
const timerPresetId = resolveTimerPresetIdFromLabel(selectedTimerLabel);
const selectedAtmosphere =
findAtmosphereOptionForSelection(selectedSceneId, selectedPresetId)
?? (currentSession?.atmosphereId ? { id: currentSession.atmosphereId } : null);
if (!trimmedNextGoal || !trimmedCurrentGoal || !timerPresetId || !currentSession) {
if (!trimmedNextGoal || !trimmedCurrentGoal || !currentSession) {
return false;
}
@@ -259,8 +264,9 @@ export const useSpaceWorkspaceSessionControls = ({
const nextState = await advanceGoal({
completedGoal: trimmedCurrentGoal,
nextGoal: trimmedNextGoal,
atmosphereId: selectedAtmosphere?.id ?? null,
focusDurationMinutes: selectedDurationMinutes,
sceneId: selectedSceneId,
timerPresetId,
soundPresetId: selectedPresetId,
focusPlanItemId: linkedFocusPlanItemId ?? undefined,
});
@@ -290,9 +296,9 @@ export const useSpaceWorkspaceSessionControls = ({
linkedFocusPlanItemId,
pushStatusLine,
resolveSoundPlaybackUrl,
selectedDurationMinutes,
selectedPresetId,
selectedSceneId,
selectedTimerLabel,
setGoalInput,
setLinkedFocusPlanItemId,
setPendingSessionEntryPoint,

View File

@@ -8,7 +8,7 @@ import type { SceneTheme } from '@/entities/scene';
interface UseWorkspacePersistenceParams {
hasHydratedSelection: boolean;
selectedScene: SceneTheme;
selectedTimerPresetId: string | undefined;
selectedDurationMinutes: number;
selectedPresetId: string;
goalInput: string;
showResumePrompt: boolean;
@@ -19,7 +19,7 @@ interface UseWorkspacePersistenceParams {
export const useWorkspacePersistence = ({
hasHydratedSelection,
selectedScene,
selectedTimerPresetId,
selectedDurationMinutes,
selectedPresetId,
goalInput,
showResumePrompt,
@@ -41,7 +41,7 @@ export const useWorkspacePersistence = ({
WORKSPACE_SELECTION_STORAGE_KEY,
JSON.stringify({
sceneId: selectedScene.id,
timerPresetId: selectedTimerPresetId,
durationMinutes: selectedDurationMinutes,
soundPresetId: selectedPresetId,
goal: normalizedGoal,
override: selectionOverride,
@@ -53,7 +53,7 @@ export const useWorkspacePersistence = ({
resumeGoal,
selectedPresetId,
selectedScene.id,
selectedTimerPresetId,
selectedDurationMinutes,
selectionOverride,
showResumePrompt,
]);

View File

@@ -3,20 +3,16 @@ import {
normalizeSceneId,
SCENE_THEMES,
} from '@/entities/scene';
import {
SOUND_PRESETS,
TIMER_PRESETS,
type TimerPreset,
} from '@/entities/session';
import { SOUND_PRESETS } from '@/entities/session';
export type SelectionOverride = {
sound: boolean;
timer: boolean;
duration: boolean;
};
export interface StoredWorkspaceSelection {
sceneId?: string;
timerPresetId?: string;
durationMinutes?: number;
soundPresetId?: string;
goal?: string;
override?: Partial<SelectionOverride>;
@@ -85,56 +81,32 @@ export const resolveInitialSoundPreset = (
return SOUND_PRESETS[0].id;
};
export const TIMER_SELECTION_PRESETS = TIMER_PRESETS.filter(
(preset): preset is TimerPreset & { focusMinutes: number; breakMinutes: number } =>
typeof preset.focusMinutes === 'number' && typeof preset.breakMinutes === 'number',
).slice(0, 3);
export const DURATION_SELECTION_OPTIONS = [25, 45, 70, 90] as const;
export const resolveTimerLabelFromPresetId = (presetId?: string) => {
if (!presetId) {
return null;
}
const preset = TIMER_SELECTION_PRESETS.find((candidate) => candidate.id === presetId);
if (!preset) {
return null;
}
return preset.label;
};
export const resolveTimerPresetIdFromLabel = (timerLabel: string) => {
const preset = TIMER_SELECTION_PRESETS.find((candidate) => candidate.label === timerLabel);
return preset?.id;
};
export const resolveInitialTimerLabel = (
timerLabelFromQuery: string | null,
storedPresetId?: string,
recommendedPresetId?: string,
export const resolveInitialDurationMinutes = (
storedDurationMinutes?: number,
recommendedDurationMinutes?: number,
) => {
if (timerLabelFromQuery && TIMER_SELECTION_PRESETS.some((preset) => preset.label === timerLabelFromQuery)) {
return timerLabelFromQuery;
if (typeof storedDurationMinutes === 'number' && storedDurationMinutes >= 5 && storedDurationMinutes <= 180) {
return storedDurationMinutes;
}
const storedLabel = resolveTimerLabelFromPresetId(storedPresetId);
if (storedLabel) {
return storedLabel;
if (
typeof recommendedDurationMinutes === 'number' &&
recommendedDurationMinutes >= 5 &&
recommendedDurationMinutes <= 180
) {
return recommendedDurationMinutes;
}
const recommendedLabel = resolveTimerLabelFromPresetId(recommendedPresetId);
if (recommendedLabel) {
return recommendedLabel;
}
return TIMER_SELECTION_PRESETS[0]?.label ?? '25/5';
return 50;
};
export const resolveFocusTimeDisplayFromTimerLabel = (timerLabel: string) => {
const preset = TIMER_SELECTION_PRESETS.find((candidate) => candidate.label === timerLabel);
const focusMinutes = preset?.focusMinutes ?? 25;
return `${String(focusMinutes).padStart(2, '0')}:00`;
export const formatDurationMinutesLabel = (minutes: number) => {
return `${minutes}m`;
};
export const resolveFocusTimeDisplayFromDurationMinutes = (durationMinutes: number) => {
const safeMinutes = Number.isFinite(durationMinutes) ? Math.max(5, Math.min(180, durationMinutes)) : 25;
return `${String(safeMinutes).padStart(2, '0')}:00`;
};

View File

@@ -19,31 +19,24 @@ import { useHudStatusLine } from "@/shared/lib/useHudStatusLine";
import { copy } from "@/shared/i18n";
import { SpaceFocusHudWidget } from "@/widgets/space-focus-hud";
import { SpaceSetupDrawerWidget } from "@/widgets/space-setup-drawer";
import { useRouter, useSearchParams } from "next/navigation";
import {
findAtmosphereOptionForSelection,
getRecommendedDurationMinutes,
} from "@/widgets/focus-dashboard/model/atmosphereEntry";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import type { SessionEntryPoint, WorkspaceMode } from "../model/types";
import { useSpaceWorkspaceSelection } from "../model/useSpaceWorkspaceSelection";
import { useSpaceWorkspaceSessionControls } from "../model/useSpaceWorkspaceSessionControls";
import {
resolveFocusTimeDisplayFromTimerLabel,
resolveInitialSceneId,
resolveInitialSoundPreset,
resolveInitialTimerLabel,
TIMER_SELECTION_PRESETS,
DURATION_SELECTION_OPTIONS,
resolveFocusTimeDisplayFromDurationMinutes,
resolveInitialDurationMinutes,
} from "../model/workspaceSelection";
import { FocusTopToast } from "./FocusTopToast";
export const SpaceWorkspaceWidget = () => {
const searchParams = useSearchParams();
const router = useRouter();
const sceneQuery = searchParams.get("scene") ?? searchParams.get("room");
const goalQuery = searchParams.get("goal")?.trim() ?? "";
const focusPlanItemIdQuery = searchParams.get("planItemId");
const soundQuery = searchParams.get("sound");
const timerQuery = searchParams.get("timer");
const hasQueryOverrides = Boolean(
sceneQuery || goalQuery || focusPlanItemIdQuery || soundQuery || timerQuery,
);
const {
addThought,
@@ -59,31 +52,28 @@ export const SpaceWorkspaceWidget = () => {
const { isPro } = usePlanTier();
const { review, summary: weeklySummary } = useFocusStats();
const initialSceneId = useMemo(
() => resolveInitialSceneId(sceneQuery, undefined),
[sceneQuery],
);
const initialSceneId = useMemo(() => SCENE_THEMES[0].id, []);
const initialScene = useMemo(
() => getSceneById(initialSceneId) ?? SCENE_THEMES[0],
[initialSceneId],
);
const initialSoundPresetId = useMemo(
() =>
resolveInitialSoundPreset(
soundQuery,
undefined,
initialScene.recommendedSoundPresetId,
),
[initialScene.recommendedSoundPresetId, soundQuery],
() => initialScene.recommendedSoundPresetId,
[initialScene.recommendedSoundPresetId],
);
const initialTimerLabel = useMemo(
const initialAtmosphere = useMemo(
() =>
resolveInitialTimerLabel(
timerQuery,
findAtmosphereOptionForSelection(initialSceneId, initialSoundPresetId) ??
findAtmosphereOptionForSelection(initialSceneId),
[initialSceneId, initialSoundPresetId],
);
const initialDurationMinutes = useMemo(
() =>
resolveInitialDurationMinutes(
undefined,
initialScene.recommendedTimerPresetId,
initialAtmosphere ? getRecommendedDurationMinutes(initialAtmosphere) : undefined,
),
[initialScene.recommendedTimerPresetId, timerQuery],
[initialAtmosphere],
);
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>("setup");
@@ -143,14 +133,9 @@ export const SpaceWorkspaceWidget = () => {
const selection = useSpaceWorkspaceSelection({
initialSceneId,
initialGoal: goalQuery,
initialFocusPlanItemId: focusPlanItemIdQuery,
initialTimerLabel,
sceneQuery,
goalQuery,
soundQuery,
timerQuery,
hasQueryOverrides,
initialGoal: "",
initialFocusPlanItemId: null,
initialDurationMinutes,
currentSession,
sceneAssetMap,
selectedPresetId,
@@ -177,7 +162,7 @@ export const SpaceWorkspaceWidget = () => {
goalInput: selection.goalInput,
linkedFocusPlanItemId: selection.linkedFocusPlanItemId,
selectedSceneId: selection.selectedSceneId,
selectedTimerLabel: selection.selectedTimerLabel,
selectedDurationMinutes: selection.selectedDurationMinutes,
selectedPresetId,
soundPlaybackError,
pushStatusLine,
@@ -222,10 +207,10 @@ export const SpaceWorkspaceWidget = () => {
: undefined;
useEffect(() => {
if (!isBootstrapping && !currentSession && !hasQueryOverrides) {
if (!isBootstrapping && !currentSession) {
router.replace("/app");
}
}, [isBootstrapping, currentSession, hasQueryOverrides, router]);
}, [isBootstrapping, currentSession, router]);
useEffect(() => {
if (isBootstrapping || didResolveEntryRouteRef.current) {
@@ -237,7 +222,7 @@ export const SpaceWorkspaceWidget = () => {
if (!currentSession) {
return;
}
}, [currentSession, isBootstrapping, router]);
}, [currentSession, isBootstrapping]);
useEffect(() => {
const preferMobile =
@@ -255,7 +240,7 @@ export const SpaceWorkspaceWidget = () => {
const resolvedTimeDisplay =
timeDisplay ??
resolveFocusTimeDisplayFromTimerLabel(selection.selectedTimerLabel);
resolveFocusTimeDisplayFromDurationMinutes(selection.selectedDurationMinutes);
return (
<div className="relative h-dvh overflow-hidden text-white">
@@ -277,17 +262,17 @@ export const SpaceWorkspaceWidget = () => {
scenes={selection.setupScenes}
sceneAssetMap={sceneAssetMap}
selectedSceneId={selection.selectedScene.id}
selectedTimerLabel={selection.selectedTimerLabel}
selectedDurationLabel={selection.selectedDurationLabel}
selectedSoundPresetId={selectedPresetId}
goalInput={selection.goalInput}
selectedGoalId={selection.selectedGoalId}
goalChips={GOAL_CHIPS}
soundPresets={SOUND_PRESETS}
timerPresets={TIMER_SELECTION_PRESETS}
durationOptions={DURATION_SELECTION_OPTIONS}
canStart={selection.canStart}
onSceneSelect={selection.handleSelectScene}
onTimerSelect={(timerLabel) =>
selection.handleSelectTimer(timerLabel, true)
onDurationSelect={(durationMinutes) =>
selection.handleSelectDuration(durationMinutes, true)
}
onSoundSelect={(presetId) =>
selection.handleSelectSound(presetId, true)

View File

@@ -11,12 +11,8 @@ import { cn } from '@/shared/lib/cn';
const DEFAULT_STATS_SCENE_ID = getSceneById('forest')?.id ?? SCENE_THEMES[0].id;
const reviewStageSceneByPreset = (presetId: string) => {
if (presetId.startsWith('forest')) {
return getSceneById('forest') ?? SCENE_THEMES[0];
}
return getSceneById(DEFAULT_STATS_SCENE_ID) ?? SCENE_THEMES[0];
const reviewStageSceneByCarryForward = (sceneId: string) => {
return getSceneById(sceneId) ?? getSceneById(DEFAULT_STATS_SCENE_ID) ?? SCENE_THEMES[0];
};
export const StatsOverviewWidget = () => {
@@ -26,8 +22,8 @@ export const StatsOverviewWidget = () => {
const { review, isLoading, error, source, refetch } = useFocusStats();
const activeScene = useMemo(
() => reviewStageSceneByPreset(review.carryForward.presetId),
[review.carryForward.presetId],
() => reviewStageSceneByCarryForward(review.carryForward.sceneId),
[review.carryForward.sceneId],
);
const sourceLabel = source === 'api' ? stats.sourceApi : stats.sourceMock;
const syncLabel = error ? error : isLoading ? stats.loading : stats.synced;
@@ -221,9 +217,11 @@ export const StatsOverviewWidget = () => {
{isPro && <span className="text-white/30">PRO</span>}
</p>
<p className="mt-3 text-[15px] font-medium tracking-tight text-white/90">
{review.carryForward.presetLabel}
{review.carryForward.atmosphereLabel}
</p>
<p className="mt-1 text-[12px] text-white/50">
{review.carryForward.durationMinutes} · .
</p>
<p className="mt-1 text-[12px] text-white/50"> .</p>
</div>
</div>