mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
refactor(swipe): split useSwipeScreen helpers
This commit is contained in:
1 parent
70449659cf
commit
5a9defb1ee
5 files changed
+253
-110
No files matched your search
@@ -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/<hook>-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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null | undefined>;
|
||||
}
|
||||
|
||||
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<ResponseData | undefined> => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user