feat(flow): paused resume gate와 auto-resume 연결
This commit is contained in:
@@ -42,9 +42,11 @@ const entryCopy = {
|
||||
resumeEyebrow: 'Resume',
|
||||
resumeRunning: '진행 중인 세션이 있어요.',
|
||||
resumePaused: '잠시 멈춘 세션이 있어요.',
|
||||
resumeCta: '이어서 들어가기',
|
||||
resumeCta: '이어서 몰입하기',
|
||||
resumeRefocusCta: '한 조각 다시 잡기',
|
||||
resumeRouting: '진행 중인 세션으로 돌아가는 중이에요.',
|
||||
resumeMicroStepLabel: '마지막 한 조각',
|
||||
resumePausedHint: '같은 목표를 바로 이어가거나, 다시 시작할 한 조각만 먼저 정리할 수 있어요.',
|
||||
resumeNewGoalHint: '새 목표는 현재 세션을 마무리한 뒤 시작할 수 있어요.',
|
||||
loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.',
|
||||
reviewEyebrow: 'Weekly Review',
|
||||
@@ -77,6 +79,8 @@ const inputShellClass =
|
||||
'w-full rounded-[1.75rem] border border-white/14 bg-white/[0.06] px-5 py-4 text-white outline-none transition focus:border-white/24 focus:bg-white/[0.09]';
|
||||
const primaryButtonClass =
|
||||
'inline-flex items-center justify-center rounded-full border border-white/16 bg-white/[0.14] px-6 py-3 text-sm font-medium text-white transition hover:bg-white/[0.18] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-48';
|
||||
const secondaryButtonClass =
|
||||
'inline-flex items-center justify-center rounded-full border border-white/10 bg-white/[0.06] px-5 py-3 text-sm font-medium text-white/84 transition hover:bg-white/[0.1] hover:text-white active:scale-[0.99]';
|
||||
|
||||
const timerLabelById: Record<string, string> = {
|
||||
'25-5': '25/5',
|
||||
@@ -262,6 +266,10 @@ export const FocusDashboardWidget = () => {
|
||||
router.push('/space?resume=continue');
|
||||
};
|
||||
|
||||
const handleResumeRefocus = () => {
|
||||
router.push('/space?resume=refocus');
|
||||
};
|
||||
|
||||
const shouldShowWeeklyReviewTeaser =
|
||||
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
|
||||
const shouldShowResumeReviewEntry =
|
||||
@@ -344,13 +352,19 @@ export const FocusDashboardWidget = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
||||
{entryCopy.resumeCta}
|
||||
</button>
|
||||
<p className="text-xs text-white/48 sm:text-right">{activeRitualMeta}</p>
|
||||
<div className="flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
||||
{entryCopy.resumeCta}
|
||||
</button>
|
||||
<button type="button" onClick={handleResumeRefocus} className={secondaryButtonClass}>
|
||||
{entryCopy.resumeRefocusCta}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-white/48 sm:max-w-[15rem] sm:text-right">{activeRitualMeta}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/56">{entryCopy.resumeNewGoalHint}</p>
|
||||
<p className="text-sm text-white/56">{entryCopy.resumePausedHint}</p>
|
||||
<p className="text-sm text-white/46">{entryCopy.resumeNewGoalHint}</p>
|
||||
|
||||
{shouldShowResumeReviewEntry ? (
|
||||
<Link
|
||||
|
||||
@@ -20,7 +20,9 @@ interface SpaceFocusHudWidgetProps {
|
||||
canStartSession?: boolean;
|
||||
canPauseSession?: boolean;
|
||||
canRestartSession?: boolean;
|
||||
entryOverlayIntent?: 'resume-refocus' | null;
|
||||
returnPromptMode?: 'focus' | 'break' | null;
|
||||
onEntryOverlayIntentHandled?: () => void;
|
||||
onStartRequested?: () => void;
|
||||
onPauseRequested?: () => void;
|
||||
onRestartRequested?: () => void;
|
||||
@@ -42,7 +44,9 @@ export const SpaceFocusHudWidget = ({
|
||||
canStartSession = false,
|
||||
canPauseSession = false,
|
||||
canRestartSession = false,
|
||||
entryOverlayIntent = null,
|
||||
returnPromptMode = null,
|
||||
onEntryOverlayIntentHandled,
|
||||
onStartRequested,
|
||||
onPauseRequested,
|
||||
onRestartRequested,
|
||||
@@ -176,6 +180,15 @@ export const SpaceFocusHudWidget = ({
|
||||
}
|
||||
}, [normalizedMicroStep, overlay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (entryOverlayIntent !== 'resume-refocus' || !hasActiveSession || overlay !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
openRefocus('microStep', 'manual');
|
||||
onEntryOverlayIntentHandled?.();
|
||||
}, [entryOverlayIntent, hasActiveSession, onEntryOverlayIntentHandled, openRefocus, overlay]);
|
||||
|
||||
const handleOpenCompleteSheet = (preferredView: 'choice' | 'next' = 'choice') => {
|
||||
setIntentError(null);
|
||||
setCompletePreferredView(preferredView);
|
||||
|
||||
@@ -104,6 +104,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||
useState<SessionEntryPoint>("space-setup");
|
||||
const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false);
|
||||
const [hasConsumedEntryOverlayIntent, setHasConsumedEntryOverlayIntent] = useState(false);
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -209,6 +210,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
setSelectedGoalId: selection.setSelectedGoalId,
|
||||
setShowResumePrompt: selection.setShowResumePrompt,
|
||||
});
|
||||
const handleStartRequested = controls.handleStartRequested;
|
||||
|
||||
const awayReturnRecovery = useAwayReturnRecovery({
|
||||
currentSession,
|
||||
@@ -226,6 +228,13 @@ export const SpaceWorkspaceWidget = () => {
|
||||
const allowsPausedReentry =
|
||||
resumeIntent === "continue" || resumeIntent === "refocus";
|
||||
const didResolveEntryRouteRef = useRef(false);
|
||||
const didHandleResumeIntentRef = useRef(false);
|
||||
const entryOverlayIntent =
|
||||
!hasConsumedEntryOverlayIntent &&
|
||||
resumeIntent === "refocus" &&
|
||||
currentSession?.state === "paused"
|
||||
? "resume-refocus"
|
||||
: null;
|
||||
const secondaryReviewTeaser = shouldShowSecondaryReviewTeaser
|
||||
? {
|
||||
title: isPro
|
||||
@@ -262,6 +271,28 @@ export const SpaceWorkspaceWidget = () => {
|
||||
}
|
||||
}, [allowsPausedReentry, currentSession, isBootstrapping, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isBootstrapping ||
|
||||
!currentSession ||
|
||||
currentSession.state !== "paused" ||
|
||||
didHandleResumeIntentRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resumeIntent === "continue") {
|
||||
didHandleResumeIntentRef.current = true;
|
||||
router.replace("/space");
|
||||
void handleStartRequested();
|
||||
return;
|
||||
}
|
||||
|
||||
if (resumeIntent === "refocus") {
|
||||
return;
|
||||
}
|
||||
}, [currentSession, handleStartRequested, isBootstrapping, resumeIntent, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const preferMobile =
|
||||
typeof window !== "undefined"
|
||||
@@ -355,14 +386,19 @@ export const SpaceWorkspaceWidget = () => {
|
||||
hasActiveSession={Boolean(currentSession)}
|
||||
playbackState={resolvedPlaybackState}
|
||||
sessionPhase={phase ?? 'focus'}
|
||||
isSessionActionPending={isSessionMutating}
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
canRestartSession={controls.canRestartSession}
|
||||
returnPromptMode={awayReturnRecovery.returnPromptMode}
|
||||
onStartRequested={() => {
|
||||
void controls.handleStartRequested();
|
||||
}}
|
||||
isSessionActionPending={isSessionMutating}
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
canRestartSession={controls.canRestartSession}
|
||||
entryOverlayIntent={entryOverlayIntent}
|
||||
returnPromptMode={awayReturnRecovery.returnPromptMode}
|
||||
onEntryOverlayIntentHandled={() => {
|
||||
setHasConsumedEntryOverlayIntent(true);
|
||||
router.replace("/space");
|
||||
}}
|
||||
onStartRequested={() => {
|
||||
void handleStartRequested();
|
||||
}}
|
||||
onPauseRequested={() => {
|
||||
void controls.handlePauseRequested();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user