diff --git a/.agents/tasks/prd-mobile-refactor.json b/.agents/tasks/prd-mobile-refactor.json index 4b0d9f7..80cd09c 100644 --- a/.agents/tasks/prd-mobile-refactor.json +++ b/.agents/tasks/prd-mobile-refactor.json @@ -95,7 +95,7 @@ { "id": "US-003", "title": "Refactor oversized shared components", - "status": "in_progress", + "status": "done", "dependsOn": [ "US-001" ], @@ -107,13 +107,13 @@ "No change to public component API unless required for split" ], "startedAt": "2026-01-26T14:09:11.801117+00:00", - "completedAt": null, - "updatedAt": "2026-01-26T14:09:11.801233+00:00" + "completedAt": "2026-01-26T14:22:30.296005+00:00", + "updatedAt": "2026-01-26T14:22:30.295815+00:00" }, { "id": "US-004", "title": "Refactor oversized hooks/services/utils", - "status": "open", + "status": "in_progress", "dependsOn": [ "US-001" ], @@ -123,7 +123,10 @@ "Example: a 500-line hook becomes a main hook + `utils/-helpers.ts`", "Negative case: do not split logic in a way that increases public API surface without justification", "All previous exports remain available or are re-exported from the original entry point" - ] + ], + "startedAt": "2026-01-26T14:22:32.363863+00:00", + "completedAt": null, + "updatedAt": "2026-01-26T14:22:32.363979+00:00" }, { "id": "US-005", diff --git a/.ralph/activity.log b/.ralph/activity.log index 86d31da..5c6449b 100644 --- a/.ralph/activity.log +++ b/.ralph/activity.log @@ -1,6 +1,7 @@ # Activity Log ## Run Summary +- 2026-01-26 14:22:30 | run=20260126-140911-55357 | iter=1 | mode=build | story=US-003 | duration=799s | status=success - 2026-01-25 21:09:07 | run=20260125-204012-65223 | iter=25 | mode=build | story=US-003 | duration=0s | status=error - 2026-01-25 21:09:04 | run=20260125-204012-65223 | iter=24 | mode=build | story=US-003 | duration=0s | status=error - 2026-01-25 21:09:01 | run=20260125-204012-65223 | iter=23 | mode=build | story=US-003 | duration=0s | status=error @@ -93,3 +94,8 @@ [2026-01-26 14:17:23] Attempted Expo web for browser verification; expo start --web failed with localStorage.getItem error [2026-01-26 14:17:58] Ran quality gates and build (npm test failed: missing script; npm run lint failed with existing issues; npm run typecheck/build passed) [2026-01-26 14:21:26] Updated progress log for US-003 with commit/tests +[2026-01-26 14:22:30] ITERATION 1 end (duration=799s) +[2026-01-26 14:22:32] ITERATION 2 start (mode=build story=US-004) +[2026-01-26 14:22:45] Start US-004 work +[2026-01-26 14:25:08] Refactored useSwipeScreen helpers +[2026-01-26 14:26:13] Ran tests and build for US-004 diff --git a/.ralph/runs/run-20260126-140911-55357-iter-1.md b/.ralph/runs/run-20260126-140911-55357-iter-1.md new file mode 100644 index 0000000..3dd7d6e --- /dev/null +++ b/.ralph/runs/run-20260126-140911-55357-iter-1.md @@ -0,0 +1,59 @@ +# Ralph Run Summary + +- Run ID: 20260126-140911-55357 +- Iteration: 1 +- Mode: build +- Story: US-003: Refactor oversized shared components +- Started: 2026-01-26 14:09:11 +- Ended: 2026-01-26 14:22:30 +- Duration: 799s +- Status: success +- Log: /Users/jjbrunton/Projects/Sauci/.ralph/runs/run-20260126-140911-55357-iter-1.log + +## Git +- Head (before): a01079a7c3c6c727a55cd46a72543e86f2378932 +- Head (after): 70449659cf1ded7041b71ecf07aa2e7bc76f8346 + +### Commits +- 7044965 chore(ralph): log US-003 progress +- f302cb6 refactor(ui): split IntensitySlider pieces + +### Changed Files (commits) +- .agents/tasks/prd-mobile-refactor.json +- .ralph/activity.log +- .ralph/progress.md +- .ralph/runs/run-20260125-204012-65223-iter-10.md +- .ralph/runs/run-20260125-204012-65223-iter-11.md +- .ralph/runs/run-20260125-204012-65223-iter-12.md +- .ralph/runs/run-20260125-204012-65223-iter-13.md +- .ralph/runs/run-20260125-204012-65223-iter-14.md +- .ralph/runs/run-20260125-204012-65223-iter-15.md +- .ralph/runs/run-20260125-204012-65223-iter-16.md +- .ralph/runs/run-20260125-204012-65223-iter-17.md +- .ralph/runs/run-20260125-204012-65223-iter-18.md +- .ralph/runs/run-20260125-204012-65223-iter-19.md +- .ralph/runs/run-20260125-204012-65223-iter-2.md +- .ralph/runs/run-20260125-204012-65223-iter-20.md +- .ralph/runs/run-20260125-204012-65223-iter-21.md +- .ralph/runs/run-20260125-204012-65223-iter-22.md +- .ralph/runs/run-20260125-204012-65223-iter-23.md +- .ralph/runs/run-20260125-204012-65223-iter-24.md +- .ralph/runs/run-20260125-204012-65223-iter-25.md +- .ralph/runs/run-20260125-204012-65223-iter-3.md +- .ralph/runs/run-20260125-204012-65223-iter-4.md +- .ralph/runs/run-20260125-204012-65223-iter-5.md +- .ralph/runs/run-20260125-204012-65223-iter-6.md +- .ralph/runs/run-20260125-204012-65223-iter-7.md +- .ralph/runs/run-20260125-204012-65223-iter-8.md +- .ralph/runs/run-20260125-204012-65223-iter-9.md +- apps/mobile/src/components/ui/IntensitySlider.tsx +- apps/mobile/src/components/ui/IntensitySlider/IntensitySliderCurrentCard.tsx +- apps/mobile/src/components/ui/IntensitySlider/IntensitySliderInfoModal.tsx +- apps/mobile/src/components/ui/IntensitySlider/IntensitySliderLevels.tsx +- apps/mobile/src/components/ui/IntensitySlider/IntensitySliderTrack.tsx +- apps/mobile/src/components/ui/IntensitySlider/constants.ts +- apps/mobile/src/components/ui/IntensitySlider/styles.ts + +### Uncommitted Changes +- .ralph/activity.log + diff --git a/apps/mobile/src/features/swipe/hooks/useSwipeScreen.ts b/apps/mobile/src/features/swipe/hooks/useSwipeScreen.ts index ae1634b..9a4864f 100644 --- a/apps/mobile/src/features/swipe/hooks/useSwipeScreen.ts +++ b/apps/mobile/src/features/swipe/hooks/useSwipeScreen.ts @@ -5,7 +5,6 @@ import { useAuthStore, usePacksStore } from "../../../store"; import { skipQuestion, getSkippedQuestionIds } from "../../../lib/skippedQuestions"; import { invokeWithAuthRetry } from "../../../lib/authErrorHandler"; import { Events } from "../../../lib/analytics"; -import { getCategoryColor } from "../../../theme"; import type { AnswerType } from "../../../types"; import type { DailyLimitInfo, PackInfo, ResponseData } from "../types"; import { @@ -16,6 +15,13 @@ import { fetchRecommendedQuestions, uploadResponseMedia, } from "../services/swipeService"; +import { + buildPackInfo, + calculateEffectiveTotal, + filterSupportedQuestions, + getCountdownState, + resolveResponseData, +} from "../utils/useSwipeScreen-helpers"; interface GapInfo { unanswered: number; @@ -44,11 +50,7 @@ export const useSwipeScreen = () => { const { enabledPackIds, ensureEnabledPacksLoaded, packs } = usePacksStore(); const { user, partner, couple } = useAuthStore(); - const supportedTypes = ['swipe', 'text_answer', 'photo', 'audio', 'who_likely']; - - const filteredQuestions = questions.filter(q => - !q.question_type || supportedTypes.includes(q.question_type) - ); + const filteredQuestions = filterSupportedQuestions(questions); const hasTrackedExhausted = useRef(false); const fetchRequestId = useRef(0); @@ -194,21 +196,12 @@ export const useSwipeScreen = () => { } const updateCountdown = () => { - const now = new Date(); - const reset = new Date(dailyLimitInfo.reset_at); - const diff = reset.getTime() - now.getTime(); + const { expired, text } = getCountdownState(dailyLimitInfo.reset_at); + setCountdown(text); - if (diff <= 0) { - setCountdown("00:00:00"); + if (expired) { fetchQuestions(); - return; } - - const hours = Math.floor(diff / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`); }; updateCountdown(); @@ -290,20 +283,12 @@ export const useSwipeScreen = () => { } }, [enabledPackIds, mode, packId, questions.length, currentIndex]); - const effectiveTotal = (() => { - let total = filteredQuestions.length; - - if (gapInfo && gapInfo.threshold > 0) { - const remaining = gapInfo.threshold - gapInfo.unanswered; - total = Math.min(total, currentIndex + Math.max(0, remaining)); - } - - if (dailyLimitInfo && dailyLimitInfo.limit_value > 0 && !dailyLimitInfo.is_blocked) { - total = Math.min(total, currentIndex + dailyLimitInfo.remaining); - } - - return total; - })(); + const effectiveTotal = calculateEffectiveTotal( + filteredQuestions.length, + gapInfo, + dailyLimitInfo, + currentIndex + ); const handleAnswer = useCallback(async (questionId: string, answer: AnswerType | 'skip', responseData?: ResponseData) => { if (answer === 'skip') { @@ -314,47 +299,13 @@ export const useSwipeScreen = () => { } try { - let finalResponseData = responseData; - - if (responseData && user?.id) { - if (responseData.type === 'photo' && responseData.media_path) { - setIsUploading(true); - try { - const uploadedPath = await uploadResponseMedia( - responseData.media_path, - questionId, - 'photo', - user.id - ); - if (uploadedPath) { - finalResponseData = { ...responseData, media_path: uploadedPath }; - } else { - console.error('Photo upload failed, skipping response_data'); - finalResponseData = undefined; - } - } finally { - setIsUploading(false); - } - } else if (responseData.type === 'audio' && responseData.media_path) { - setIsUploading(true); - try { - const uploadedPath = await uploadResponseMedia( - responseData.media_path, - questionId, - 'audio', - user.id - ); - if (uploadedPath) { - finalResponseData = { ...responseData, media_path: uploadedPath }; - } else { - console.error('Audio upload failed, skipping response_data'); - finalResponseData = undefined; - } - } finally { - setIsUploading(false); - } - } - } + const finalResponseData = await resolveResponseData({ + responseData, + questionId, + userId: user?.id, + setIsUploading, + uploadResponseMedia, + }); const { data, error } = await invokeWithAuthRetry("submit-response", { body: { @@ -403,38 +354,16 @@ export const useSwipeScreen = () => { setCurrentIndex(prev => prev + 1); }, []); - const getQuestionPackInfo = useCallback((question: any): PackInfo | null => { - if (packId) { - const pack = packs.find(p => p.id === packId); - const categoryColor = pack ? getCategoryColor(pack.category) : undefined; - - if (packContext) { - return { ...packContext, color: categoryColor }; - } - if (pack) { - return { name: pack.name, icon: pack.icon || 'layers', color: categoryColor }; - } - } - - if (question.pack_id) { - const pack = packs.find(p => p.id === question.pack_id); - const categoryColor = pack ? getCategoryColor(pack.category) : undefined; - - if (question.pack_name) { - return { - name: question.pack_name, - icon: question.pack_icon || 'layers', - color: categoryColor, - }; - } - - if (pack) { - return { name: pack.name, icon: pack.icon || 'layers', color: categoryColor }; - } - } - - return null; - }, [packContext, packId, packs]); + const getQuestionPackInfo = useCallback( + (question: any): PackInfo | null => + buildPackInfo({ + packId, + packContext, + packs, + question, + }), + [packContext, packId, packs] + ); return { packId, diff --git a/apps/mobile/src/features/swipe/utils/useSwipeScreen-helpers.ts b/apps/mobile/src/features/swipe/utils/useSwipeScreen-helpers.ts new file mode 100644 index 0000000..eb599a0 --- /dev/null +++ b/apps/mobile/src/features/swipe/utils/useSwipeScreen-helpers.ts @@ -0,0 +1,146 @@ +import { getCategoryColor } from "../../../theme"; +import type { Category } from "../../../types"; +import type { DailyLimitInfo, PackInfo, ResponseData } from "../types"; + +const supportedQuestionTypes = ["swipe", "text_answer", "photo", "audio", "who_likely"]; + +interface GapInfoInput { + unanswered: number; + threshold: number; +} + +interface CountdownState { + expired: boolean; + text: string; +} + +interface PackLike { + id: string; + name: string; + icon?: string | null; + category?: Category; +} + +interface UploadResponseParams { + responseData?: ResponseData; + questionId: string; + userId?: string; + setIsUploading: (value: boolean) => void; + uploadResponseMedia: (mediaPath: string, questionId: string, mediaType: "photo" | "audio", userId: string) => Promise; +} + +export const filterSupportedQuestions = (questions: any[]) => + questions.filter(question => !question.question_type || supportedQuestionTypes.includes(question.question_type)); + +export const calculateEffectiveTotal = ( + totalQuestions: number, + gapInfo: GapInfoInput | null, + dailyLimitInfo: DailyLimitInfo | null, + currentIndex: number +) => { + let total = totalQuestions; + + if (gapInfo && gapInfo.threshold > 0) { + const remaining = gapInfo.threshold - gapInfo.unanswered; + total = Math.min(total, currentIndex + Math.max(0, remaining)); + } + + if (dailyLimitInfo && dailyLimitInfo.limit_value > 0 && !dailyLimitInfo.is_blocked) { + total = Math.min(total, currentIndex + dailyLimitInfo.remaining); + } + + return total; +}; + +export const getCountdownState = (resetAt: string): CountdownState => { + const now = new Date(); + const reset = new Date(resetAt); + const diff = reset.getTime() - now.getTime(); + + if (diff <= 0) { + return { expired: true, text: "00:00:00" }; + } + + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + return { + expired: false, + text: `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`, + }; +}; + +export const buildPackInfo = (params: { + packId?: string; + packContext: { name: string; icon: string } | null; + packs: PackLike[]; + question: any; +}): PackInfo | null => { + const { packId, packContext, packs, question } = params; + + if (packId) { + const pack = packs.find(item => item.id === packId); + const categoryColor = pack ? getCategoryColor(pack.category) : undefined; + + if (packContext) { + return { ...packContext, color: categoryColor }; + } + if (pack) { + return { name: pack.name, icon: pack.icon || "layers", color: categoryColor }; + } + } + + if (question.pack_id) { + const pack = packs.find(item => item.id === question.pack_id); + const categoryColor = pack ? getCategoryColor(pack.category) : undefined; + + if (question.pack_name) { + return { + name: question.pack_name, + icon: question.pack_icon || "layers", + color: categoryColor, + }; + } + + if (pack) { + return { name: pack.name, icon: pack.icon || "layers", color: categoryColor }; + } + } + + return null; +}; + +export const resolveResponseData = async ({ + responseData, + questionId, + userId, + setIsUploading, + uploadResponseMedia, +}: UploadResponseParams): Promise => { + if (!responseData || !userId) { + return responseData; + } + + if (responseData.type !== "photo" && responseData.type !== "audio") { + return responseData; + } + + if (!responseData.media_path) { + return responseData; + } + + setIsUploading(true); + try { + const uploadedPath = await uploadResponseMedia(responseData.media_path, questionId, responseData.type, userId); + if (uploadedPath) { + return { ...responseData, media_path: uploadedPath }; + } + console.error(`${responseData.type === "photo" ? "Photo" : "Audio"} upload failed, skipping response_data`); + return undefined; + } finally { + setIsUploading(false); + } +};