fix(space): remove stale setup drawer flow
This commit is contained in:
@@ -92,7 +92,7 @@ export interface StartFocusSessionRequest {
|
||||
soundPresetId?: string | null;
|
||||
focusPlanItemId?: string;
|
||||
microStep?: string | null;
|
||||
entryPoint?: 'space-setup' | 'goal-complete' | 'resume-restore';
|
||||
entryPoint?: 'space-setup' | 'goal-complete';
|
||||
}
|
||||
|
||||
export interface CompleteFocusSessionRequest {
|
||||
|
||||
@@ -9,28 +9,6 @@ export const spaceEn = {
|
||||
placeholder: 'e.g. Draft the first page of the contract',
|
||||
hint: 'Keep it small. Just the next concrete piece.',
|
||||
},
|
||||
setup: {
|
||||
...koSpace.setup,
|
||||
panelAriaLabel: 'Focus entry panel',
|
||||
eyebrow: 'Execution Setup',
|
||||
title: 'Set the goal, time, and atmosphere. Then step in.',
|
||||
description: 'Pick a goal, background, timer, and sound. Then move straight into the focus stage.',
|
||||
resumeTitle: 'Pick up the last block',
|
||||
startFresh: 'Start fresh',
|
||||
resumePrepare: 'Prepare to resume',
|
||||
sceneLabel: 'Background',
|
||||
timerLabel: 'Time',
|
||||
soundLabel: 'Sound',
|
||||
reviewTeaserEyebrow: 'Weekly Review',
|
||||
reviewTeaserTitle: 'Take another look at this week?',
|
||||
reviewTeaserTitlePro: 'Review this week and reopen the rhythm that worked best?',
|
||||
reviewTeaserHelper: 'You can jump right back in, or pause for a quick weekly review first.',
|
||||
reviewTeaserHelperPro: 'You can jump right back in, or check this week’s flow and recommended atmosphere first.',
|
||||
reviewTeaserCta: 'Open weekly review',
|
||||
reviewTeaserDismiss: 'Later',
|
||||
readyHint: 'Add a goal to unlock the start flow.',
|
||||
openFocusScreen: 'Open focus stage',
|
||||
},
|
||||
timerHud: {
|
||||
...koSpace.timerHud,
|
||||
actions: [
|
||||
|
||||
@@ -5,27 +5,6 @@ export const space = {
|
||||
placeholder: '예: 계약서 1페이지 정리',
|
||||
hint: '크게 말고, 바로 다음 한 조각.',
|
||||
},
|
||||
setup: {
|
||||
panelAriaLabel: '집중 시작 패널',
|
||||
eyebrow: 'Execution Setup',
|
||||
title: '이번 세션만 가볍게 맞추고 들어가요.',
|
||||
description: '목표, 배경, 타이머, 사운드만 정하고 바로 실행 화면으로 들어갑니다.',
|
||||
resumeTitle: '지난 한 조각 이어서',
|
||||
startFresh: '새로 시작',
|
||||
resumePrepare: '이어서 준비',
|
||||
sceneLabel: '배경',
|
||||
timerLabel: '타이머',
|
||||
soundLabel: '사운드',
|
||||
reviewTeaserEyebrow: 'Weekly Review',
|
||||
reviewTeaserTitle: '이번 주 review를 다시 볼까요?',
|
||||
reviewTeaserTitlePro: '이번 주 흐름과 잘 맞았던 ritual을 다시 볼까요?',
|
||||
reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.',
|
||||
reviewTeaserHelperPro: '원하면 이번 주 흐름과 추천 ritual을 다시 보고 다음 세션으로 이어갈 수 있어요.',
|
||||
reviewTeaserCta: '주간 review 보기',
|
||||
reviewTeaserDismiss: '나중에',
|
||||
readyHint: '목표를 적으면 시작할 수 있어요.',
|
||||
openFocusScreen: '실행 화면 열기',
|
||||
},
|
||||
timerHud: {
|
||||
actions: [
|
||||
{ id: 'start', label: '시작', icon: '▶' },
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ui/SpaceSetupDrawerWidget';
|
||||
@@ -1,355 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||
import type { SceneAssetMap } from '@/entities/media';
|
||||
import type { SceneTheme } from '@/entities/scene';
|
||||
import type {
|
||||
GoalChip,
|
||||
SoundPreset,
|
||||
} from '@/entities/session';
|
||||
import { copy } from '@/shared/i18n';
|
||||
import { SceneSelectCarousel } from '@/features/scene-select';
|
||||
import { SessionGoalField } from '@/features/session-goal';
|
||||
import { Button } from '@/shared/ui';
|
||||
import { cn } from '@/shared/lib/cn';
|
||||
|
||||
type SelectionPopover = 'space' | 'timer' | 'sound';
|
||||
|
||||
interface SpaceSetupDrawerWidgetProps {
|
||||
open: boolean;
|
||||
scenes: SceneTheme[];
|
||||
sceneAssetMap?: SceneAssetMap;
|
||||
selectedSceneId: string;
|
||||
selectedDurationLabel: string;
|
||||
selectedSoundPresetId: string;
|
||||
goalInput: string;
|
||||
selectedGoalId: string | null;
|
||||
goalChips: GoalChip[];
|
||||
soundPresets: SoundPreset[];
|
||||
durationOptions: readonly number[];
|
||||
canStart: boolean;
|
||||
onSceneSelect: (sceneId: string) => void;
|
||||
onDurationSelect: (durationMinutes: number) => void;
|
||||
onSoundSelect: (soundPresetId: string) => void;
|
||||
onGoalChange: (value: string) => void;
|
||||
onGoalChipSelect: (chip: GoalChip) => void;
|
||||
onStart: () => void;
|
||||
resumeHint?: {
|
||||
goal: string;
|
||||
onResume: () => void;
|
||||
onStartFresh: () => void;
|
||||
};
|
||||
reviewTeaser?: {
|
||||
title: string;
|
||||
summary: string;
|
||||
ctaHref: string;
|
||||
ctaLabel: string;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface SummaryChipProps {
|
||||
label: string;
|
||||
value: string;
|
||||
open: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const SummaryChip = ({ label, value, open, onClick }: SummaryChipProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[11px] transition-colors',
|
||||
open
|
||||
? 'border-sky-200/42 bg-sky-200/14 text-white/92'
|
||||
: 'border-white/14 bg-white/[0.04] text-white/80 hover:bg-white/[0.09]',
|
||||
)}
|
||||
>
|
||||
<span className="text-white/62">{label}</span>
|
||||
<span className="max-w-[124px] truncate text-white/92">{value}</span>
|
||||
<span aria-hidden className="text-white/56">▾</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const SpaceSetupDrawerWidget = ({
|
||||
open,
|
||||
scenes,
|
||||
sceneAssetMap,
|
||||
selectedSceneId,
|
||||
selectedDurationLabel,
|
||||
selectedSoundPresetId,
|
||||
goalInput,
|
||||
selectedGoalId,
|
||||
goalChips,
|
||||
soundPresets,
|
||||
durationOptions,
|
||||
canStart,
|
||||
onSceneSelect,
|
||||
onDurationSelect,
|
||||
onSoundSelect,
|
||||
onGoalChange,
|
||||
onGoalChipSelect,
|
||||
onStart,
|
||||
resumeHint,
|
||||
reviewTeaser,
|
||||
}: SpaceSetupDrawerWidgetProps) => {
|
||||
const { setup } = copy.space;
|
||||
const [openPopover, setOpenPopover] = useState<SelectionPopover | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const selectedScene = useMemo(() => {
|
||||
return scenes.find((scene) => scene.id === selectedSceneId) ?? scenes[0];
|
||||
}, [scenes, selectedSceneId]);
|
||||
|
||||
const selectedSoundLabel = useMemo(() => {
|
||||
return (
|
||||
soundPresets.find((preset) => preset.id === selectedSoundPresetId)?.label ??
|
||||
soundPresets[0]?.label ??
|
||||
copy.common.default
|
||||
);
|
||||
}, [selectedSoundPresetId, soundPresets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!openPopover) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpenPopover(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
|
||||
if (!panelRef.current?.contains(target)) {
|
||||
setOpenPopover(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
document.addEventListener('mousedown', handlePointerDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.removeEventListener('mousedown', handlePointerDown);
|
||||
};
|
||||
}, [openPopover]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const togglePopover = (popover: SelectionPopover) => {
|
||||
setOpenPopover((current) => (current === popover ? null : popover));
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!canStart) {
|
||||
return;
|
||||
}
|
||||
|
||||
onStart();
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
className="fixed left-1/2 top-1/2 z-40 w-[min(428px,92vw)] -translate-x-1/2 -translate-y-1/2"
|
||||
aria-label={setup.panelAriaLabel}
|
||||
>
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="rounded-3xl border border-white/14 bg-[linear-gradient(160deg,rgba(15,23,42,0.68)_0%,rgba(8,13,27,0.56)_100%)] p-4 text-white shadow-[0_22px_52px_rgba(2,6,23,0.38)] backdrop-blur-2xl sm:p-5"
|
||||
>
|
||||
<header className="mb-3 space-y-1">
|
||||
<p className="text-[10px] uppercase tracking-[0.18em] text-white/48">{setup.eyebrow}</p>
|
||||
<h1 className="text-[1.45rem] font-semibold leading-tight text-white">{setup.title}</h1>
|
||||
<p className="text-xs text-white/60">{setup.description}</p>
|
||||
</header>
|
||||
|
||||
{resumeHint ? (
|
||||
<div className="mb-3 rounded-2xl border border-white/14 bg-black/22 px-3 py-2.5">
|
||||
<p className="text-[11px] text-white/62">{setup.resumeTitle}</p>
|
||||
<p className="mt-1 truncate text-sm text-white/88">{resumeHint.goal}</p>
|
||||
<div className="mt-2 flex items-center justify-end gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={resumeHint.onStartFresh}
|
||||
className="rounded-full border border-white/16 bg-white/[0.04] px-2.5 py-1 text-[11px] text-white/72 transition-colors hover:bg-white/[0.1]"
|
||||
>
|
||||
{setup.startFresh}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resumeHint.onResume}
|
||||
className="rounded-full border border-sky-200/34 bg-sky-200/14 px-2.5 py-1 text-[11px] text-white/90 transition-colors hover:bg-sky-200/22"
|
||||
>
|
||||
{setup.resumePrepare}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="relative mb-3">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<SummaryChip
|
||||
label={setup.sceneLabel}
|
||||
value={selectedScene?.name ?? copy.common.defaultBackground}
|
||||
open={openPopover === 'space'}
|
||||
onClick={() => togglePopover('space')}
|
||||
/>
|
||||
<SummaryChip
|
||||
label={setup.timerLabel}
|
||||
value={selectedDurationLabel}
|
||||
open={openPopover === 'timer'}
|
||||
onClick={() => togglePopover('timer')}
|
||||
/>
|
||||
<SummaryChip
|
||||
label={setup.soundLabel}
|
||||
value={selectedSoundLabel}
|
||||
open={openPopover === 'sound'}
|
||||
onClick={() => togglePopover('sound')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{openPopover === 'space' ? (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+0.5rem)] z-20 rounded-2xl border border-white/14 bg-slate-950/80 p-2.5 shadow-[0_18px_44px_rgba(2,6,23,0.4)] backdrop-blur-xl animate-[popover-rise_220ms_ease-out] motion-reduce:animate-none">
|
||||
<SceneSelectCarousel
|
||||
scenes={scenes.slice(0, 4)}
|
||||
selectedSceneId={selectedSceneId}
|
||||
sceneAssetMap={sceneAssetMap}
|
||||
onSelect={(sceneId) => {
|
||||
onSceneSelect(sceneId);
|
||||
setOpenPopover(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{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">
|
||||
{durationOptions.map((minutes) => {
|
||||
const selected = `${minutes}m` === selectedDurationLabel;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={minutes}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onDurationSelect(minutes);
|
||||
setOpenPopover(null);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-full border px-2.5 py-0.5 text-[10px] transition-colors',
|
||||
selected
|
||||
? 'border-sky-200/34 bg-sky-200/14 text-white/90'
|
||||
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
|
||||
)}
|
||||
>
|
||||
{minutes}m
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{openPopover === 'sound' ? (
|
||||
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-20 w-[min(300px,88vw)] 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">
|
||||
{soundPresets.slice(0, 6).map((preset) => {
|
||||
const selected = preset.id === selectedSoundPresetId;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSoundSelect(preset.id);
|
||||
setOpenPopover(null);
|
||||
}}
|
||||
className={cn(
|
||||
'rounded-full border px-2.5 py-0.5 text-[10px] transition-colors',
|
||||
selected
|
||||
? 'border-sky-200/34 bg-sky-200/14 text-white/90'
|
||||
: 'border-white/12 bg-white/[0.03] text-white/66 hover:bg-white/8',
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<form id="space-setup-form" className="space-y-3" onSubmit={handleSubmit}>
|
||||
<SessionGoalField
|
||||
autoFocus={open}
|
||||
goalInput={goalInput}
|
||||
selectedGoalId={selectedGoalId}
|
||||
goalChips={goalChips}
|
||||
onGoalChange={onGoalChange}
|
||||
onGoalChipSelect={onGoalChipSelect}
|
||||
/>
|
||||
|
||||
<div className="space-y-1.5 pt-1">
|
||||
{!canStart ? <p className="text-[10px] text-white/56">{setup.readyHint}</p> : null}
|
||||
<Button
|
||||
type="submit"
|
||||
form="space-setup-form"
|
||||
size="full"
|
||||
disabled={!canStart}
|
||||
className={cn(
|
||||
'h-10 rounded-xl !bg-sky-300/84 !text-slate-900 shadow-[0_8px_16px_rgba(125,211,252,0.24)] hover:!bg-sky-300 disabled:!bg-white/10 disabled:!text-white/42',
|
||||
)}
|
||||
>
|
||||
{setup.openFocusScreen}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{reviewTeaser ? (
|
||||
<div className="mt-3 rounded-[1.25rem] border border-white/10 bg-black/16 px-3.5 py-3 backdrop-blur-md">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-[0.16em] text-white/42">
|
||||
{setup.reviewTeaserEyebrow}
|
||||
</p>
|
||||
<p className="mt-1 text-[13px] font-medium leading-[1.5] text-white/88">
|
||||
{reviewTeaser.title}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] leading-[1.55] text-white/56">
|
||||
{reviewTeaser.summary}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reviewTeaser.onDismiss}
|
||||
className="shrink-0 rounded-full border border-white/12 bg-white/[0.04] px-2 py-1 text-[10px] text-white/62 transition hover:bg-white/[0.09] hover:text-white/84"
|
||||
>
|
||||
{setup.reviewTeaserDismiss}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={reviewTeaser.ctaHref}
|
||||
className="mt-3 inline-flex items-center rounded-full border border-white/12 bg-white/[0.06] px-3 py-1.5 text-[11px] font-medium text-white/82 transition hover:bg-white/[0.1] hover:text-white"
|
||||
>
|
||||
{reviewTeaser.ctaLabel}
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export type WorkspaceMode = 'setup' | 'focus';
|
||||
export type SessionEntryPoint = 'space-setup' | 'goal-complete' | 'resume-restore';
|
||||
export type SessionEntryPoint = 'space-setup' | 'goal-complete';
|
||||
|
||||
export type SelectionOverride = {
|
||||
sound: boolean;
|
||||
@@ -10,6 +10,5 @@ export interface StoredWorkspaceSelection {
|
||||
sceneId?: string;
|
||||
durationMinutes?: number;
|
||||
soundPresetId?: string;
|
||||
goal?: string;
|
||||
override?: Partial<SelectionOverride>;
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
const [goalInput, setGoalInput] = useState(initialGoal);
|
||||
const [linkedFocusPlanItemId, setLinkedFocusPlanItemId] = useState<string | null>(initialFocusPlanItemId);
|
||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||
const [resumeGoal, setResumeGoal] = useState('');
|
||||
const [showResumePrompt, setShowResumePrompt] = useState(false);
|
||||
const [hasHydratedSelection, setHasHydratedSelection] = useState(false);
|
||||
const [selectionOverride, setSelectionOverride] = useState<SelectionOverride>({
|
||||
sound: false,
|
||||
@@ -290,24 +288,19 @@ export const useSpaceWorkspaceSelection = ({
|
||||
]);
|
||||
|
||||
const handleGoalChipSelect = useCallback((chip: GoalChip) => {
|
||||
setShowResumePrompt(false);
|
||||
setLinkedFocusPlanItemId(null);
|
||||
setSelectedGoalId(chip.id);
|
||||
setGoalInput(chip.label);
|
||||
}, []);
|
||||
|
||||
const handleGoalChange = useCallback((value: string) => {
|
||||
if (showResumePrompt) {
|
||||
setShowResumePrompt(false);
|
||||
}
|
||||
|
||||
setLinkedFocusPlanItemId(null);
|
||||
setGoalInput(value);
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
setSelectedGoalId(null);
|
||||
}
|
||||
}, [showResumePrompt]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const storedSelection = readStoredWorkspaceSelection();
|
||||
@@ -325,7 +318,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
storedSelection.soundPresetId && SOUND_PRESETS.some((preset) => preset.id === storedSelection.soundPresetId)
|
||||
? storedSelection.soundPresetId
|
||||
: null;
|
||||
const restoredGoal = storedSelection.goal?.trim() ?? '';
|
||||
const rafId = window.requestAnimationFrame(() => {
|
||||
setSelectionOverride(restoredSelectionOverride);
|
||||
|
||||
@@ -341,11 +333,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setSelectedPresetId(restoredSoundPresetId);
|
||||
}
|
||||
|
||||
if (restoredGoal.length > 0) {
|
||||
setResumeGoal(restoredGoal);
|
||||
setShowResumePrompt(true);
|
||||
}
|
||||
|
||||
setHasHydratedSelection(true);
|
||||
});
|
||||
|
||||
@@ -428,7 +415,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setGoalInput(currentSession.goal);
|
||||
setLinkedFocusPlanItemId(currentSession.focusPlanItemId ?? null);
|
||||
setSelectedGoalId(null);
|
||||
setShowResumePrompt(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -442,8 +428,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
selectedDurationMinutes,
|
||||
selectedPresetId,
|
||||
goalInput,
|
||||
showResumePrompt,
|
||||
resumeGoal,
|
||||
selectionOverride,
|
||||
});
|
||||
|
||||
@@ -463,8 +447,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
goalInput,
|
||||
linkedFocusPlanItemId,
|
||||
selectedGoalId,
|
||||
resumeGoal,
|
||||
showResumePrompt,
|
||||
hasHydratedSelection,
|
||||
selectionOverride,
|
||||
selectedScene,
|
||||
@@ -474,8 +456,6 @@ export const useSpaceWorkspaceSelection = ({
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
setResumeGoal,
|
||||
handleSelectScene,
|
||||
handleSelectDuration,
|
||||
handleSelectSound,
|
||||
|
||||
@@ -61,7 +61,6 @@ interface UseSpaceWorkspaceSessionControlsParams {
|
||||
setGoalInput: (value: string) => void;
|
||||
setLinkedFocusPlanItemId: (value: string | null) => void;
|
||||
setSelectedGoalId: (value: string | null) => void;
|
||||
setShowResumePrompt: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useSpaceWorkspaceSessionControls = ({
|
||||
@@ -94,7 +93,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
}: UseSpaceWorkspaceSessionControlsParams) => {
|
||||
const queuedFocusStatusMessageRef = useRef<string | null>(null);
|
||||
const lastSoundPlaybackErrorRef = useRef<string | null>(null);
|
||||
@@ -115,12 +113,11 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint(entryPoint);
|
||||
setPreviewPlaybackState('paused');
|
||||
setWorkspaceMode('focus');
|
||||
queuedFocusStatusMessageRef.current = copy.space.workspace.readyToStart;
|
||||
}, [setPendingSessionEntryPoint, setPreviewPlaybackState, setShowResumePrompt, setWorkspaceMode]);
|
||||
}, [setPendingSessionEntryPoint, setPreviewPlaybackState, setWorkspaceMode]);
|
||||
|
||||
const startFocusFlow = useCallback(async () => {
|
||||
const trimmedGoal = goalInput.trim();
|
||||
@@ -266,7 +263,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
setGoalInput(trimmedNextGoal);
|
||||
setLinkedFocusPlanItemId(nextState.nextSession.focusPlanItemId ?? null);
|
||||
setSelectedGoalId(null);
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint('goal-complete');
|
||||
setPreviewPlaybackState('running');
|
||||
setWorkspaceMode('focus');
|
||||
@@ -289,7 +285,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
setPendingSessionEntryPoint,
|
||||
setPreviewPlaybackState,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
setWorkspaceMode,
|
||||
unlockPlayback,
|
||||
]);
|
||||
@@ -313,7 +308,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint('space-setup');
|
||||
setPreviewPlaybackState('paused');
|
||||
setWorkspaceMode('setup');
|
||||
@@ -325,7 +319,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
pushStatusLine,
|
||||
setPendingSessionEntryPoint,
|
||||
setPreviewPlaybackState,
|
||||
setShowResumePrompt,
|
||||
setWorkspaceMode,
|
||||
]);
|
||||
|
||||
@@ -348,7 +341,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint('space-setup');
|
||||
setPreviewPlaybackState('paused');
|
||||
setWorkspaceMode('setup');
|
||||
@@ -360,7 +352,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
pushStatusLine,
|
||||
setPendingSessionEntryPoint,
|
||||
setPreviewPlaybackState,
|
||||
setShowResumePrompt,
|
||||
setWorkspaceMode,
|
||||
]);
|
||||
|
||||
@@ -383,7 +374,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
setShowResumePrompt(false);
|
||||
setPendingSessionEntryPoint('space-setup');
|
||||
setPreviewPlaybackState('paused');
|
||||
setWorkspaceMode('setup');
|
||||
@@ -395,7 +385,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
pushStatusLine,
|
||||
setPendingSessionEntryPoint,
|
||||
setPreviewPlaybackState,
|
||||
setShowResumePrompt,
|
||||
setWorkspaceMode,
|
||||
]);
|
||||
|
||||
@@ -477,7 +466,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
setGoalInput(updatedSession.goal);
|
||||
setLinkedFocusPlanItemId(updatedSession.focusPlanItemId ?? null);
|
||||
setSelectedGoalId(null);
|
||||
setShowResumePrompt(false);
|
||||
return true;
|
||||
}, [
|
||||
currentSession,
|
||||
@@ -485,7 +473,6 @@ export const useSpaceWorkspaceSessionControls = ({
|
||||
setGoalInput,
|
||||
setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId,
|
||||
setShowResumePrompt,
|
||||
updateCurrentIntent,
|
||||
]);
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ interface UseWorkspacePersistenceParams {
|
||||
selectedDurationMinutes: number;
|
||||
selectedPresetId: string;
|
||||
goalInput: string;
|
||||
showResumePrompt: boolean;
|
||||
resumeGoal: string;
|
||||
selectionOverride: SelectionOverride;
|
||||
}
|
||||
|
||||
@@ -22,8 +20,6 @@ export const useWorkspacePersistence = ({
|
||||
selectedDurationMinutes,
|
||||
selectedPresetId,
|
||||
goalInput,
|
||||
showResumePrompt,
|
||||
resumeGoal,
|
||||
selectionOverride,
|
||||
}: UseWorkspacePersistenceParams) => {
|
||||
useEffect(() => {
|
||||
@@ -31,30 +27,21 @@ export const useWorkspacePersistence = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedGoal = goalInput.trim().length > 0
|
||||
? goalInput.trim()
|
||||
: showResumePrompt
|
||||
? resumeGoal
|
||||
: '';
|
||||
|
||||
window.localStorage.setItem(
|
||||
WORKSPACE_SELECTION_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
sceneId: selectedScene.id,
|
||||
durationMinutes: selectedDurationMinutes,
|
||||
soundPresetId: selectedPresetId,
|
||||
goal: normalizedGoal,
|
||||
override: selectionOverride,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
goalInput,
|
||||
hasHydratedSelection,
|
||||
resumeGoal,
|
||||
selectedPresetId,
|
||||
selectedScene.id,
|
||||
selectedDurationMinutes,
|
||||
selectionOverride,
|
||||
showResumePrompt,
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface StoredWorkspaceSelection {
|
||||
sceneId?: string;
|
||||
durationMinutes?: number;
|
||||
soundPresetId?: string;
|
||||
goal?: string;
|
||||
override?: Partial<SelectionOverride>;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,24 +6,20 @@ import {
|
||||
preloadAssetImage,
|
||||
useMediaCatalog,
|
||||
} from "@/entities/media";
|
||||
import { usePlanTier } from "@/entities/plan";
|
||||
import { getSceneById, SCENE_THEMES } from "@/entities/scene";
|
||||
import { GOAL_CHIPS, SOUND_PRESETS, useThoughtInbox } from "@/entities/session";
|
||||
import { useThoughtInbox } from "@/entities/session";
|
||||
import {
|
||||
focusSessionApi,
|
||||
type CompletionResult,
|
||||
type CurrentSessionThought,
|
||||
useFocusSessionEngine,
|
||||
} from "@/features/focus-session";
|
||||
import { useFocusStats } from "@/features/stats";
|
||||
import {
|
||||
useSoundPlayback,
|
||||
useSoundPresetSelection,
|
||||
} from "@/features/sound-preset";
|
||||
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 { CompletionResultModal } from "@/widgets/space-focus-hud/ui/CompletionResultModal";
|
||||
import {
|
||||
findAtmosphereOptionForSelection,
|
||||
@@ -35,7 +31,6 @@ import type { SessionEntryPoint, WorkspaceMode } from "../model/types";
|
||||
import { useSpaceWorkspaceSelection } from "../model/useSpaceWorkspaceSelection";
|
||||
import { useSpaceWorkspaceSessionControls } from "../model/useSpaceWorkspaceSessionControls";
|
||||
import {
|
||||
DURATION_SELECTION_OPTIONS,
|
||||
resolveFocusTimeDisplayFromDurationMinutes,
|
||||
resolveInitialDurationMinutes,
|
||||
} from "../model/workspaceSelection";
|
||||
@@ -55,8 +50,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
usedFallbackManifest,
|
||||
hasResolvedManifest,
|
||||
} = useMediaCatalog();
|
||||
const { isPro } = usePlanTier();
|
||||
const { review, summary: weeklySummary } = useFocusStats();
|
||||
|
||||
const initialSceneId = useMemo(() => SCENE_THEMES[0].id, []);
|
||||
const initialScene = useMemo(
|
||||
@@ -88,7 +81,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
>("paused");
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||
useState<SessionEntryPoint>("space-setup");
|
||||
const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false);
|
||||
const [, setCurrentSessionThoughts] = useState<CurrentSessionThought[]>([]);
|
||||
const [pendingCompletionResult, setPendingCompletionResult] = useState<CompletionResult | null>(null);
|
||||
|
||||
@@ -190,31 +182,8 @@ export const SpaceWorkspaceWidget = () => {
|
||||
setGoalInput: selection.setGoalInput,
|
||||
setLinkedFocusPlanItemId: selection.setLinkedFocusPlanItemId,
|
||||
setSelectedGoalId: selection.setSelectedGoalId,
|
||||
setShowResumePrompt: selection.setShowResumePrompt,
|
||||
});
|
||||
|
||||
const hasEnoughWeeklyData =
|
||||
weeklySummary.last7Days.startedSessions >= 3 &&
|
||||
(weeklySummary.last7Days.completedSessions >= 2 ||
|
||||
weeklySummary.recovery.pausedSessions > 0);
|
||||
const shouldShowSecondaryReviewTeaser =
|
||||
workspaceMode === "setup" &&
|
||||
showReviewTeaserAfterComplete &&
|
||||
hasEnoughWeeklyData;
|
||||
const didResolveEntryRouteRef = useRef(false);
|
||||
const secondaryReviewTeaser = shouldShowSecondaryReviewTeaser
|
||||
? {
|
||||
title: isPro
|
||||
? copy.space.setup.reviewTeaserTitlePro
|
||||
: copy.space.setup.reviewTeaserTitle,
|
||||
summary: isPro
|
||||
? review.carryForward.keepDoing
|
||||
: copy.space.setup.reviewTeaserHelper,
|
||||
ctaHref: "/stats",
|
||||
ctaLabel: copy.space.setup.reviewTeaserCta,
|
||||
onDismiss: () => setShowReviewTeaserAfterComplete(false),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBootstrapping && !currentSession && !pendingCompletionResult) {
|
||||
@@ -302,58 +271,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
<main className="relative flex-1" />
|
||||
</div>
|
||||
|
||||
<SpaceSetupDrawerWidget
|
||||
open={!isFocusMode && !isCompletionResultOpen}
|
||||
scenes={selection.setupScenes}
|
||||
sceneAssetMap={sceneAssetMap}
|
||||
selectedSceneId={selection.selectedScene.id}
|
||||
selectedDurationLabel={selection.selectedDurationLabel}
|
||||
selectedSoundPresetId={selectedPresetId}
|
||||
goalInput={selection.goalInput}
|
||||
selectedGoalId={selection.selectedGoalId}
|
||||
goalChips={GOAL_CHIPS}
|
||||
soundPresets={SOUND_PRESETS}
|
||||
durationOptions={DURATION_SELECTION_OPTIONS}
|
||||
canStart={selection.canStart}
|
||||
onSceneSelect={selection.handleSelectScene}
|
||||
onDurationSelect={(durationMinutes) =>
|
||||
selection.handleSelectDuration(durationMinutes, true)
|
||||
}
|
||||
onSoundSelect={(presetId) =>
|
||||
selection.handleSelectSound(presetId, true)
|
||||
}
|
||||
onGoalChange={selection.handleGoalChange}
|
||||
onGoalChipSelect={selection.handleGoalChipSelect}
|
||||
onStart={() => {
|
||||
setShowReviewTeaserAfterComplete(false);
|
||||
controls.handleSetupFocusOpen();
|
||||
}}
|
||||
reviewTeaser={secondaryReviewTeaser}
|
||||
resumeHint={
|
||||
selection.showResumePrompt && selection.resumeGoal
|
||||
? {
|
||||
goal: selection.resumeGoal,
|
||||
onResume: () => {
|
||||
setShowReviewTeaserAfterComplete(false);
|
||||
selection.setGoalInput(selection.resumeGoal);
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
controls.openFocusMode(
|
||||
selection.resumeGoal,
|
||||
"resume-restore",
|
||||
);
|
||||
},
|
||||
onStartFresh: () => {
|
||||
setShowReviewTeaserAfterComplete(false);
|
||||
selection.setGoalInput("");
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{isFocusMode ? (
|
||||
<SpaceFocusHudWidget
|
||||
sessionId={currentSession?.id ?? null}
|
||||
@@ -429,7 +346,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
onClose={() => {
|
||||
setPendingCompletionResult(null);
|
||||
setCurrentSessionThoughts([]);
|
||||
router.replace('/app');
|
||||
void router.replace('/app');
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user