refactor(space): scene 도메인과 current session 계약을 정리
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
export * from './model/rooms';
|
|
||||||
export * from './model/types';
|
|
||||||
2
src/entities/scene/index.ts
Normal file
2
src/entities/scene/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './model/scenes';
|
||||||
|
export * from './model/types';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import type { RoomTheme } from './types';
|
import type { SceneTheme } from './types';
|
||||||
|
|
||||||
const HUB_CURATION_ORDER = [
|
const HUB_CURATION_ORDER = [
|
||||||
'quiet-library',
|
'quiet-library',
|
||||||
@@ -9,9 +9,9 @@ const HUB_CURATION_ORDER = [
|
|||||||
'fireplace',
|
'fireplace',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const HUB_RECOMMENDED_ROOM_COUNT = 3;
|
const HUB_RECOMMENDED_SCENE_COUNT = 3;
|
||||||
|
|
||||||
export const ROOM_THEMES: RoomTheme[] = [
|
export const SCENE_THEMES: SceneTheme[] = [
|
||||||
{
|
{
|
||||||
id: 'rain-window',
|
id: 'rain-window',
|
||||||
name: '비 오는 창가',
|
name: '비 오는 창가',
|
||||||
@@ -234,65 +234,65 @@ export const ROOM_THEMES: RoomTheme[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getRoomById = (roomId: string) => {
|
export const getSceneById = (id: string) => {
|
||||||
return ROOM_THEMES.find((room) => room.id === roomId);
|
return SCENE_THEMES.find((scene) => scene.id === id);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRoomCardPhotoUrl = (room: RoomTheme) => {
|
export const getSceneCardPhotoUrl = (scene: SceneTheme) => {
|
||||||
// Swap managedCardPhotoUrl when you start serving first-party assets.
|
// Swap managedCardPhotoUrl when you start serving first-party assets.
|
||||||
return room.managedCardPhotoUrl ?? room.cardPhotoUrl;
|
return scene.managedCardPhotoUrl ?? scene.cardPhotoUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRoomCardBackgroundStyle = (room: RoomTheme): CSSProperties => {
|
export const getSceneCardBackgroundStyle = (scene: SceneTheme): CSSProperties => {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url('${getRoomCardPhotoUrl(room)}')`,
|
backgroundImage: `url('${getSceneCardPhotoUrl(scene)}')`,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRoomBackgroundStyle = (room: RoomTheme): CSSProperties => {
|
export const getSceneBackgroundStyle = (scene: SceneTheme): CSSProperties => {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url('${getRoomCardPhotoUrl(room)}'), linear-gradient(160deg, #1e293b 0%, #0f172a 100%)`,
|
backgroundImage: `url('${getSceneCardPhotoUrl(scene)}'), linear-gradient(160deg, #1e293b 0%, #0f172a 100%)`,
|
||||||
backgroundSize: 'cover, cover',
|
backgroundSize: 'cover, cover',
|
||||||
backgroundPosition: 'center, center',
|
backgroundPosition: 'center, center',
|
||||||
backgroundRepeat: 'no-repeat, no-repeat',
|
backgroundRepeat: 'no-repeat, no-repeat',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const uniqueByRoomId = (rooms: Array<RoomTheme | undefined>) => {
|
const uniqueBySceneId = (scenes: Array<SceneTheme | undefined>) => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
||||||
return rooms.filter((room): room is RoomTheme => {
|
return scenes.filter((scene): scene is SceneTheme => {
|
||||||
if (!room || seen.has(room.id)) {
|
if (!scene || seen.has(scene.id)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
seen.add(room.id);
|
seen.add(scene.id);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHubRoomSections = (
|
export const getHubSceneSections = (
|
||||||
rooms: RoomTheme[],
|
scenes: SceneTheme[],
|
||||||
selectedRoomId: string,
|
selectedSceneId: string,
|
||||||
recommendedCount = HUB_RECOMMENDED_ROOM_COUNT,
|
recommendedCount = HUB_RECOMMENDED_SCENE_COUNT,
|
||||||
) => {
|
) => {
|
||||||
const roomById = new Map(rooms.map((room) => [room.id, room] as const));
|
const sceneById = new Map(scenes.map((scene) => [scene.id, scene] as const));
|
||||||
const selectedRoom = roomById.get(selectedRoomId);
|
const selectedScene = sceneById.get(selectedSceneId);
|
||||||
const curatedRooms = HUB_CURATION_ORDER.map((id) => roomById.get(id));
|
const curatedScenes = HUB_CURATION_ORDER.map((id) => sceneById.get(id));
|
||||||
|
|
||||||
const recommendedRooms = uniqueByRoomId([
|
const recommendedScenes = uniqueBySceneId([
|
||||||
selectedRoom,
|
selectedScene,
|
||||||
...curatedRooms,
|
...curatedScenes,
|
||||||
...rooms,
|
...scenes,
|
||||||
]).slice(0, recommendedCount);
|
]).slice(0, recommendedCount);
|
||||||
|
|
||||||
const recommendedRoomIds = new Set(recommendedRooms.map((room) => room.id));
|
const recommendedSceneIds = new Set(recommendedScenes.map((scene) => scene.id));
|
||||||
const allRooms = [...recommendedRooms, ...rooms.filter((room) => !recommendedRoomIds.has(room.id))];
|
const allScenes = [...recommendedScenes, ...scenes.filter((scene) => !recommendedSceneIds.has(scene.id))];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
recommendedRooms,
|
recommendedScenes,
|
||||||
allRooms,
|
allScenes,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
export type RoomTag = '저자극' | '움직임 적음' | '딥워크' | '감성';
|
export type SceneTag = '저자극' | '움직임 적음' | '딥워크' | '감성';
|
||||||
|
|
||||||
export interface RoomPresence {
|
export interface ScenePresence {
|
||||||
focus: number;
|
focus: number;
|
||||||
break: number;
|
break: number;
|
||||||
away: number;
|
away: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomTheme {
|
export interface SceneTheme {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
tags: RoomTag[];
|
tags: SceneTag[];
|
||||||
recommendedSound: string;
|
recommendedSound: string;
|
||||||
recommendedSoundPresetId: string;
|
recommendedSoundPresetId: string;
|
||||||
recommendedTimerPresetId: string;
|
recommendedTimerPresetId: string;
|
||||||
@@ -21,7 +21,7 @@ export interface RoomTheme {
|
|||||||
googleImageSearchUrl: string;
|
googleImageSearchUrl: string;
|
||||||
managedCardPhotoUrl: string | null;
|
managedCardPhotoUrl: string | null;
|
||||||
activeMembers: number;
|
activeMembers: number;
|
||||||
presence: RoomPresence;
|
presence: ScenePresence;
|
||||||
previewImage: string;
|
previewImage: string;
|
||||||
previewGradient: string;
|
previewGradient: string;
|
||||||
}
|
}
|
||||||
@@ -72,25 +72,25 @@ export const RECENT_THOUGHTS: RecentThought[] = [
|
|||||||
{
|
{
|
||||||
id: 'thought-1',
|
id: 'thought-1',
|
||||||
text: '내일 미팅 전에 제안서 첫 문단만 다시 다듬기',
|
text: '내일 미팅 전에 제안서 첫 문단만 다시 다듬기',
|
||||||
roomName: '도서관',
|
sceneName: '도서관',
|
||||||
capturedAt: '방금 전',
|
capturedAt: '방금 전',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'thought-2',
|
id: 'thought-2',
|
||||||
text: '기획 문서의 핵심 흐름을 한 문장으로 정리해두기',
|
text: '기획 문서의 핵심 흐름을 한 문장으로 정리해두기',
|
||||||
roomName: '비 오는 창가',
|
sceneName: '비 오는 창가',
|
||||||
capturedAt: '24분 전',
|
capturedAt: '24분 전',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'thought-3',
|
id: 'thought-3',
|
||||||
text: '오후에 확인할 이슈 번호만 메모하고 지금 작업 복귀',
|
text: '오후에 확인할 이슈 번호만 메모하고 지금 작업 복귀',
|
||||||
roomName: '숲',
|
sceneName: '숲',
|
||||||
capturedAt: '1시간 전',
|
capturedAt: '1시간 전',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'thought-4',
|
id: 'thought-4',
|
||||||
text: '리뷰 코멘트는 오늘 17시 이후에 한 번에 처리',
|
text: '리뷰 코멘트는 오늘 17시 이후에 한 번에 처리',
|
||||||
roomName: '벽난로',
|
sceneName: '벽난로',
|
||||||
capturedAt: '어제',
|
capturedAt: '어제',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export interface FocusStatCard {
|
|||||||
export interface RecentThought {
|
export interface RecentThought {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
roomName: string;
|
sceneName: string;
|
||||||
capturedAt: string;
|
capturedAt: string;
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,37 @@ const readStoredThoughts = () => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed.filter((thought): thought is RecentThought => {
|
return parsed.flatMap((thought): RecentThought[] => {
|
||||||
return (
|
if (!thought || typeof thought !== 'object') {
|
||||||
thought &&
|
return [];
|
||||||
typeof thought.id === 'string' &&
|
}
|
||||||
typeof thought.text === 'string' &&
|
|
||||||
typeof thought.roomName === 'string' &&
|
const sceneName =
|
||||||
typeof thought.capturedAt === 'string' &&
|
typeof thought.sceneName === 'string'
|
||||||
(typeof thought.isCompleted === 'undefined' || typeof thought.isCompleted === 'boolean')
|
? thought.sceneName
|
||||||
);
|
: typeof thought.roomName === 'string'
|
||||||
|
? thought.roomName
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof thought.id !== 'string' ||
|
||||||
|
typeof thought.text !== 'string' ||
|
||||||
|
typeof thought.capturedAt !== 'string' ||
|
||||||
|
!sceneName ||
|
||||||
|
(typeof thought.isCompleted !== 'undefined' && typeof thought.isCompleted !== 'boolean')
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: thought.id,
|
||||||
|
text: thought.text,
|
||||||
|
sceneName,
|
||||||
|
capturedAt: thought.capturedAt,
|
||||||
|
isCompleted: thought.isCompleted,
|
||||||
|
},
|
||||||
|
];
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
@@ -70,7 +92,7 @@ export const useThoughtInbox = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addThought = useCallback((text: string, roomName: string) => {
|
const addThought = useCallback((text: string, sceneName: string) => {
|
||||||
const trimmedText = text.trim();
|
const trimmedText = text.trim();
|
||||||
|
|
||||||
if (!trimmedText) {
|
if (!trimmedText) {
|
||||||
@@ -80,7 +102,7 @@ export const useThoughtInbox = () => {
|
|||||||
const thought: RecentThought = {
|
const thought: RecentThought = {
|
||||||
id: `thought-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
id: `thought-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
text: trimmedText,
|
text: trimmedText,
|
||||||
roomName,
|
sceneName,
|
||||||
capturedAt: '방금 전',
|
capturedAt: '방금 전',
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type FocusSessionCompletionType = 'goal-complete' | 'timer-complete';
|
|||||||
|
|
||||||
export interface FocusSession {
|
export interface FocusSession {
|
||||||
id: string;
|
id: string;
|
||||||
roomId: string;
|
sceneId: string;
|
||||||
goal: string;
|
goal: string;
|
||||||
timerPresetId: string;
|
timerPresetId: string;
|
||||||
soundPresetId: string | null;
|
soundPresetId: string | null;
|
||||||
@@ -24,7 +24,7 @@ export interface FocusSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StartFocusSessionRequest {
|
export interface StartFocusSessionRequest {
|
||||||
roomId: string;
|
sceneId: string;
|
||||||
goal: string;
|
goal: string;
|
||||||
timerPresetId: string;
|
timerPresetId: string;
|
||||||
soundPresetId?: string | null;
|
soundPresetId?: string | null;
|
||||||
@@ -53,7 +53,7 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 새로운 집중 세션을 생성하고 즉시 running 상태로 시작한다.
|
* - 새로운 집중 세션을 생성하고 즉시 running 상태로 시작한다.
|
||||||
* - roomId, goal, timerPresetId, soundPresetId를 저장한다.
|
* - sceneId, goal, timerPresetId, soundPresetId를 저장한다.
|
||||||
* - 응답에는 생성 직후의 세션 상태와 남은 시간 계산에 필요한 시각 필드를 포함한다.
|
* - 응답에는 생성 직후의 세션 상태와 남은 시간 계산에 필요한 시각 필드를 포함한다.
|
||||||
*/
|
*/
|
||||||
startSession: async (payload: StartFocusSessionRequest): Promise<FocusSession> => {
|
startSession: async (payload: StartFocusSessionRequest): Promise<FocusSession> => {
|
||||||
@@ -66,11 +66,12 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 현재 세션의 현재 phase를 일시정지한다.
|
* - 현재 세션의 현재 phase를 일시정지한다.
|
||||||
|
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||||
* - phaseRemainingSeconds를 정확히 저장해 재개 시 이어서 동작하게 한다.
|
* - phaseRemainingSeconds를 정확히 저장해 재개 시 이어서 동작하게 한다.
|
||||||
* - 이미 paused 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
* - 이미 paused 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
||||||
*/
|
*/
|
||||||
pauseSession: async (sessionId: string): Promise<FocusSession> => {
|
pauseSession: async (): Promise<FocusSession> => {
|
||||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/pause`, {
|
return apiClient<FocusSession>('api/v1/focus-sessions/current/pause', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -78,11 +79,12 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 일시정지된 세션을 재개하고 새 phaseEndsAt/serverNow를 반환한다.
|
* - 일시정지된 세션을 재개하고 새 phaseEndsAt/serverNow를 반환한다.
|
||||||
|
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||||
* - 이미 running 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
* - 이미 running 상태여도 멱등적으로 최신 세션 상태를 반환한다.
|
||||||
* - 남은 시간을 다시 계산할 수 있게 phaseRemainingSeconds도 함께 내려준다.
|
* - 남은 시간을 다시 계산할 수 있게 phaseRemainingSeconds도 함께 내려준다.
|
||||||
*/
|
*/
|
||||||
resumeSession: async (sessionId: string): Promise<FocusSession> => {
|
resumeSession: async (): Promise<FocusSession> => {
|
||||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/resume`, {
|
return apiClient<FocusSession>('api/v1/focus-sessions/current/resume', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -90,11 +92,12 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 현재 진행 중인 phase를 처음 길이로 다시 시작한다.
|
* - 현재 진행 중인 phase를 처음 길이로 다시 시작한다.
|
||||||
|
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||||
* - focus/break 어느 phase인지 유지한 채 phaseStartedAt, phaseEndsAt, phaseRemainingSeconds를 갱신한다.
|
* - focus/break 어느 phase인지 유지한 채 phaseStartedAt, phaseEndsAt, phaseRemainingSeconds를 갱신한다.
|
||||||
* - 클라이언트의 Reset 버튼은 이 API 응답으로 즉시 HUD를 다시 그린다.
|
* - 클라이언트의 Reset 버튼은 이 API 응답으로 즉시 HUD를 다시 그린다.
|
||||||
*/
|
*/
|
||||||
restartCurrentPhase: async (sessionId: string): Promise<FocusSession> => {
|
restartCurrentPhase: async (): Promise<FocusSession> => {
|
||||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/restart-phase`, {
|
return apiClient<FocusSession>('api/v1/focus-sessions/current/restart-phase', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -102,14 +105,12 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 현재 세션을 완료 처리하고 통계 집계 대상으로 반영한다.
|
* - 현재 세션을 완료 처리하고 통계 집계 대상으로 반영한다.
|
||||||
|
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||||
* - completionType으로 goal-complete / timer-complete을 구분해 저장한다.
|
* - completionType으로 goal-complete / timer-complete을 구분해 저장한다.
|
||||||
* - 완료된 세션 스냅샷을 반환하거나, 최소한 성공적으로 완료되었음을 알 수 있는 응답을 보낸다.
|
* - 완료된 세션 스냅샷을 반환하거나, 최소한 성공적으로 완료되었음을 알 수 있는 응답을 보낸다.
|
||||||
*/
|
*/
|
||||||
completeSession: async (
|
completeSession: async (payload: CompleteFocusSessionRequest): Promise<FocusSession> => {
|
||||||
sessionId: string,
|
return apiClient<FocusSession>('api/v1/focus-sessions/current/complete', {
|
||||||
payload: CompleteFocusSessionRequest,
|
|
||||||
): Promise<FocusSession> => {
|
|
||||||
return apiClient<FocusSession>(`api/v1/focus-sessions/${sessionId}/complete`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
@@ -118,11 +119,12 @@ export const focusSessionApi = {
|
|||||||
/**
|
/**
|
||||||
* Backend Codex:
|
* Backend Codex:
|
||||||
* - 현재 세션을 중도 종료(abandon) 처리한다.
|
* - 현재 세션을 중도 종료(abandon) 처리한다.
|
||||||
|
* - 클라이언트는 sessionId 대신 access token으로 사용자의 current session을 식별한다.
|
||||||
* - 통계에서는 abandon 여부를 구분할 수 있게 저장한다.
|
* - 통계에서는 abandon 여부를 구분할 수 있게 저장한다.
|
||||||
* - 성공 시 204 또는 빈 성공 응답을 반환한다.
|
* - 성공 시 204 또는 빈 성공 응답을 반환한다.
|
||||||
*/
|
*/
|
||||||
abandonSession: async (sessionId: string): Promise<void> => {
|
abandonSession: async (): Promise<void> => {
|
||||||
return apiClient<void>(`api/v1/focus-sessions/${sessionId}/abandon`, {
|
return apiClient<void>('api/v1/focus-sessions/current/abandon', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
expectNoContent: true,
|
expectNoContent: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await runMutation(
|
const session = await runMutation(
|
||||||
() => focusSessionApi.pauseSession(currentSession.id),
|
() => focusSessionApi.pauseSession(),
|
||||||
'세션을 일시정지하지 못했어요.',
|
'세션을 일시정지하지 못했어요.',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await runMutation(
|
const session = await runMutation(
|
||||||
() => focusSessionApi.resumeSession(currentSession.id),
|
() => focusSessionApi.resumeSession(),
|
||||||
'세션을 다시 시작하지 못했어요.',
|
'세션을 다시 시작하지 못했어요.',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await runMutation(
|
const session = await runMutation(
|
||||||
() => focusSessionApi.restartCurrentPhase(currentSession.id),
|
() => focusSessionApi.restartCurrentPhase(),
|
||||||
'현재 페이즈를 다시 시작하지 못했어요.',
|
'현재 페이즈를 다시 시작하지 못했어요.',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await runMutation(
|
const session = await runMutation(
|
||||||
() => focusSessionApi.completeSession(currentSession.id, payload),
|
() => focusSessionApi.completeSession(payload),
|
||||||
'세션을 완료 처리하지 못했어요.',
|
'세션을 완료 처리하지 못했어요.',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export const useFocusSessionEngine = (): UseFocusSessionEngineResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await runMutation(
|
const result = await runMutation(
|
||||||
() => focusSessionApi.abandonSession(currentSession.id),
|
() => focusSessionApi.abandonSession(),
|
||||||
'세션을 종료하지 못했어요.',
|
'세션을 종료하지 못했어요.',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const InboxList = ({ thoughts, onCompleteThought, onDeleteThought, classN
|
|||||||
{thought.text}
|
{thought.text}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1.5 text-[11px] text-white/54">
|
<p className="mt-1.5 text-[11px] text-white/54">
|
||||||
{thought.roomName} · {thought.capturedAt}
|
{thought.sceneName} · {thought.capturedAt}
|
||||||
</p>
|
</p>
|
||||||
<div className="absolute right-2 top-2 flex items-center gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100">
|
<div className="absolute right-2 top-2 flex items-center gap-1 opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100">
|
||||||
<button
|
<button
|
||||||
|
|||||||
1
src/features/scene-select/index.ts
Normal file
1
src/features/scene-select/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './ui/SceneSelectCarousel';
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
import { getRoomCardBackgroundStyle, type RoomTheme } from '@/entities/room';
|
import { getSceneCardBackgroundStyle, type SceneTheme } from '@/entities/scene';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
|
|
||||||
interface SpaceSelectCarouselProps {
|
interface SceneSelectCarouselProps {
|
||||||
rooms: RoomTheme[];
|
scenes: SceneTheme[];
|
||||||
selectedRoomId: string;
|
selectedSceneId: string;
|
||||||
onSelect: (roomId: string) => void;
|
onSelect: (sceneId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpaceSelectCarousel = ({
|
export const SceneSelectCarousel = ({
|
||||||
rooms,
|
scenes,
|
||||||
selectedRoomId,
|
selectedSceneId,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: SpaceSelectCarouselProps) => {
|
}: SceneSelectCarouselProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="-mx-1 overflow-x-auto px-1 pb-1">
|
<div className="-mx-1 overflow-x-auto px-1 pb-1">
|
||||||
<div className="flex min-w-full gap-2.5">
|
<div className="flex min-w-full gap-2.5">
|
||||||
{rooms.map((room) => {
|
{scenes.map((scene) => {
|
||||||
const selected = room.id === selectedRoomId;
|
const selected = scene.id === selectedSceneId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={room.id}
|
key={scene.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelect(room.id)}
|
onClick={() => onSelect(scene.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative h-24 min-w-[138px] overflow-hidden rounded-xl border text-left sm:min-w-[148px]',
|
'group relative h-24 min-w-[138px] overflow-hidden rounded-xl border text-left sm:min-w-[148px]',
|
||||||
selected
|
selected
|
||||||
? 'border-sky-200/38 shadow-[0_0_0_1px_rgba(186,230,253,0.2),0_0_10px_rgba(56,189,248,0.12)]'
|
? 'border-sky-200/38 shadow-[0_0_0_1px_rgba(186,230,253,0.2),0_0_10px_rgba(56,189,248,0.12)]'
|
||||||
: 'border-white/16 hover:border-white/24',
|
: 'border-white/16 hover:border-white/24',
|
||||||
)}
|
)}
|
||||||
style={getRoomCardBackgroundStyle(room)}
|
style={getSceneCardBackgroundStyle(scene)}
|
||||||
aria-label={`${room.name} 선택`}
|
aria-label={`${scene.name} 선택`}
|
||||||
>
|
>
|
||||||
<span className="absolute inset-x-0 bottom-0 h-[56%] bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.2)_52%,rgba(2,6,23,0.24)_100%)]" />
|
<span className="absolute inset-x-0 bottom-0 h-[56%] bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.2)_52%,rgba(2,6,23,0.24)_100%)]" />
|
||||||
{selected ? (
|
{selected ? (
|
||||||
@@ -39,8 +39,8 @@ export const SpaceSelectCarousel = ({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="absolute inset-x-2 bottom-2">
|
<div className="absolute inset-x-2 bottom-2">
|
||||||
<p className="truncate text-sm font-semibold text-white/96">{room.name}</p>
|
<p className="truncate text-sm font-semibold text-white/96">{scene.name}</p>
|
||||||
<p className="truncate text-[11px] text-white/66">{room.vibeLabel}</p>
|
<p className="truncate text-[11px] text-white/66">{scene.vibeLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './ui/SpaceSelectCarousel';
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
|
import { useAuthStore } from '@/store/useAuthStore';
|
||||||
|
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080';
|
||||||
const TOKEN_COOKIE_KEY = 'vr_access_token';
|
const TOKEN_COOKIE_KEY = 'vr_access_token';
|
||||||
@@ -41,6 +42,16 @@ const isRecord = (value: unknown): value is Record<string, unknown> => {
|
|||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readAccessToken = () => {
|
||||||
|
const storeToken = useAuthStore.getState().accessToken;
|
||||||
|
|
||||||
|
if (storeToken) {
|
||||||
|
return storeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cookies.get(TOKEN_COOKIE_KEY) ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
const readErrorMessage = async (response: Response) => {
|
const readErrorMessage = async (response: Response) => {
|
||||||
const contentType = response.headers.get('content-type') ?? '';
|
const contentType = response.headers.get('content-type') ?? '';
|
||||||
|
|
||||||
@@ -67,7 +78,7 @@ export const apiClient = async <T>(
|
|||||||
...requestOptions
|
...requestOptions
|
||||||
} = options;
|
} = options;
|
||||||
const url = `${API_BASE_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
const url = `${API_BASE_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
||||||
const token = Cookies.get(TOKEN_COOKIE_KEY);
|
const token = readAccessToken();
|
||||||
const defaultHeaders: Record<string, string> = {
|
const defaultHeaders: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { PlanTier } from '@/entities/plan';
|
|||||||
import {
|
import {
|
||||||
PRO_FEATURE_CARDS,
|
PRO_FEATURE_CARDS,
|
||||||
} from '@/entities/plan';
|
} from '@/entities/plan';
|
||||||
import { getRoomCardBackgroundStyle, type RoomTheme } from '@/entities/room';
|
import { getSceneCardBackgroundStyle, type SceneTheme } from '@/entities/scene';
|
||||||
import { SOUND_PRESETS, type TimerPreset } from '@/entities/session';
|
import { SOUND_PRESETS, type TimerPreset } from '@/entities/session';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
|
import { useReducedMotion } from '@/shared/lib/useReducedMotion';
|
||||||
@@ -13,8 +13,8 @@ import { Toggle } from '@/shared/ui';
|
|||||||
|
|
||||||
interface ControlCenterSheetWidgetProps {
|
interface ControlCenterSheetWidgetProps {
|
||||||
plan: PlanTier;
|
plan: PlanTier;
|
||||||
rooms: RoomTheme[];
|
scenes: SceneTheme[];
|
||||||
selectedRoomId: string;
|
selectedSceneId: string;
|
||||||
selectedTimerLabel: string;
|
selectedTimerLabel: string;
|
||||||
selectedSoundPresetId: string;
|
selectedSoundPresetId: string;
|
||||||
sceneRecommendedSoundLabel: string;
|
sceneRecommendedSoundLabel: string;
|
||||||
@@ -22,7 +22,7 @@ interface ControlCenterSheetWidgetProps {
|
|||||||
timerPresets: TimerPreset[];
|
timerPresets: TimerPreset[];
|
||||||
autoHideControls: boolean;
|
autoHideControls: boolean;
|
||||||
onAutoHideControlsChange: (next: boolean) => void;
|
onAutoHideControlsChange: (next: boolean) => void;
|
||||||
onSelectRoom: (roomId: string) => void;
|
onSelectScene: (sceneId: string) => void;
|
||||||
onSelectTimer: (timerLabel: string) => void;
|
onSelectTimer: (timerLabel: string) => void;
|
||||||
onSelectSound: (presetId: string) => void;
|
onSelectSound: (presetId: string) => void;
|
||||||
onSelectProFeature: (featureId: string) => void;
|
onSelectProFeature: (featureId: string) => void;
|
||||||
@@ -40,8 +40,8 @@ const SectionTitle = ({ title, description }: { title: string; description: stri
|
|||||||
|
|
||||||
export const ControlCenterSheetWidget = ({
|
export const ControlCenterSheetWidget = ({
|
||||||
plan,
|
plan,
|
||||||
rooms,
|
scenes,
|
||||||
selectedRoomId,
|
selectedSceneId,
|
||||||
selectedTimerLabel,
|
selectedTimerLabel,
|
||||||
selectedSoundPresetId,
|
selectedSoundPresetId,
|
||||||
sceneRecommendedSoundLabel,
|
sceneRecommendedSoundLabel,
|
||||||
@@ -49,7 +49,7 @@ export const ControlCenterSheetWidget = ({
|
|||||||
timerPresets,
|
timerPresets,
|
||||||
autoHideControls,
|
autoHideControls,
|
||||||
onAutoHideControlsChange,
|
onAutoHideControlsChange,
|
||||||
onSelectRoom,
|
onSelectScene,
|
||||||
onSelectTimer,
|
onSelectTimer,
|
||||||
onSelectSound,
|
onSelectSound,
|
||||||
onSelectProFeature,
|
onSelectProFeature,
|
||||||
@@ -64,14 +64,14 @@ export const ControlCenterSheetWidget = ({
|
|||||||
? 'transition-none'
|
? 'transition-none'
|
||||||
: 'transition-colors duration-[220ms] ease-out';
|
: 'transition-colors duration-[220ms] ease-out';
|
||||||
|
|
||||||
const selectedRoom = useMemo(() => {
|
const selectedScene = useMemo(() => {
|
||||||
return rooms.find((room) => room.id === selectedRoomId) ?? rooms[0];
|
return scenes.find((scene) => scene.id === selectedSceneId) ?? scenes[0];
|
||||||
}, [rooms, selectedRoomId]);
|
}, [scenes, selectedSceneId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
<section className="space-y-2.5 rounded-2xl border border-white/12 bg-black/22 p-3.5 backdrop-blur-md">
|
||||||
<SectionTitle title="Scene" description={selectedRoom?.name ?? '공간'} />
|
<SectionTitle title="Background" description={selectedScene?.name ?? '기본 배경'} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 snap-x snap-mandatory scrollbar-none',
|
'-mx-1 flex gap-2.5 overflow-x-auto px-1 pb-1.5 snap-x snap-mandatory scrollbar-none',
|
||||||
@@ -79,15 +79,15 @@ export const ControlCenterSheetWidget = ({
|
|||||||
)}
|
)}
|
||||||
style={{ scrollBehavior: reducedMotion ? 'auto' : 'smooth' }}
|
style={{ scrollBehavior: reducedMotion ? 'auto' : 'smooth' }}
|
||||||
>
|
>
|
||||||
{rooms.slice(0, 6).map((room) => {
|
{scenes.slice(0, 6).map((scene) => {
|
||||||
const selected = room.id === selectedRoomId;
|
const selected = scene.id === selectedSceneId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={room.id}
|
key={scene.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSelectRoom(room.id);
|
onSelectScene(scene.id);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative h-24 w-[130px] shrink-0 snap-start overflow-hidden rounded-xl border text-left',
|
'relative h-24 w-[130px] shrink-0 snap-start overflow-hidden rounded-xl border text-left',
|
||||||
@@ -96,11 +96,11 @@ export const ControlCenterSheetWidget = ({
|
|||||||
selected ? 'border-sky-200/44 shadow-[0_8px_16px_rgba(56,189,248,0.18)]' : 'border-white/16',
|
selected ? 'border-sky-200/44 shadow-[0_8px_16px_rgba(56,189,248,0.18)]' : 'border-white/16',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div aria-hidden className="absolute inset-0 bg-cover bg-center" style={getRoomCardBackgroundStyle(room)} />
|
<div aria-hidden className="absolute inset-0 bg-cover bg-center" style={getSceneCardBackgroundStyle(scene)} />
|
||||||
<div aria-hidden className="absolute inset-0 bg-gradient-to-t from-black/56 via-black/18 to-black/6" />
|
<div aria-hidden className="absolute inset-0 bg-gradient-to-t from-black/56 via-black/18 to-black/6" />
|
||||||
<div className="absolute inset-x-2 bottom-2 min-w-0">
|
<div className="absolute inset-x-2 bottom-2 min-w-0">
|
||||||
<p className="truncate text-sm font-medium text-white/90">{room.name}</p>
|
<p className="truncate text-sm font-medium text-white/90">{scene.name}</p>
|
||||||
<p className="truncate text-[11px] text-white/66">{room.vibeLabel}</p>
|
<p className="truncate text-[11px] text-white/66">{scene.vibeLabel}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||||
import type { RoomTheme } from '@/entities/room';
|
import type { SceneTheme } from '@/entities/scene';
|
||||||
import type { GoalChip, SoundPreset, TimerPreset } from '@/entities/session';
|
import type { GoalChip, SoundPreset, TimerPreset } from '@/entities/session';
|
||||||
import { SpaceSelectCarousel } from '@/features/space-select';
|
import { SceneSelectCarousel } from '@/features/scene-select';
|
||||||
import { SessionGoalField } from '@/features/session-goal';
|
import { SessionGoalField } from '@/features/session-goal';
|
||||||
import { Button } from '@/shared/ui';
|
import { Button } from '@/shared/ui';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
@@ -12,8 +12,8 @@ type RitualPopover = 'space' | 'timer' | 'sound';
|
|||||||
|
|
||||||
interface SpaceSetupDrawerWidgetProps {
|
interface SpaceSetupDrawerWidgetProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
rooms: RoomTheme[];
|
scenes: SceneTheme[];
|
||||||
selectedRoomId: string;
|
selectedSceneId: string;
|
||||||
selectedTimerLabel: string;
|
selectedTimerLabel: string;
|
||||||
selectedSoundPresetId: string;
|
selectedSoundPresetId: string;
|
||||||
goalInput: string;
|
goalInput: string;
|
||||||
@@ -22,7 +22,7 @@ interface SpaceSetupDrawerWidgetProps {
|
|||||||
soundPresets: SoundPreset[];
|
soundPresets: SoundPreset[];
|
||||||
timerPresets: TimerPreset[];
|
timerPresets: TimerPreset[];
|
||||||
canStart: boolean;
|
canStart: boolean;
|
||||||
onRoomSelect: (roomId: string) => void;
|
onSceneSelect: (sceneId: string) => void;
|
||||||
onTimerSelect: (timerLabel: string) => void;
|
onTimerSelect: (timerLabel: string) => void;
|
||||||
onSoundSelect: (soundPresetId: string) => void;
|
onSoundSelect: (soundPresetId: string) => void;
|
||||||
onGoalChange: (value: string) => void;
|
onGoalChange: (value: string) => void;
|
||||||
@@ -63,8 +63,8 @@ const SummaryChip = ({ label, value, open, onClick }: SummaryChipProps) => {
|
|||||||
|
|
||||||
export const SpaceSetupDrawerWidget = ({
|
export const SpaceSetupDrawerWidget = ({
|
||||||
open,
|
open,
|
||||||
rooms,
|
scenes,
|
||||||
selectedRoomId,
|
selectedSceneId,
|
||||||
selectedTimerLabel,
|
selectedTimerLabel,
|
||||||
selectedSoundPresetId,
|
selectedSoundPresetId,
|
||||||
goalInput,
|
goalInput,
|
||||||
@@ -73,7 +73,7 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
soundPresets,
|
soundPresets,
|
||||||
timerPresets,
|
timerPresets,
|
||||||
canStart,
|
canStart,
|
||||||
onRoomSelect,
|
onSceneSelect,
|
||||||
onTimerSelect,
|
onTimerSelect,
|
||||||
onSoundSelect,
|
onSoundSelect,
|
||||||
onGoalChange,
|
onGoalChange,
|
||||||
@@ -84,9 +84,9 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
const [openPopover, setOpenPopover] = useState<RitualPopover | null>(null);
|
const [openPopover, setOpenPopover] = useState<RitualPopover | null>(null);
|
||||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const selectedRoom = useMemo(() => {
|
const selectedScene = useMemo(() => {
|
||||||
return rooms.find((room) => room.id === selectedRoomId) ?? rooms[0];
|
return scenes.find((scene) => scene.id === selectedSceneId) ?? scenes[0];
|
||||||
}, [rooms, selectedRoomId]);
|
}, [scenes, selectedSceneId]);
|
||||||
|
|
||||||
const selectedSoundLabel = useMemo(() => {
|
const selectedSoundLabel = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -183,8 +183,8 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
<div className="relative mb-3">
|
<div className="relative mb-3">
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
<SummaryChip
|
<SummaryChip
|
||||||
label="공간"
|
label="배경"
|
||||||
value={selectedRoom?.name ?? '기본 공간'}
|
value={selectedScene?.name ?? '기본 배경'}
|
||||||
open={openPopover === 'space'}
|
open={openPopover === 'space'}
|
||||||
onClick={() => togglePopover('space')}
|
onClick={() => togglePopover('space')}
|
||||||
/>
|
/>
|
||||||
@@ -204,11 +204,11 @@ export const SpaceSetupDrawerWidget = ({
|
|||||||
|
|
||||||
{openPopover === 'space' ? (
|
{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">
|
<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">
|
||||||
<SpaceSelectCarousel
|
<SceneSelectCarousel
|
||||||
rooms={rooms.slice(0, 4)}
|
scenes={scenes.slice(0, 4)}
|
||||||
selectedRoomId={selectedRoomId}
|
selectedSceneId={selectedSceneId}
|
||||||
onSelect={(roomId) => {
|
onSelect={(sceneId) => {
|
||||||
onRoomSelect(roomId);
|
onSceneSelect(sceneId);
|
||||||
setOpenPopover(null);
|
setOpenPopover(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
import { useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||||
import type { PlanTier } from '@/entities/plan';
|
import type { PlanTier } from '@/entities/plan';
|
||||||
import type { RoomTheme } from '@/entities/room';
|
import type { SceneTheme } from '@/entities/scene';
|
||||||
import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session';
|
import { SOUND_PRESETS, type RecentThought, type TimerPreset } from '@/entities/session';
|
||||||
import { ExitHoldButton } from '@/features/exit-hold';
|
import { ExitHoldButton } from '@/features/exit-hold';
|
||||||
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
|
import { ManagePlanSheetContent, PaywallSheetContent } from '@/features/paywall-sheet';
|
||||||
@@ -19,8 +19,8 @@ import { QuickSoundPopover } from './popovers/QuickSoundPopover';
|
|||||||
import { InboxToolPanel } from './panels/InboxToolPanel';
|
import { InboxToolPanel } from './panels/InboxToolPanel';
|
||||||
interface SpaceToolsDockWidgetProps {
|
interface SpaceToolsDockWidgetProps {
|
||||||
isFocusMode: boolean;
|
isFocusMode: boolean;
|
||||||
rooms: RoomTheme[];
|
scenes: SceneTheme[];
|
||||||
selectedRoomId: string;
|
selectedSceneId: string;
|
||||||
selectedTimerLabel: string;
|
selectedTimerLabel: string;
|
||||||
timerPresets: TimerPreset[];
|
timerPresets: TimerPreset[];
|
||||||
selectedPresetId: string;
|
selectedPresetId: string;
|
||||||
@@ -32,7 +32,7 @@ interface SpaceToolsDockWidgetProps {
|
|||||||
thoughtCount: number;
|
thoughtCount: number;
|
||||||
sceneRecommendedSoundLabel: string;
|
sceneRecommendedSoundLabel: string;
|
||||||
sceneRecommendedTimerLabel: string;
|
sceneRecommendedTimerLabel: string;
|
||||||
onRoomSelect: (roomId: string) => void;
|
onSceneSelect: (sceneId: string) => void;
|
||||||
onTimerSelect: (timerLabel: string) => void;
|
onTimerSelect: (timerLabel: string) => void;
|
||||||
onQuickSoundSelect: (presetId: string) => void;
|
onQuickSoundSelect: (presetId: string) => void;
|
||||||
onCaptureThought: (note: string) => RecentThought | null;
|
onCaptureThought: (note: string) => RecentThought | null;
|
||||||
@@ -47,8 +47,8 @@ interface SpaceToolsDockWidgetProps {
|
|||||||
|
|
||||||
export const SpaceToolsDockWidget = ({
|
export const SpaceToolsDockWidget = ({
|
||||||
isFocusMode,
|
isFocusMode,
|
||||||
rooms,
|
scenes,
|
||||||
selectedRoomId,
|
selectedSceneId,
|
||||||
selectedTimerLabel,
|
selectedTimerLabel,
|
||||||
timerPresets,
|
timerPresets,
|
||||||
selectedPresetId,
|
selectedPresetId,
|
||||||
@@ -60,7 +60,7 @@ export const SpaceToolsDockWidget = ({
|
|||||||
thoughtCount,
|
thoughtCount,
|
||||||
sceneRecommendedSoundLabel,
|
sceneRecommendedSoundLabel,
|
||||||
sceneRecommendedTimerLabel,
|
sceneRecommendedTimerLabel,
|
||||||
onRoomSelect,
|
onSceneSelect,
|
||||||
onTimerSelect,
|
onTimerSelect,
|
||||||
onQuickSoundSelect,
|
onQuickSoundSelect,
|
||||||
onCaptureThought,
|
onCaptureThought,
|
||||||
@@ -495,8 +495,8 @@ export const SpaceToolsDockWidget = ({
|
|||||||
{utilityPanel === 'control-center' ? (
|
{utilityPanel === 'control-center' ? (
|
||||||
<ControlCenterSheetWidget
|
<ControlCenterSheetWidget
|
||||||
plan={plan}
|
plan={plan}
|
||||||
rooms={rooms}
|
scenes={scenes}
|
||||||
selectedRoomId={selectedRoomId}
|
selectedSceneId={selectedSceneId}
|
||||||
selectedTimerLabel={selectedTimerLabel}
|
selectedTimerLabel={selectedTimerLabel}
|
||||||
selectedSoundPresetId={selectedPresetId}
|
selectedSoundPresetId={selectedPresetId}
|
||||||
sceneRecommendedSoundLabel={sceneRecommendedSoundLabel}
|
sceneRecommendedSoundLabel={sceneRecommendedSoundLabel}
|
||||||
@@ -504,8 +504,8 @@ export const SpaceToolsDockWidget = ({
|
|||||||
timerPresets={timerPresets}
|
timerPresets={timerPresets}
|
||||||
autoHideControls={autoHideControls}
|
autoHideControls={autoHideControls}
|
||||||
onAutoHideControlsChange={setAutoHideControls}
|
onAutoHideControlsChange={setAutoHideControls}
|
||||||
onSelectRoom={(roomId) => {
|
onSelectScene={(sceneId) => {
|
||||||
onRoomSelect(roomId);
|
onSceneSelect(sceneId);
|
||||||
}}
|
}}
|
||||||
onSelectTimer={(label) => {
|
onSelectTimer={(label) => {
|
||||||
onTimerSelect(label);
|
onTimerSelect(label);
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { RoomTheme } from '@/entities/room';
|
import type { SceneTheme } from '@/entities/scene';
|
||||||
import type { TimerPreset } from '@/entities/session';
|
import type { TimerPreset } from '@/entities/session';
|
||||||
import { DEFAULT_PRESET_OPTIONS } from '@/shared/config/settingsOptions';
|
import { DEFAULT_PRESET_OPTIONS } from '@/shared/config/settingsOptions';
|
||||||
import { cn } from '@/shared/lib/cn';
|
import { cn } from '@/shared/lib/cn';
|
||||||
|
|
||||||
interface SettingsToolPanelProps {
|
interface SettingsToolPanelProps {
|
||||||
rooms: RoomTheme[];
|
scenes: SceneTheme[];
|
||||||
selectedRoomId: string;
|
selectedSceneId: string;
|
||||||
selectedTimerLabel: string;
|
selectedTimerLabel: string;
|
||||||
timerPresets: TimerPreset[];
|
timerPresets: TimerPreset[];
|
||||||
onSelectRoom: (roomId: string) => void;
|
onSelectScene: (sceneId: string) => void;
|
||||||
onSelectTimer: (timerLabel: string) => void;
|
onSelectTimer: (timerLabel: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsToolPanel = ({
|
export const SettingsToolPanel = ({
|
||||||
rooms,
|
scenes,
|
||||||
selectedRoomId,
|
selectedSceneId,
|
||||||
selectedTimerLabel,
|
selectedTimerLabel,
|
||||||
timerPresets,
|
timerPresets,
|
||||||
onSelectRoom,
|
onSelectScene,
|
||||||
onSelectTimer,
|
onSelectTimer,
|
||||||
}: SettingsToolPanelProps) => {
|
}: SettingsToolPanelProps) => {
|
||||||
const [reduceMotion, setReduceMotion] = useState(false);
|
const [reduceMotion, setReduceMotion] = useState(false);
|
||||||
@@ -59,17 +59,17 @@ export const SettingsToolPanel = ({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
<section className="rounded-2xl border border-white/14 bg-white/7 px-3.5 py-3">
|
||||||
<p className="text-sm font-medium text-white">공간</p>
|
<p className="text-sm font-medium text-white">배경</p>
|
||||||
<p className="mt-1 text-xs text-white/58">몰입 중에도 공간을 바꿀 수 있어요.</p>
|
<p className="mt-1 text-xs text-white/58">몰입 중에도 배경 scene을 바꿀 수 있어요.</p>
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{rooms.slice(0, 4).map((room) => {
|
{scenes.slice(0, 4).map((scene) => {
|
||||||
const selected = room.id === selectedRoomId;
|
const selected = scene.id === selectedSceneId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={room.id}
|
key={scene.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectRoom(room.id)}
|
onClick={() => onSelectScene(scene.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full border px-3 py-1.5 text-xs transition-colors',
|
'rounded-full border px-3 py-1.5 text-xs transition-colors',
|
||||||
selected
|
selected
|
||||||
@@ -77,7 +77,7 @@ export const SettingsToolPanel = ({
|
|||||||
: 'border-white/18 bg-white/8 text-white/80 hover:bg-white/14',
|
: 'border-white/18 bg-white/8 text-white/80 hover:bg-white/14',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{room.name}
|
{scene.name}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
getRoomBackgroundStyle,
|
getSceneBackgroundStyle,
|
||||||
getRoomById,
|
getSceneById,
|
||||||
ROOM_THEMES,
|
SCENE_THEMES,
|
||||||
} from '@/entities/room';
|
} from '@/entities/scene';
|
||||||
import {
|
import {
|
||||||
GOAL_CHIPS,
|
GOAL_CHIPS,
|
||||||
SOUND_PRESETS,
|
SOUND_PRESETS,
|
||||||
@@ -62,16 +62,16 @@ const readStoredWorkspaceSelection = (): StoredWorkspaceSelection => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveInitialRoomId = (roomIdFromQuery: string | null, storedSceneId?: string) => {
|
const resolveInitialSceneId = (sceneIdFromQuery: string | null, storedSceneId?: string) => {
|
||||||
if (roomIdFromQuery && getRoomById(roomIdFromQuery)) {
|
if (sceneIdFromQuery && getSceneById(sceneIdFromQuery)) {
|
||||||
return roomIdFromQuery;
|
return sceneIdFromQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedSceneId && getRoomById(storedSceneId)) {
|
if (storedSceneId && getSceneById(storedSceneId)) {
|
||||||
return storedSceneId;
|
return storedSceneId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ROOM_THEMES[0].id;
|
return SCENE_THEMES[0].id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveInitialSoundPreset = (
|
const resolveInitialSoundPreset = (
|
||||||
@@ -150,11 +150,11 @@ const resolveFocusTimeDisplayFromTimerLabel = (timerLabel: string) => {
|
|||||||
|
|
||||||
export const SpaceWorkspaceWidget = () => {
|
export const SpaceWorkspaceWidget = () => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const roomQuery = searchParams.get('room');
|
const sceneQuery = searchParams.get('scene') ?? searchParams.get('room');
|
||||||
const goalQuery = searchParams.get('goal')?.trim() ?? '';
|
const goalQuery = searchParams.get('goal')?.trim() ?? '';
|
||||||
const soundQuery = searchParams.get('sound');
|
const soundQuery = searchParams.get('sound');
|
||||||
const timerQuery = searchParams.get('timer');
|
const timerQuery = searchParams.get('timer');
|
||||||
const hasQueryOverrides = Boolean(roomQuery || goalQuery || soundQuery || timerQuery);
|
const hasQueryOverrides = Boolean(sceneQuery || goalQuery || soundQuery || timerQuery);
|
||||||
const {
|
const {
|
||||||
thoughts,
|
thoughts,
|
||||||
thoughtCount,
|
thoughtCount,
|
||||||
@@ -166,22 +166,22 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
setThoughtCompleted,
|
setThoughtCompleted,
|
||||||
} = useThoughtInbox();
|
} = useThoughtInbox();
|
||||||
|
|
||||||
const initialRoomId = resolveInitialRoomId(roomQuery, undefined);
|
const initialSceneId = resolveInitialSceneId(sceneQuery, undefined);
|
||||||
const initialRoom = getRoomById(initialRoomId) ?? ROOM_THEMES[0];
|
const initialScene = getSceneById(initialSceneId) ?? SCENE_THEMES[0];
|
||||||
const initialGoal = goalQuery;
|
const initialGoal = goalQuery;
|
||||||
const initialSoundPresetId = resolveInitialSoundPreset(
|
const initialSoundPresetId = resolveInitialSoundPreset(
|
||||||
soundQuery,
|
soundQuery,
|
||||||
undefined,
|
undefined,
|
||||||
initialRoom.recommendedSoundPresetId,
|
initialScene.recommendedSoundPresetId,
|
||||||
);
|
);
|
||||||
const initialTimerLabel = resolveInitialTimerLabel(
|
const initialTimerLabel = resolveInitialTimerLabel(
|
||||||
timerQuery,
|
timerQuery,
|
||||||
undefined,
|
undefined,
|
||||||
initialRoom.recommendedTimerPresetId,
|
initialScene.recommendedTimerPresetId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('setup');
|
const [workspaceMode, setWorkspaceMode] = useState<WorkspaceMode>('setup');
|
||||||
const [selectedRoomId, setSelectedRoomId] = useState(initialRoomId);
|
const [selectedSceneId, setSelectedSceneId] = useState(initialSceneId);
|
||||||
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
const [selectedTimerLabel, setSelectedTimerLabel] = useState(initialTimerLabel);
|
||||||
const [goalInput, setGoalInput] = useState(initialGoal);
|
const [goalInput, setGoalInput] = useState(initialGoal);
|
||||||
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
const [selectedGoalId, setSelectedGoalId] = useState<string | null>(null);
|
||||||
@@ -216,19 +216,19 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
abandonSession,
|
abandonSession,
|
||||||
} = useFocusSessionEngine();
|
} = useFocusSessionEngine();
|
||||||
|
|
||||||
const selectedRoom = useMemo(() => {
|
const selectedScene = useMemo(() => {
|
||||||
return getRoomById(selectedRoomId) ?? ROOM_THEMES[0];
|
return getSceneById(selectedSceneId) ?? SCENE_THEMES[0];
|
||||||
}, [selectedRoomId]);
|
}, [selectedSceneId]);
|
||||||
|
|
||||||
const setupRooms = useMemo(() => {
|
const setupScenes = useMemo(() => {
|
||||||
const visibleRooms = ROOM_THEMES.slice(0, 6);
|
const visibleScenes = SCENE_THEMES.slice(0, 6);
|
||||||
|
|
||||||
if (visibleRooms.some((room) => room.id === selectedRoom.id)) {
|
if (visibleScenes.some((scene) => scene.id === selectedScene.id)) {
|
||||||
return visibleRooms;
|
return visibleScenes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [selectedRoom, ...visibleRooms].slice(0, 6);
|
return [selectedScene, ...visibleScenes].slice(0, 6);
|
||||||
}, [selectedRoom]);
|
}, [selectedScene]);
|
||||||
|
|
||||||
const canStart = goalInput.trim().length > 0;
|
const canStart = goalInput.trim().length > 0;
|
||||||
const isFocusMode = workspaceMode === 'focus';
|
const isFocusMode = workspaceMode === 'focus';
|
||||||
@@ -237,17 +237,17 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
const resolvedTimeDisplay = timeDisplay ?? resolveFocusTimeDisplayFromTimerLabel(selectedTimerLabel);
|
const resolvedTimeDisplay = timeDisplay ?? resolveFocusTimeDisplayFromTimerLabel(selectedTimerLabel);
|
||||||
|
|
||||||
const applyRecommendedSelections = useCallback((
|
const applyRecommendedSelections = useCallback((
|
||||||
roomId: string,
|
sceneId: string,
|
||||||
overrideState: SelectionOverride = selectionOverride,
|
overrideState: SelectionOverride = selectionOverride,
|
||||||
) => {
|
) => {
|
||||||
const room = getRoomById(roomId);
|
const scene = getSceneById(sceneId);
|
||||||
|
|
||||||
if (!room) {
|
if (!scene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overrideState.timer) {
|
if (!overrideState.timer) {
|
||||||
const recommendedTimerLabel = resolveTimerLabelFromPresetId(room.recommendedTimerPresetId);
|
const recommendedTimerLabel = resolveTimerLabelFromPresetId(scene.recommendedTimerPresetId);
|
||||||
|
|
||||||
if (recommendedTimerLabel) {
|
if (recommendedTimerLabel) {
|
||||||
setSelectedTimerLabel(recommendedTimerLabel);
|
setSelectedTimerLabel(recommendedTimerLabel);
|
||||||
@@ -256,9 +256,9 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!overrideState.sound &&
|
!overrideState.sound &&
|
||||||
SOUND_PRESETS.some((preset) => preset.id === room.recommendedSoundPresetId)
|
SOUND_PRESETS.some((preset) => preset.id === scene.recommendedSoundPresetId)
|
||||||
) {
|
) {
|
||||||
setSelectedPresetId(room.recommendedSoundPresetId);
|
setSelectedPresetId(scene.recommendedSoundPresetId);
|
||||||
}
|
}
|
||||||
}, [selectionOverride, setSelectedPresetId]);
|
}, [selectionOverride, setSelectedPresetId]);
|
||||||
|
|
||||||
@@ -268,8 +268,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
sound: Boolean(storedSelection.override?.sound),
|
sound: Boolean(storedSelection.override?.sound),
|
||||||
timer: Boolean(storedSelection.override?.timer),
|
timer: Boolean(storedSelection.override?.timer),
|
||||||
};
|
};
|
||||||
const restoredRoomId =
|
const restoredSceneId =
|
||||||
!roomQuery && storedSelection.sceneId && getRoomById(storedSelection.sceneId)
|
!sceneQuery && storedSelection.sceneId && getSceneById(storedSelection.sceneId)
|
||||||
? storedSelection.sceneId
|
? storedSelection.sceneId
|
||||||
: null;
|
: null;
|
||||||
const restoredTimerLabel = !timerQuery
|
const restoredTimerLabel = !timerQuery
|
||||||
@@ -283,8 +283,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
const rafId = window.requestAnimationFrame(() => {
|
const rafId = window.requestAnimationFrame(() => {
|
||||||
setSelectionOverride(restoredSelectionOverride);
|
setSelectionOverride(restoredSelectionOverride);
|
||||||
|
|
||||||
if (restoredRoomId) {
|
if (restoredSceneId) {
|
||||||
setSelectedRoomId(restoredRoomId);
|
setSelectedSceneId(restoredSceneId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoredTimerLabel) {
|
if (restoredTimerLabel) {
|
||||||
@@ -306,7 +306,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(rafId);
|
window.cancelAnimationFrame(rafId);
|
||||||
};
|
};
|
||||||
}, [goalQuery, hasQueryOverrides, roomQuery, setSelectedPresetId, soundQuery, timerQuery]);
|
}, [goalQuery, hasQueryOverrides, sceneQuery, setSelectedPresetId, soundQuery, timerQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
@@ -321,7 +321,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
? currentSession.soundPresetId
|
? currentSession.soundPresetId
|
||||||
: selectedPresetId;
|
: selectedPresetId;
|
||||||
const rafId = window.requestAnimationFrame(() => {
|
const rafId = window.requestAnimationFrame(() => {
|
||||||
setSelectedRoomId(currentSession.roomId);
|
setSelectedSceneId(currentSession.sceneId);
|
||||||
setSelectedTimerLabel(nextTimerLabel);
|
setSelectedTimerLabel(nextTimerLabel);
|
||||||
setSelectedPresetId(nextSoundPresetId);
|
setSelectedPresetId(nextSoundPresetId);
|
||||||
setGoalInput(currentSession.goal);
|
setGoalInput(currentSession.goal);
|
||||||
@@ -336,9 +336,9 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
};
|
};
|
||||||
}, [currentSession, selectedPresetId, selectedTimerLabel, setSelectedPresetId]);
|
}, [currentSession, selectedPresetId, selectedTimerLabel, setSelectedPresetId]);
|
||||||
|
|
||||||
const handleSelectRoom = (roomId: string) => {
|
const handleSelectScene = (sceneId: string) => {
|
||||||
setSelectedRoomId(roomId);
|
setSelectedSceneId(sceneId);
|
||||||
applyRecommendedSelections(roomId);
|
applyRecommendedSelections(sceneId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectTimer = (timerLabel: string, markOverride = false) => {
|
const handleSelectTimer = (timerLabel: string, markOverride = false) => {
|
||||||
@@ -407,7 +407,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
setWorkspaceMode('focus');
|
setWorkspaceMode('focus');
|
||||||
|
|
||||||
const startedSession = await startSession({
|
const startedSession = await startSession({
|
||||||
roomId: selectedRoomId,
|
sceneId: selectedSceneId,
|
||||||
goal: trimmedGoal,
|
goal: trimmedGoal,
|
||||||
timerPresetId,
|
timerPresetId,
|
||||||
soundPresetId: selectedPresetId,
|
soundPresetId: selectedPresetId,
|
||||||
@@ -552,21 +552,21 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
WORKSPACE_SELECTION_STORAGE_KEY,
|
WORKSPACE_SELECTION_STORAGE_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
sceneId: selectedRoomId,
|
sceneId: selectedSceneId,
|
||||||
timerPresetId,
|
timerPresetId,
|
||||||
soundPresetId: selectedPresetId,
|
soundPresetId: selectedPresetId,
|
||||||
goal: normalizedGoal,
|
goal: normalizedGoal,
|
||||||
override: selectionOverride,
|
override: selectionOverride,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [goalInput, hasHydratedSelection, resumeGoal, selectedRoomId, selectedTimerLabel, selectedPresetId, selectionOverride, showResumePrompt]);
|
}, [goalInput, hasHydratedSelection, resumeGoal, selectedSceneId, selectedTimerLabel, selectedPresetId, selectionOverride, showResumePrompt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-dvh overflow-hidden text-white">
|
<div className="relative h-dvh overflow-hidden text-white">
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="absolute inset-0 bg-cover bg-center will-change-transform animate-[space-stage-pan_42s_ease-in-out_infinite_alternate] motion-reduce:animate-none"
|
className="absolute inset-0 bg-cover bg-center will-change-transform animate-[space-stage-pan_42s_ease-in-out_infinite_alternate] motion-reduce:animate-none"
|
||||||
style={getRoomBackgroundStyle(selectedRoom)}
|
style={getSceneBackgroundStyle(selectedScene)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative z-10 flex h-full flex-col">
|
<div className="relative z-10 flex h-full flex-col">
|
||||||
@@ -575,8 +575,8 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
|
|
||||||
<SpaceSetupDrawerWidget
|
<SpaceSetupDrawerWidget
|
||||||
open={!isFocusMode}
|
open={!isFocusMode}
|
||||||
rooms={setupRooms}
|
scenes={setupScenes}
|
||||||
selectedRoomId={selectedRoom.id}
|
selectedSceneId={selectedScene.id}
|
||||||
selectedTimerLabel={selectedTimerLabel}
|
selectedTimerLabel={selectedTimerLabel}
|
||||||
selectedSoundPresetId={selectedPresetId}
|
selectedSoundPresetId={selectedPresetId}
|
||||||
goalInput={goalInput}
|
goalInput={goalInput}
|
||||||
@@ -585,7 +585,7 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
soundPresets={SOUND_PRESETS}
|
soundPresets={SOUND_PRESETS}
|
||||||
timerPresets={TIMER_SELECTION_PRESETS}
|
timerPresets={TIMER_SELECTION_PRESETS}
|
||||||
canStart={canStart}
|
canStart={canStart}
|
||||||
onRoomSelect={handleSelectRoom}
|
onSceneSelect={handleSelectScene}
|
||||||
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
||||||
onSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
onSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
||||||
onGoalChange={handleGoalChange}
|
onGoalChange={handleGoalChange}
|
||||||
@@ -641,23 +641,23 @@ export const SpaceWorkspaceWidget = () => {
|
|||||||
|
|
||||||
<SpaceToolsDockWidget
|
<SpaceToolsDockWidget
|
||||||
isFocusMode={isFocusMode}
|
isFocusMode={isFocusMode}
|
||||||
rooms={setupRooms}
|
scenes={setupScenes}
|
||||||
selectedRoomId={selectedRoom.id}
|
selectedSceneId={selectedScene.id}
|
||||||
selectedTimerLabel={selectedTimerLabel}
|
selectedTimerLabel={selectedTimerLabel}
|
||||||
timerPresets={TIMER_SELECTION_PRESETS}
|
timerPresets={TIMER_SELECTION_PRESETS}
|
||||||
thoughts={thoughts}
|
thoughts={thoughts}
|
||||||
thoughtCount={thoughtCount}
|
thoughtCount={thoughtCount}
|
||||||
selectedPresetId={selectedPresetId}
|
selectedPresetId={selectedPresetId}
|
||||||
onRoomSelect={handleSelectRoom}
|
onSceneSelect={handleSelectScene}
|
||||||
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
onTimerSelect={(timerLabel) => handleSelectTimer(timerLabel, true)}
|
||||||
onQuickSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
onQuickSoundSelect={(presetId) => handleSelectSound(presetId, true)}
|
||||||
sceneRecommendedSoundLabel={selectedRoom.recommendedSound}
|
sceneRecommendedSoundLabel={selectedScene.recommendedSound}
|
||||||
sceneRecommendedTimerLabel={resolveTimerLabelFromPresetId(selectedRoom.recommendedTimerPresetId) ?? selectedTimerLabel}
|
sceneRecommendedTimerLabel={resolveTimerLabelFromPresetId(selectedScene.recommendedTimerPresetId) ?? selectedTimerLabel}
|
||||||
soundVolume={masterVolume}
|
soundVolume={masterVolume}
|
||||||
onSetSoundVolume={setMasterVolume}
|
onSetSoundVolume={setMasterVolume}
|
||||||
isSoundMuted={isMuted}
|
isSoundMuted={isMuted}
|
||||||
onSetSoundMuted={setMuted}
|
onSetSoundMuted={setMuted}
|
||||||
onCaptureThought={(note) => addThought(note, selectedRoom.name)}
|
onCaptureThought={(note) => addThought(note, selectedScene.name)}
|
||||||
onDeleteThought={removeThought}
|
onDeleteThought={removeThought}
|
||||||
onSetThoughtCompleted={setThoughtCompleted}
|
onSetThoughtCompleted={setThoughtCompleted}
|
||||||
onRestoreThought={restoreThought}
|
onRestoreThought={restoreThought}
|
||||||
|
|||||||
Reference in New Issue
Block a user