feat(space): secondary weekly review teaser 추가
This commit is contained in:
@@ -16,6 +16,13 @@ export const space = {
|
||||
sceneLabel: '배경',
|
||||
timerLabel: '타이머',
|
||||
soundLabel: '사운드',
|
||||
reviewTeaserEyebrow: 'Weekly Review',
|
||||
reviewTeaserTitle: '방금 끝낸 흐름까지 review에 담아둘까요?',
|
||||
reviewTeaserTitlePro: '방금 끝낸 흐름까지 포함해 이번 주 리듬을 다시 볼까요?',
|
||||
reviewTeaserHelper: '지금은 바로 다시 시작해도 괜찮고, 원하면 주간 review를 잠깐 보고 갈 수 있어요.',
|
||||
reviewTeaserHelperPro: '방금 마친 흐름과 가장 잘 맞는 ritual을 같이 보고 다음 세션으로 이어갈 수 있어요.',
|
||||
reviewTeaserCta: '주간 review 보기',
|
||||
reviewTeaserDismiss: '나중에',
|
||||
readyHint: '목표를 적으면 시작할 수 있어요.',
|
||||
openFocusScreen: '실행 화면 열기',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'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';
|
||||
@@ -40,6 +41,13 @@ interface SpaceSetupDrawerWidgetProps {
|
||||
onResume: () => void;
|
||||
onStartFresh: () => void;
|
||||
};
|
||||
reviewTeaser?: {
|
||||
title: string;
|
||||
summary: string;
|
||||
ctaHref: string;
|
||||
ctaLabel: string;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface SummaryChipProps {
|
||||
@@ -88,6 +96,7 @@ export const SpaceSetupDrawerWidget = ({
|
||||
onGoalChipSelect,
|
||||
onStart,
|
||||
resumeHint,
|
||||
reviewTeaser,
|
||||
}: SpaceSetupDrawerWidgetProps) => {
|
||||
const { setup } = copy.space;
|
||||
const [openPopover, setOpenPopover] = useState<SelectionPopover | null>(null);
|
||||
@@ -309,6 +318,38 @@ export const SpaceSetupDrawerWidget = ({
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -6,14 +6,17 @@ 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 { 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 { SpaceToolsDockWidget } from "@/widgets/space-tools-dock";
|
||||
@@ -63,6 +66,8 @@ export const SpaceWorkspaceWidget = () => {
|
||||
usedFallbackManifest,
|
||||
hasResolvedManifest,
|
||||
} = useMediaCatalog();
|
||||
const { isPro } = usePlanTier();
|
||||
const { review, summary: weeklySummary } = useFocusStats();
|
||||
|
||||
const initialSceneId = useMemo(
|
||||
() => resolveInitialSceneId(sceneQuery, undefined),
|
||||
@@ -97,6 +102,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
>("paused");
|
||||
const [pendingSessionEntryPoint, setPendingSessionEntryPoint] =
|
||||
useState<SessionEntryPoint>("space-setup");
|
||||
const [showReviewTeaserAfterComplete, setShowReviewTeaserAfterComplete] = useState(false);
|
||||
|
||||
const {
|
||||
selectedPresetId,
|
||||
@@ -208,6 +214,27 @@ export const SpaceWorkspaceWidget = () => {
|
||||
isBootstrapping,
|
||||
syncCurrentSession,
|
||||
});
|
||||
const hasEnoughWeeklyData =
|
||||
weeklySummary.last7Days.startedSessions >= 3 &&
|
||||
(weeklySummary.last7Days.completedSessions >= 2 ||
|
||||
review.recoveryQuality.availability === "ready");
|
||||
const shouldShowSecondaryReviewTeaser =
|
||||
workspaceMode === "setup" &&
|
||||
showReviewTeaserAfterComplete &&
|
||||
hasEnoughWeeklyData;
|
||||
const secondaryReviewTeaser = shouldShowSecondaryReviewTeaser
|
||||
? {
|
||||
title: isPro
|
||||
? copy.space.setup.reviewTeaserTitlePro
|
||||
: copy.space.setup.reviewTeaserTitle,
|
||||
summary: isPro
|
||||
? review.carryForward.keepDoing
|
||||
: copy.space.setup.reviewTeaserHelper,
|
||||
ctaHref: "/stats?review=weekly&origin=space-complete",
|
||||
ctaLabel: copy.space.setup.reviewTeaserCta,
|
||||
onDismiss: () => setShowReviewTeaserAfterComplete(false),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBootstrapping && !currentSession && !hasQueryOverrides) {
|
||||
@@ -270,12 +297,17 @@ export const SpaceWorkspaceWidget = () => {
|
||||
}
|
||||
onGoalChange={selection.handleGoalChange}
|
||||
onGoalChipSelect={selection.handleGoalChipSelect}
|
||||
onStart={controls.handleSetupFocusOpen}
|
||||
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);
|
||||
@@ -285,6 +317,7 @@ export const SpaceWorkspaceWidget = () => {
|
||||
);
|
||||
},
|
||||
onStartFresh: () => {
|
||||
setShowReviewTeaserAfterComplete(false);
|
||||
selection.setGoalInput("");
|
||||
selection.setSelectedGoalId(null);
|
||||
selection.setShowResumePrompt(false);
|
||||
@@ -319,7 +352,15 @@ export const SpaceWorkspaceWidget = () => {
|
||||
onDismissReturnPrompt={awayReturnRecovery.dismissReturnPrompt}
|
||||
onStatusMessage={pushStatusLine}
|
||||
onIntentUpdate={controls.handleIntentUpdate}
|
||||
onGoalFinish={controls.handleGoalComplete}
|
||||
onGoalFinish={async () => {
|
||||
const didFinish = await controls.handleGoalComplete();
|
||||
|
||||
if (didFinish) {
|
||||
setShowReviewTeaserAfterComplete(true);
|
||||
}
|
||||
|
||||
return didFinish;
|
||||
}}
|
||||
onGoalUpdate={controls.handleGoalAdvance}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user