fix(flow): app entry를 no-session 전용으로 단순화
This commit is contained in:
@@ -262,7 +262,7 @@ const buildCarryForward = (summary: FocusStatsSummary): WeeklyReviewViewModel['c
|
||||
return {
|
||||
hintKey,
|
||||
presetId: 'forest-50-10',
|
||||
presetLabel: 'Forest · 50/10 · Forest Birds',
|
||||
presetLabel: 'Forest · Forest Birds',
|
||||
keepDoing,
|
||||
tryNext,
|
||||
ctaLabel: copy.stats.reviewCarryCta,
|
||||
|
||||
@@ -34,7 +34,7 @@ const REVIEW_ENTRY_PRESETS = {
|
||||
sceneId: DEFAULT_SCENE_ID,
|
||||
soundPresetId: DEFAULT_SOUND_ID,
|
||||
timerPresetId: DEFAULT_TIMER_ID,
|
||||
label: '숲 · 50/10 · Forest Birds',
|
||||
label: '숲 · Forest Birds',
|
||||
},
|
||||
} as const;
|
||||
const DEFAULT_ATMOSPHERE =
|
||||
@@ -45,30 +45,12 @@ const entryCopy = {
|
||||
goalPlaceholder: '예: 제안서 첫 문단만 다듬기',
|
||||
durationLabel: '예상 시간(분)',
|
||||
durationPlaceholder: '예: 70',
|
||||
durationHelper: '입력한 시간은 지금 가장 가까운 기본 리듬으로 먼저 맞춰서 들어가요.',
|
||||
durationHelper: '이 목표를 끝내는 데 걸릴 것 같은 시간을 적어요.',
|
||||
startNow: '이 분위기로 들어가기',
|
||||
startLoading: '입장 준비 중...',
|
||||
atmosphereTitle: '어떤 분위기에서 들어갈까요?',
|
||||
atmosphereBody:
|
||||
'배경과 사운드는 같이 움직여요. 오늘 goal에 맞는 atmosphere 하나만 고르면 바로 들어갈 수 있어요.',
|
||||
resumeEyebrow: 'Resume',
|
||||
resumeRunning: '진행 중인 세션이 있어요.',
|
||||
resumePaused: '잠시 멈춘 세션이 있어요.',
|
||||
resumeCta: '이어서 몰입하기',
|
||||
resumeRefocusCta: '한 조각 다시 잡기',
|
||||
resumeRouting: '진행 중인 세션으로 돌아가는 중이에요.',
|
||||
resumeMicroStepLabel: '마지막 한 조각',
|
||||
resumePausedHint: '같은 목표를 바로 이어가거나, 다시 시작할 한 조각만 먼저 정리할 수 있어요.',
|
||||
resumeNewGoalHint: '새 목표로 전환하려면 지금 멈춘 세션을 먼저 어떻게 정리할지 결정해야 해요.',
|
||||
resumeTakeoverCta: '새 목표로 전환',
|
||||
takeoverEyebrow: '새 목표로 전환',
|
||||
takeoverTitle: '현재 멈춘 세션을 어떻게 할까요?',
|
||||
takeoverBody: '지금 멈춘 흐름을 조용히 없애지 않고, 먼저 정리한 뒤 새 목표로 넘어가요.',
|
||||
takeoverKeepCta: '이어서 하기',
|
||||
takeoverConfirmCta: '이 세션은 여기서 정리하고 새로 시작',
|
||||
takeoverCancelCta: '취소',
|
||||
takeoverLoading: '세션을 정리하는 중...',
|
||||
takeoverFailed: '멈춘 세션을 정리하지 못했어요. 잠시 후 다시 시도해 주세요.',
|
||||
loadFailed: '세션 상태를 불러오지 못했어요. 새로 시작은 계속 할 수 있어요.',
|
||||
reviewEyebrow: 'Weekly Review',
|
||||
reviewTitle: '이번 주 review를 잠깐 보고 갈까요?',
|
||||
@@ -77,9 +59,6 @@ const entryCopy = {
|
||||
reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
|
||||
reviewCtaPro: '나에게 맞는 흐름 보기',
|
||||
reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
|
||||
resumeReviewEyebrow: 'Weekly Review',
|
||||
resumeReviewTitle: '잠깐 review를 보고 다시 들어갈 수 있어요.',
|
||||
resumeReviewHelper: '현재 세션은 그대로 두고, 이번 주 흐름만 짧게 확인합니다.',
|
||||
reviewReturnEyebrow: '방금 본 review 기준',
|
||||
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
||||
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
||||
@@ -89,25 +68,13 @@ const entryCopy = {
|
||||
reviewReturnBodySmaller: '길이를 늘리기보다, 더 작은 goal과 더 구체적인 첫 한 조각으로 시작하면 이어가기 쉬워져요.',
|
||||
reviewReturnBodyClosure: '큰 흐름보다 지금 블록을 어디서 마무리할지 먼저 떠올리면 끝까지 가져가기 쉬워져요.',
|
||||
reviewReturnBodyStart: '길이를 늘리기보다, 아주 작은 goal로 이번 주 첫 세션 하나를 더 여는 데 집중해 보세요.',
|
||||
reviewReturnRitualLabel: '추천 ritual · 숲 · 50/10 · Forest Birds',
|
||||
reviewReturnRitualLabel: '추천 atmosphere · 숲 · Forest Birds',
|
||||
paywallLead: 'Calm Session OS PRO',
|
||||
paywallBody: 'Pro는 더 빠른 ritual과 더 깊은 review로 시작과 복귀를 가볍게 만듭니다.',
|
||||
};
|
||||
|
||||
const goalCardClass =
|
||||
'w-full rounded-[2rem] border border-white/12 bg-[#0f1115]/26 px-6 py-6 shadow-[0_24px_60px_rgba(3,7,18,0.32)] backdrop-blur-xl md:px-8 md:py-8';
|
||||
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 resolveSoundLabel = (soundPresetId?: string | null) => {
|
||||
if (!soundPresetId) {
|
||||
return 'Silent';
|
||||
}
|
||||
|
||||
return SOUND_PRESETS.find((preset) => preset.id === soundPresetId)?.label ?? 'Silent';
|
||||
};
|
||||
|
||||
const reviewCarryCopyByHint: Record<
|
||||
ReviewCarryHint,
|
||||
@@ -171,10 +138,6 @@ export const FocusDashboardWidget = () => {
|
||||
const [currentSession, setCurrentSession] = useState<FocusSession | null>(null);
|
||||
const [isCheckingSession, setIsCheckingSession] = useState(true);
|
||||
const [sessionLookupError, setSessionLookupError] = useState<string | null>(null);
|
||||
const [isTakeoverSheetOpen, setIsTakeoverSheetOpen] = useState(false);
|
||||
const [isResolvingTakeover, setIsResolvingTakeover] = useState(false);
|
||||
const [takeoverError, setTakeoverError] = useState<string | null>(null);
|
||||
const [focusGoalAfterTakeover, setFocusGoalAfterTakeover] = useState(false);
|
||||
|
||||
const goalInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const selectedAtmosphere = useMemo(
|
||||
@@ -192,14 +155,6 @@ export const FocusDashboardWidget = () => {
|
||||
return getSceneById(currentSession?.sceneId ?? selectedAtmosphere.sceneId) ?? SCENE_THEMES[0];
|
||||
}, [currentSession?.sceneId, selectedAtmosphere.sceneId]);
|
||||
|
||||
const activeRitualMeta = useMemo(() => {
|
||||
const timerLabel =
|
||||
getTimerPresetMetaById(currentSession?.timerPresetId ?? DEFAULT_TIMER_ID).label;
|
||||
const soundLabel = resolveSoundLabel(currentSession?.soundPresetId ?? DEFAULT_SOUND_ID);
|
||||
|
||||
return `${activeScene.name} · ${timerLabel} · ${soundLabel}`;
|
||||
}, [activeScene.name, currentSession?.soundPresetId, currentSession?.timerPresetId]);
|
||||
|
||||
const trimmedGoal = goalDraft.trim();
|
||||
const canStart =
|
||||
trimmedGoal.length > 0 &&
|
||||
@@ -232,11 +187,8 @@ export const FocusDashboardWidget = () => {
|
||||
const durationHelper =
|
||||
parsedDurationMinutes === null
|
||||
? '이 목표를 끝내는 데 걸릴 것 같은 시간을 분 단위로 적어주세요.'
|
||||
: parsedDurationMinutes === resolvedTimerPreset.focusMinutes
|
||||
? `${entryCopy.durationHelper} 지금은 ${resolvedTimerPreset.label} 리듬으로 바로 들어가요.`
|
||||
: `${entryCopy.durationHelper} ${parsedDurationMinutes}분은 지금 ${resolvedTimerPreset.label} 리듬으로 먼저 들어가요.`;
|
||||
const isRunningSession = currentSession?.state === 'running';
|
||||
const isPausedSession = currentSession?.state === 'paused';
|
||||
: entryCopy.durationHelper;
|
||||
const hasCurrentSession = Boolean(currentSession);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -272,25 +224,10 @@ export const FocusDashboardWidget = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCheckingSession && isRunningSession) {
|
||||
if (!isCheckingSession && hasCurrentSession) {
|
||||
router.replace('/space');
|
||||
}
|
||||
}, [isCheckingSession, isRunningSession, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusGoalAfterTakeover || currentSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
goalInputRef.current?.focus();
|
||||
setFocusGoalAfterTakeover(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
};
|
||||
}, [currentSession, focusGoalAfterTakeover]);
|
||||
}, [hasCurrentSession, isCheckingSession, router]);
|
||||
|
||||
const openPaywall = () => {
|
||||
if (!isPro) {
|
||||
@@ -298,13 +235,6 @@ export const FocusDashboardWidget = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetEntryDrafts = () => {
|
||||
setGoalDraft('');
|
||||
setSelectedAtmosphereId(initialAtmosphere.id);
|
||||
setDurationDraft(String(initialDurationMinutes));
|
||||
setHasEditedDuration(false);
|
||||
};
|
||||
|
||||
const handleDurationChange = (value: string) => {
|
||||
setDurationDraft(sanitizeDurationDraft(value));
|
||||
setHasEditedDuration(true);
|
||||
@@ -369,56 +299,8 @@ export const FocusDashboardWidget = () => {
|
||||
setIsStartingSession(false);
|
||||
};
|
||||
|
||||
const handleResumeSession = () => {
|
||||
router.push('/space?resume=continue');
|
||||
};
|
||||
|
||||
const handleResumeRefocus = () => {
|
||||
router.push('/space?resume=refocus');
|
||||
};
|
||||
|
||||
const handleOpenTakeoverSheet = () => {
|
||||
setTakeoverError(null);
|
||||
setIsTakeoverSheetOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseTakeoverSheet = () => {
|
||||
if (isResolvingTakeover) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTakeoverSheetOpen(false);
|
||||
setTakeoverError(null);
|
||||
};
|
||||
|
||||
const handleConfirmTakeover = async () => {
|
||||
if (!currentSession || isResolvingTakeover) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsResolvingTakeover(true);
|
||||
setTakeoverError(null);
|
||||
|
||||
try {
|
||||
await focusSessionApi.abandonSession();
|
||||
setCurrentSession(null);
|
||||
setIsTakeoverSheetOpen(false);
|
||||
setSessionLookupError(null);
|
||||
resetEntryDrafts();
|
||||
setFocusGoalAfterTakeover(true);
|
||||
} catch (error) {
|
||||
setTakeoverError(
|
||||
error instanceof Error ? error.message : entryCopy.takeoverFailed,
|
||||
);
|
||||
} finally {
|
||||
setIsResolvingTakeover(false);
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowWeeklyReviewTeaser =
|
||||
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
|
||||
const shouldShowResumeReviewEntry =
|
||||
!isCheckingSession && isPausedSession && hasEnoughWeeklyData;
|
||||
|
||||
return (
|
||||
<div className="relative min-h-dvh overflow-hidden bg-slate-950 text-white selection:bg-white/20">
|
||||
@@ -443,9 +325,6 @@ export const FocusDashboardWidget = () => {
|
||||
<div className="w-full max-w-[42rem]">
|
||||
{isCheckingSession ? (
|
||||
<div className={cn(goalCardClass, 'space-y-4 text-center')}>
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/46">
|
||||
{entryCopy.resumeEyebrow}
|
||||
</p>
|
||||
<p className="text-[15px] text-white/72">세션 상태를 불러오는 중이에요.</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -467,135 +346,58 @@ export const FocusDashboardWidget = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isRunningSession ? (
|
||||
<div className={cn(goalCardClass, 'space-y-4 text-center')}>
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/46">
|
||||
{entryCopy.resumeEyebrow}
|
||||
</p>
|
||||
<p className="text-[15px] text-white/72">{entryCopy.resumeRouting}</p>
|
||||
</div>
|
||||
) : currentSession ? (
|
||||
<div className={cn(goalCardClass, 'space-y-5')}>
|
||||
<div className="space-y-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/46">
|
||||
{entryCopy.resumeEyebrow}
|
||||
</p>
|
||||
<h1 className="text-[1.8rem] font-light leading-[1.14] tracking-[-0.03em] text-white md:text-[2.2rem]">
|
||||
{currentSession.goal}
|
||||
</h1>
|
||||
<p className="text-sm text-white/68">
|
||||
{entryCopy.resumePaused}
|
||||
</p>
|
||||
{currentSession.microStep ? (
|
||||
<div className="rounded-[1.1rem] border border-white/10 bg-white/[0.04] px-4 py-3">
|
||||
<p className="mb-1 text-[11px] font-medium uppercase tracking-[0.16em] text-white/44">
|
||||
{entryCopy.resumeMicroStepLabel}
|
||||
</p>
|
||||
<p className="text-[15px] text-white/82">{currentSession.microStep}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<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.resumePausedHint}</p>
|
||||
<p className="text-sm text-white/46">{entryCopy.resumeNewGoalHint}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenTakeoverSheet}
|
||||
className="inline-flex w-fit items-center text-sm font-medium text-white/62 transition hover:text-white/84"
|
||||
>
|
||||
{entryCopy.resumeTakeoverCta}
|
||||
</button>
|
||||
|
||||
{shouldShowResumeReviewEntry ? (
|
||||
<AppAtmosphereEntryShell
|
||||
canStart={canStart}
|
||||
durationDraft={durationDraft}
|
||||
durationHelper={durationHelper}
|
||||
durationInputLabel={entryCopy.durationLabel}
|
||||
durationPlaceholder={entryCopy.durationPlaceholder}
|
||||
durationSuggestions={ENTRY_DURATION_SUGGESTIONS}
|
||||
goalDraft={goalDraft}
|
||||
goalInputRef={goalInputRef}
|
||||
goalPlaceholder={entryCopy.goalPlaceholder}
|
||||
isStartingSession={isStartingSession}
|
||||
reviewEntry={
|
||||
shouldShowWeeklyReviewTeaser ? (
|
||||
<Link
|
||||
href="/stats"
|
||||
className="block rounded-[1.35rem] border border-white/10 bg-[#0f1115]/12 px-4 py-3 backdrop-blur-lg transition hover:bg-[#0f1115]/18"
|
||||
className="block rounded-[1.6rem] border border-white/10 bg-[#0f1115]/18 px-5 py-4 backdrop-blur-lg transition hover:bg-[#0f1115]/24"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||
{entryCopy.resumeReviewEyebrow}
|
||||
{entryCopy.reviewEyebrow}
|
||||
</p>
|
||||
<p className="mt-2 text-[0.96rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{entryCopy.resumeReviewTitle}
|
||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{reviewTeaserTitle}
|
||||
</p>
|
||||
<p className="mt-2 max-w-[30rem] text-[12px] leading-[1.6] text-white/60">
|
||||
{isPro ? review.carryForward.keepDoing : entryCopy.resumeReviewHelper}
|
||||
<p className="mt-2 text-[13px] leading-[1.6] text-white/62">
|
||||
{reviewTeaserSummary}
|
||||
</p>
|
||||
<p className="mt-2 text-[12px] text-white/44">{reviewTeaserHelper}</p>
|
||||
</div>
|
||||
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/72">
|
||||
<span className="inline-flex items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
||||
{reviewTeaserCta}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<AppAtmosphereEntryShell
|
||||
canStart={canStart}
|
||||
durationDraft={durationDraft}
|
||||
durationHelper={durationHelper}
|
||||
durationInputLabel={entryCopy.durationLabel}
|
||||
durationPlaceholder={entryCopy.durationPlaceholder}
|
||||
durationSuggestions={ENTRY_DURATION_SUGGESTIONS}
|
||||
goalDraft={goalDraft}
|
||||
goalInputRef={goalInputRef}
|
||||
goalPlaceholder={entryCopy.goalPlaceholder}
|
||||
isStartingSession={isStartingSession}
|
||||
reviewEntry={
|
||||
shouldShowWeeklyReviewTeaser ? (
|
||||
<Link
|
||||
href="/stats"
|
||||
className="block rounded-[1.6rem] border border-white/10 bg-[#0f1115]/18 px-5 py-4 backdrop-blur-lg transition hover:bg-[#0f1115]/24"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||
{entryCopy.reviewEyebrow}
|
||||
</p>
|
||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{reviewTeaserTitle}
|
||||
</p>
|
||||
<p className="mt-2 text-[13px] leading-[1.6] text-white/62">
|
||||
{reviewTeaserSummary}
|
||||
</p>
|
||||
<p className="mt-2 text-[12px] text-white/44">{reviewTeaserHelper}</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center text-[12px] font-medium tracking-[0.04em] text-white/74">
|
||||
{reviewTeaserCta}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : undefined
|
||||
}
|
||||
selectedAtmosphere={selectedAtmosphere}
|
||||
sessionLookupError={sessionLookupError}
|
||||
startButtonLabel={entryCopy.startNow}
|
||||
startButtonLoadingLabel={entryCopy.startLoading}
|
||||
atmosphereOptions={ATMOSPHERE_OPTIONS}
|
||||
atmosphereTitle={entryCopy.atmosphereTitle}
|
||||
atmosphereBody={entryCopy.atmosphereBody}
|
||||
onDurationChange={handleDurationChange}
|
||||
onGoalChange={setGoalDraft}
|
||||
onSelectAtmosphere={handleSelectAtmosphere}
|
||||
onSelectDuration={handleSelectDuration}
|
||||
onStartSession={() => {
|
||||
void handleStartSession();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : undefined
|
||||
}
|
||||
selectedAtmosphere={selectedAtmosphere}
|
||||
sessionLookupError={sessionLookupError}
|
||||
startButtonLabel={entryCopy.startNow}
|
||||
startButtonLoadingLabel={entryCopy.startLoading}
|
||||
atmosphereOptions={ATMOSPHERE_OPTIONS}
|
||||
atmosphereTitle={entryCopy.atmosphereTitle}
|
||||
atmosphereBody={entryCopy.atmosphereBody}
|
||||
onDurationChange={handleDurationChange}
|
||||
onGoalChange={setGoalDraft}
|
||||
onSelectAtmosphere={handleSelectAtmosphere}
|
||||
onSelectDuration={handleSelectDuration}
|
||||
onStartSession={() => {
|
||||
void handleStartSession();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -624,75 +426,6 @@ export const FocusDashboardWidget = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isTakeoverSheetOpen ? (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center p-4 sm:items-center">
|
||||
<div className="absolute inset-0 bg-slate-950/52 backdrop-blur-[3px]" />
|
||||
<div className="relative z-10 w-full max-w-[30rem] rounded-[2rem] border border-white/12 bg-[linear-gradient(165deg,rgba(15,17,21,0.92)_0%,rgba(7,10,14,0.98)_100%)] p-6 shadow-[0_24px_70px_rgba(2,6,23,0.38)] backdrop-blur-2xl">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||
{entryCopy.takeoverEyebrow}
|
||||
</p>
|
||||
<h2 className="mt-3 text-[1.45rem] font-light leading-[1.18] tracking-[-0.03em] text-white">
|
||||
{entryCopy.takeoverTitle}
|
||||
</h2>
|
||||
<p className="mt-3 text-[14px] leading-[1.7] text-white/62">
|
||||
{entryCopy.takeoverBody}
|
||||
</p>
|
||||
|
||||
{currentSession ? (
|
||||
<div className="mt-5 rounded-[1.35rem] border border-white/10 bg-white/[0.05] px-4 py-4">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/40">
|
||||
{entryCopy.resumeEyebrow}
|
||||
</p>
|
||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{currentSession.goal}
|
||||
</p>
|
||||
{currentSession.microStep ? (
|
||||
<p className="mt-2 text-[13px] leading-[1.6] text-white/58">
|
||||
{entryCopy.resumeMicroStepLabel} · {currentSession.microStep}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{takeoverError ? (
|
||||
<p className="mt-4 text-sm text-amber-100/82">{takeoverError}</p>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 flex flex-col gap-2.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResumeSession}
|
||||
disabled={isResolvingTakeover}
|
||||
className={primaryButtonClass}
|
||||
>
|
||||
{entryCopy.takeoverKeepCta}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleConfirmTakeover();
|
||||
}}
|
||||
disabled={isResolvingTakeover}
|
||||
className={cn(
|
||||
secondaryButtonClass,
|
||||
'border-white/14 bg-white/[0.05] text-white/86 hover:bg-white/[0.1]',
|
||||
)}
|
||||
>
|
||||
{isResolvingTakeover ? entryCopy.takeoverLoading : entryCopy.takeoverConfirmCta}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCloseTakeoverSheet}
|
||||
disabled={isResolvingTakeover}
|
||||
className="inline-flex items-center justify-center rounded-full px-4 py-2.5 text-sm font-medium text-white/54 transition hover:text-white/78 disabled:opacity-45"
|
||||
>
|
||||
{entryCopy.takeoverCancelCta}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,6 @@ import { FocusTopToast } from "./FocusTopToast";
|
||||
export const SpaceWorkspaceWidget = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const resumeIntent = searchParams.get("resume");
|
||||
const sceneQuery = searchParams.get("scene") ?? searchParams.get("room");
|
||||
const goalQuery = searchParams.get("goal")?.trim() ?? "";
|
||||
const focusPlanItemIdQuery = searchParams.get("planItemId");
|
||||
@@ -104,7 +103,6 @@ export const SpaceWorkspaceWidget = () => {
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||
useState<SessionEntryPoint>("space-setup");
|
||||
const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false);
|
||||
const [hasConsumedEntryOverlayIntent, setHasConsumedEntryOverlayIntent] = useState(false);
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -225,16 +223,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
workspaceMode === "setup" &&
|
||||
showReviewTeaserAfterComplete &&
|
||||
hasEnoughWeeklyData;
|
||||
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
|
||||
@@ -265,33 +254,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
if (!currentSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSession.state === "paused" && !allowsPausedReentry) {
|
||||
router.replace("/app");
|
||||
}
|
||||
}, [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]);
|
||||
}, [currentSession, isBootstrapping, router]);
|
||||
|
||||
useEffect(() => {
|
||||
const preferMobile =
|
||||
@@ -390,12 +353,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
canStartSession={controls.canStartSession}
|
||||
canPauseSession={controls.canPauseSession}
|
||||
canRestartSession={controls.canRestartSession}
|
||||
entryOverlayIntent={entryOverlayIntent}
|
||||
returnPromptMode={awayReturnRecovery.returnPromptMode}
|
||||
onEntryOverlayIntentHandled={() => {
|
||||
setHasConsumedEntryOverlayIntent(true);
|
||||
router.replace("/space");
|
||||
}}
|
||||
onStartRequested={() => {
|
||||
void handleStartRequested();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user