fix(flow): 기획-구현 불일치 정렬
This commit is contained in:
@@ -73,7 +73,7 @@ export const app = {
|
||||
reviewCarryTryClosure: '시작은 있었지만 마무리가 약했어요. 다음 주에는 완료 직전에 다른 블록으로 넘어가지 않는 흐름을 한 번 만들어 보세요.',
|
||||
reviewCarryTryStart: '시작 횟수가 적었어요. 다음 주에는 길이를 늘리기보다 첫 세션을 한 번 더 여는 것에 집중해 보세요.',
|
||||
reviewCarryCta: '이 흐름으로 다음 세션 시작',
|
||||
reviewCarryCtaPro: '가장 잘 맞은 ritual로 /app 돌아가기',
|
||||
reviewCarryCtaPro: '추천 ritual과 함께 /app 돌아가기',
|
||||
reviewCarryKeepTitle: '다음 주에 유지할 것',
|
||||
reviewCarryTryTitle: '다음 주에 바꿔볼 것',
|
||||
reviewCarryPresetLabel: '추천 ritual',
|
||||
|
||||
@@ -17,10 +17,10 @@ export const space = {
|
||||
timerLabel: '타이머',
|
||||
soundLabel: '사운드',
|
||||
reviewTeaserEyebrow: 'Weekly Review',
|
||||
reviewTeaserTitle: '방금 끝낸 흐름까지 review에 담아둘까요?',
|
||||
reviewTeaserTitlePro: '방금 끝낸 흐름까지 포함해 이번 주 리듬을 다시 볼까요?',
|
||||
reviewTeaserTitle: '이번 주 review를 다시 볼까요?',
|
||||
reviewTeaserTitlePro: '이번 주 흐름과 잘 맞았던 ritual을 다시 볼까요?',
|
||||
reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.',
|
||||
reviewTeaserHelperPro: '방금 마친 흐름과 가장 잘 맞는 ritual을 같이 보고 다음 세션으로 이어갈 수 있어요.',
|
||||
reviewTeaserHelperPro: '원하면 이번 주 흐름과 추천 ritual을 다시 보고 다음 세션으로 이어갈 수 있어요.',
|
||||
reviewTeaserCta: '주간 review 보기',
|
||||
reviewTeaserDismiss: '나중에',
|
||||
readyHint: '목표를 적으면 시작할 수 있어요.',
|
||||
@@ -65,12 +65,12 @@ export const space = {
|
||||
returnPromptEyebrow: '다시 돌아왔어요',
|
||||
returnPromptFocusTitle: '흐름은 아직 남아 있어요.',
|
||||
returnPromptFocusDescription: '멈춘 자리에서 바로 이어가거나, 다시 시작할 한 조각만 조용히 다듬을 수 있어요.',
|
||||
returnPromptBreakTitle: '자리를 비운 사이 이 블록이 끝났어요.',
|
||||
returnPromptBreakDescription: '지금부터 쉬거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.',
|
||||
returnPromptBreakTitle: '자리를 비운 사이 쉬는 시간이 시작됐어요.',
|
||||
returnPromptBreakDescription: 'break를 그대로 이어가거나, 다음 블록으로 부드럽게 넘어갈 수 있어요.',
|
||||
returnPromptContinue: '멈춘 자리에서 이어가기',
|
||||
returnPromptContinueHint: '타이머와 현재 흐름을 그대로 두고 다시 집중으로 복귀합니다.',
|
||||
returnPromptRest: '지금부터 쉬기',
|
||||
returnPromptRestHint: '지금부터 break를 시작한 것처럼 천천히 숨을 고릅니다.',
|
||||
returnPromptRest: '쉬기 이어가기',
|
||||
returnPromptRestHint: '이미 시작된 break를 그대로 두고, 조금 더 천천히 숨을 고릅니다.',
|
||||
returnPromptNext: '다음 블록 이어가기',
|
||||
returnPromptNextHint: '다음 한 조각을 정하고, 같은 흐름 안에서 부드럽게 이어갑니다.',
|
||||
returnPromptRefocus: '한 조각 다시 잡기',
|
||||
@@ -90,8 +90,8 @@ export const space = {
|
||||
suggestions: ['리뷰 코멘트 2개 처리', '문서 1문단 다듬기', '이슈 1개 정리', '메일 2개 회신'],
|
||||
placeholderFallback: '다음 한 조각을 적어보세요',
|
||||
placeholderExample: (goal: string) => `예: ${goal}`,
|
||||
title: '이 블록을 어떻게 닫을까요?',
|
||||
description: '지금은 끝내기, 쉬기, 이어가기 중 하나만 고르면 돼요.',
|
||||
title: '이 블록을 어떻게 이어갈까요?',
|
||||
description: '다음으로 이어가기, 잠시 비우기, 여기서 마무리하기 중 하나만 고르면 돼요.',
|
||||
nextTitle: '좋아요. 다음 한 조각만 정해요.',
|
||||
nextDescription: '너무 크게 잡지 말고, 바로 손을 올릴 한 줄만 남겨요.',
|
||||
currentGoalLabel: '방금 끝낸 블록',
|
||||
@@ -100,10 +100,10 @@ export const space = {
|
||||
chooseNextDescription: '다음 한 조각을 정하고 같은 흐름 안에서 계속 갑니다.',
|
||||
backButton: '돌아가기',
|
||||
closeAriaLabel: '닫기',
|
||||
finishButton: '여기까지 끝내기',
|
||||
finishButton: '여기서 마무리하기',
|
||||
finishDescription: '이 블록은 여기서 닫고, 다음 진입은 가볍게 남겨둡니다.',
|
||||
restButton: '잠깐 쉬기',
|
||||
restDescription: '이 블록은 닫고, 지금부터는 잠깐 쉬는 리듬으로 넘어갑니다.',
|
||||
restButton: '잠시 비우기',
|
||||
restDescription: '이 블록은 아직 닫지 않고, 잠깐 멈춘 뒤 돌아오라고 알려드려요.',
|
||||
confirmButton: '다음 목표로 바로 시작',
|
||||
confirmPending: '시작 중…',
|
||||
finishPending: '마무리 중…',
|
||||
|
||||
@@ -17,6 +17,14 @@ import { cn } from '@/shared/lib/cn';
|
||||
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: '숲 · 50/10 · Forest Birds',
|
||||
},
|
||||
} as const;
|
||||
const GOAL_SUGGESTIONS = copy.session.goalChips.slice(0, 4);
|
||||
|
||||
const entryCopy = {
|
||||
@@ -45,6 +53,9 @@ const entryCopy = {
|
||||
reviewTitlePro: '나에게 잘 맞았던 흐름을 다시 보고 갈까요?',
|
||||
reviewCtaPro: '나에게 맞는 흐름 보기',
|
||||
reviewHelperPro: '가장 잘 맞았던 ritual과 carry-forward를 보고 돌아올 수 있어요.',
|
||||
resumeReviewEyebrow: 'Weekly Review',
|
||||
resumeReviewTitle: '잠깐 review를 보고 다시 들어갈 수 있어요.',
|
||||
resumeReviewHelper: '현재 세션은 그대로 두고, 이번 주 흐름만 짧게 확인합니다.',
|
||||
reviewReturnEyebrow: '방금 본 review 기준',
|
||||
reviewReturnTitleSteady: '이번 주에 잘 맞았던 흐름을 그대로 가져가 보세요.',
|
||||
reviewReturnTitleSmaller: '이번엔 목표를 더 작게 잡아보세요.',
|
||||
@@ -109,6 +120,15 @@ export const FocusDashboardWidget = () => {
|
||||
const { sceneAssetMap } = useMediaCatalog();
|
||||
const { review, 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 [goalDraft, setGoalDraft] = useState('');
|
||||
const [microStepDraft, setMicroStepDraft] = useState('');
|
||||
const [isStartingSession, setIsStartingSession] = useState(false);
|
||||
@@ -120,8 +140,8 @@ export const FocusDashboardWidget = () => {
|
||||
const goalInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const activeScene = useMemo(() => {
|
||||
return getSceneById(currentSession?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0];
|
||||
}, [currentSession?.sceneId]);
|
||||
return getSceneById(currentSession?.sceneId ?? reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID) ?? SCENE_THEMES[0];
|
||||
}, [currentSession?.sceneId, reviewEntryPresetConfig?.sceneId]);
|
||||
|
||||
const activeRitualMeta = useMemo(() => {
|
||||
const timerLabel = timerLabelById[currentSession?.timerPresetId ?? DEFAULT_TIMER_ID] ?? '50/10';
|
||||
@@ -138,7 +158,6 @@ export const FocusDashboardWidget = () => {
|
||||
review.recoveryQuality.availability === 'ready');
|
||||
const reviewSource = searchParams.get('review');
|
||||
const reviewCarryHint = searchParams.get('carryHint');
|
||||
const reviewEntryPreset = searchParams.get('entryPreset');
|
||||
const normalizedReviewCarryHint: ReviewCarryHint | null =
|
||||
reviewCarryHint === 'steady' ||
|
||||
reviewCarryHint === 'smaller' ||
|
||||
@@ -151,11 +170,12 @@ export const FocusDashboardWidget = () => {
|
||||
const reviewReturnCopy =
|
||||
normalizedReviewCarryHint !== null ? reviewCarryCopyByHint[normalizedReviewCarryHint] : null;
|
||||
const reviewReturnRitualLabel =
|
||||
isPro && reviewEntryPreset === 'forest-50-10' ? entryCopy.reviewReturnRitualLabel : null;
|
||||
isPro && reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : null;
|
||||
const reviewTeaserTitle = isPro ? entryCopy.reviewTitlePro : entryCopy.reviewTitle;
|
||||
const reviewTeaserSummary = isPro ? review.carryForward.keepDoing : review.snapshotSummary;
|
||||
const reviewTeaserHelper = isPro ? entryCopy.reviewHelperPro : entryCopy.reviewHelper;
|
||||
const reviewTeaserCta = isPro ? entryCopy.reviewCtaPro : entryCopy.reviewCta;
|
||||
const entryRitualHint = reviewEntryPresetConfig ? `추천 ritual · ${reviewEntryPresetConfig.label}` : entryCopy.ritualHint;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -215,9 +235,9 @@ export const FocusDashboardWidget = () => {
|
||||
await focusSessionApi.startSession({
|
||||
goal: trimmedGoal,
|
||||
microStep: microStepDraft.trim() || null,
|
||||
sceneId: DEFAULT_SCENE_ID,
|
||||
soundPresetId: DEFAULT_SOUND_ID,
|
||||
timerPresetId: DEFAULT_TIMER_ID,
|
||||
sceneId: reviewEntryPresetConfig?.sceneId ?? DEFAULT_SCENE_ID,
|
||||
soundPresetId: reviewEntryPresetConfig?.soundPresetId ?? DEFAULT_SOUND_ID,
|
||||
timerPresetId: reviewEntryPresetConfig?.timerPresetId ?? DEFAULT_TIMER_ID,
|
||||
entryPoint: 'space-setup',
|
||||
});
|
||||
router.push('/space');
|
||||
@@ -235,6 +255,8 @@ export const FocusDashboardWidget = () => {
|
||||
|
||||
const shouldShowWeeklyReviewTeaser =
|
||||
!isCheckingSession && !currentSession && hasEnoughWeeklyData && !isReviewReturn;
|
||||
const shouldShowResumeReviewEntry =
|
||||
!isCheckingSession && Boolean(currentSession) && hasEnoughWeeklyData;
|
||||
|
||||
return (
|
||||
<div className="relative min-h-dvh overflow-hidden bg-slate-950 text-white selection:bg-white/20">
|
||||
@@ -264,37 +286,6 @@ export const FocusDashboardWidget = () => {
|
||||
</p>
|
||||
<p className="text-[15px] text-white/72">세션 상태를 불러오는 중이에요.</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">
|
||||
{currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning}
|
||||
</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">
|
||||
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
||||
{entryCopy.resumeCta}
|
||||
</button>
|
||||
<p className="text-xs text-white/48 sm:text-right">{activeRitualMeta}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/56">{entryCopy.resumeNewGoalHint}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{reviewReturnCopy ? (
|
||||
@@ -314,129 +305,188 @@ export const FocusDashboardWidget = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={goalCardClass}>
|
||||
<div className="space-y-3 text-center">
|
||||
<h1 className="text-[2rem] font-light leading-[1.08] tracking-[-0.04em] text-white md:text-[2.9rem]">
|
||||
{entryCopy.title}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[32rem] text-[15px] leading-6 text-white/70 md:text-base">
|
||||
{entryCopy.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
<label className="block">
|
||||
<span className="sr-only">Goal</span>
|
||||
<input
|
||||
ref={goalInputRef}
|
||||
value={goalDraft}
|
||||
onChange={(event) => setGoalDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void handleStartSession();
|
||||
}
|
||||
}}
|
||||
placeholder={entryCopy.goalPlaceholder}
|
||||
className={cn(
|
||||
inputShellClass,
|
||||
'text-[1.15rem] font-light tracking-[-0.02em] placeholder:text-white/34 md:text-[1.4rem]',
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-[12px] font-medium uppercase tracking-[0.16em] text-white/46">
|
||||
{entryCopy.microStepLabel}
|
||||
</span>
|
||||
<input
|
||||
value={microStepDraft}
|
||||
onChange={(event) => setMicroStepDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void handleStartSession();
|
||||
}
|
||||
}}
|
||||
placeholder={entryCopy.microStepPlaceholder}
|
||||
className={cn(inputShellClass, 'text-[0.98rem] placeholder:text-white/30')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<p className="text-sm text-white/48">{entryCopy.microStepHelper}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-2.5">
|
||||
{GOAL_SUGGESTIONS.map((suggestion) => {
|
||||
const isActive = trimmedGoal === suggestion.label;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectSuggestion(suggestion.label)}
|
||||
className={cn(
|
||||
'rounded-full border px-3.5 py-1.5 text-sm transition',
|
||||
isActive
|
||||
? 'border-white/32 bg-white/14 text-white'
|
||||
: 'border-white/14 bg-white/[0.04] text-white/72 hover:border-white/22 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{suggestion.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleStartSession();
|
||||
}}
|
||||
disabled={!canStart}
|
||||
className={primaryButtonClass}
|
||||
>
|
||||
{isStartingSession ? entryCopy.startLoading : entryCopy.startNow}
|
||||
</button>
|
||||
<div className="space-y-1 text-left sm:text-right">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-white/44">
|
||||
{entryCopy.ritualHint}
|
||||
{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>
|
||||
<p className="text-xs text-white/52">{entryCopy.ritualHelper}</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">
|
||||
{currentSession.state === 'paused' ? entryCopy.resumePaused : entryCopy.resumeRunning}
|
||||
</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">
|
||||
<button type="button" onClick={handleResumeSession} className={primaryButtonClass}>
|
||||
{entryCopy.resumeCta}
|
||||
</button>
|
||||
<p className="text-xs text-white/48 sm:text-right">{activeRitualMeta}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-white/56">{entryCopy.resumeNewGoalHint}</p>
|
||||
|
||||
{shouldShowResumeReviewEntry ? (
|
||||
<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"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||
{entryCopy.resumeReviewEyebrow}
|
||||
</p>
|
||||
<p className="mt-2 text-[0.96rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{entryCopy.resumeReviewTitle}
|
||||
</p>
|
||||
<p className="mt-2 max-w-[30rem] text-[12px] leading-[1.6] text-white/60">
|
||||
{isPro ? review.carryForward.keepDoing : entryCopy.resumeReviewHelper}
|
||||
</p>
|
||||
</div>
|
||||
<span className="inline-flex shrink-0 items-center text-[12px] font-medium tracking-[0.04em] text-white/72">
|
||||
{reviewTeaserCta}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{sessionLookupError ? (
|
||||
<p className="mt-5 text-sm text-amber-100/80">{entryCopy.loadFailed}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{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 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.16em] text-white/42">
|
||||
{entryCopy.reviewEyebrow}
|
||||
) : (
|
||||
<>
|
||||
<div className={goalCardClass}>
|
||||
<div className="space-y-3 text-center">
|
||||
<h1 className="text-[2rem] font-light leading-[1.08] tracking-[-0.04em] text-white md:text-[2.9rem]">
|
||||
{entryCopy.title}
|
||||
</h1>
|
||||
<p className="mx-auto max-w-[32rem] text-[15px] leading-6 text-white/70 md:text-base">
|
||||
{entryCopy.description}
|
||||
</p>
|
||||
<p className="mt-2 text-[1rem] font-medium tracking-[-0.02em] text-white/88">
|
||||
{reviewTeaserTitle}
|
||||
</p>
|
||||
<p className="mt-2 max-w-[34rem] 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/74">
|
||||
{reviewTeaserCta}
|
||||
</span>
|
||||
|
||||
<div className="mt-8 space-y-4">
|
||||
<label className="block">
|
||||
<span className="sr-only">Goal</span>
|
||||
<input
|
||||
ref={goalInputRef}
|
||||
value={goalDraft}
|
||||
onChange={(event) => setGoalDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void handleStartSession();
|
||||
}
|
||||
}}
|
||||
placeholder={entryCopy.goalPlaceholder}
|
||||
className={cn(
|
||||
inputShellClass,
|
||||
'text-[1.15rem] font-light tracking-[-0.02em] placeholder:text-white/34 md:text-[1.4rem]',
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-[12px] font-medium uppercase tracking-[0.16em] text-white/46">
|
||||
{entryCopy.microStepLabel}
|
||||
</span>
|
||||
<input
|
||||
value={microStepDraft}
|
||||
onChange={(event) => setMicroStepDraft(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void handleStartSession();
|
||||
}
|
||||
}}
|
||||
placeholder={entryCopy.microStepPlaceholder}
|
||||
className={cn(inputShellClass, 'text-[0.98rem] placeholder:text-white/30')}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<p className="text-sm text-white/48">{entryCopy.microStepHelper}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-wrap gap-2.5">
|
||||
{GOAL_SUGGESTIONS.map((suggestion) => {
|
||||
const isActive = trimmedGoal === suggestion.label;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
type="button"
|
||||
onClick={() => handleSelectSuggestion(suggestion.label)}
|
||||
className={cn(
|
||||
'rounded-full border px-3.5 py-1.5 text-sm transition',
|
||||
isActive
|
||||
? 'border-white/32 bg-white/14 text-white'
|
||||
: 'border-white/14 bg-white/[0.04] text-white/72 hover:border-white/22 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{suggestion.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void handleStartSession();
|
||||
}}
|
||||
disabled={!canStart}
|
||||
className={primaryButtonClass}
|
||||
>
|
||||
{isStartingSession ? entryCopy.startLoading : entryCopy.startNow}
|
||||
</button>
|
||||
<div className="space-y-1 text-left sm:text-right">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-white/44">
|
||||
{entryRitualHint}
|
||||
</p>
|
||||
<p className="text-xs text-white/52">{entryCopy.ritualHelper}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{sessionLookupError ? (
|
||||
<p className="mt-5 text-sm text-amber-100/80">{entryCopy.loadFailed}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
{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 sm:flex-row sm:items-end sm:justify-between">
|
||||
<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 max-w-[34rem] 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/74">
|
||||
{reviewTeaserCta}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,7 @@ export const SpaceFocusHudWidget = ({
|
||||
const visibleRef = useRef(false);
|
||||
const resumePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
const pausePlaybackStateRef = useRef<'running' | 'paused'>(playbackState);
|
||||
const suppressNextPausePromptRef = useRef(false);
|
||||
const restReminderTimerRef = useRef<number | null>(null);
|
||||
const normalizedGoal = goal.trim().length > 0 ? goal.trim() : copy.space.focusHud.goalFallback;
|
||||
const normalizedMicroStep = microStep?.trim() ? microStep.trim() : null;
|
||||
@@ -147,6 +148,12 @@ export const SpaceFocusHudWidget = ({
|
||||
hasActiveSession &&
|
||||
overlay === 'none'
|
||||
) {
|
||||
if (suppressNextPausePromptRef.current) {
|
||||
suppressNextPausePromptRef.current = false;
|
||||
pausePlaybackStateRef.current = playbackState;
|
||||
return;
|
||||
}
|
||||
|
||||
setIntentError(null);
|
||||
setOverlay('paused');
|
||||
onStatusMessage({
|
||||
@@ -292,7 +299,6 @@ export const SpaceFocusHudWidget = ({
|
||||
}}
|
||||
onRest={() => {
|
||||
handleDismissReturnPrompt();
|
||||
onStatusMessage({ message: copy.space.focusHud.restReminder });
|
||||
}}
|
||||
onNextGoal={() => {
|
||||
handleDismissReturnPrompt();
|
||||
@@ -352,6 +358,8 @@ export const SpaceFocusHudWidget = ({
|
||||
onFinish={() => Promise.resolve(onGoalFinish())}
|
||||
onRest={() => {
|
||||
setOverlay('none');
|
||||
suppressNextPausePromptRef.current = true;
|
||||
onPauseRequested?.();
|
||||
|
||||
if (restReminderTimerRef.current) {
|
||||
window.clearTimeout(restReminderTimerRef.current);
|
||||
|
||||
@@ -230,7 +230,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
summary: isPro
|
||||
? review.carryForward.keepDoing
|
||||
: copy.space.setup.reviewTeaserHelper,
|
||||
ctaHref: "/stats?review=weekly&origin=space-complete",
|
||||
ctaHref: "/stats",
|
||||
ctaLabel: copy.space.setup.reviewTeaserCta,
|
||||
onDismiss: () => setShowReviewTeaserAfterComplete(false),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user