diff --git a/src/app/globals.css b/src/app/globals.css
index 9baeee3..f7fbd93 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -145,22 +145,22 @@
}
html {
+ width: 100%;
height: 100%;
+ overflow: hidden;
overscroll-behavior: none;
- overscroll-behavior-y: none;
}
body {
- min-height: 100%;
+ width: 100%;
+ height: 100%;
margin: 0;
display: flex;
justify-content: center;
+ overflow: hidden;
color: var(--foreground);
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
overscroll-behavior: none;
- overscroll-behavior-y: none;
- -webkit-overflow-scrolling: touch;
- touch-action: pan-y;
}
html:lang(ar) body,
@@ -186,17 +186,21 @@ html:lang(ar) body,
.app-shell {
width: 100%;
- min-height: 100%;
+ height: 100%;
+ height: 100dvh;
padding-inline: 17px;
padding-bottom: var(--safe-bottom, 0px);
box-sizing: border-box;
+ overflow-x: hidden;
+ overflow-y: auto;
background-color: var(--background);
background-image: var(--default-page-background-image);
background-position: top;
background-repeat: no-repeat;
background-size: cover;
overscroll-behavior: none;
- overscroll-behavior-y: none;
+ touch-action: pan-y;
+ -webkit-overflow-scrolling: touch;
}
html[data-web-bootstrap="pending"] .app-shell {
diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx
index 84cb682..2ab03a4 100644
--- a/src/app/questions-list/page.tsx
+++ b/src/app/questions-list/page.tsx
@@ -1,41 +1,46 @@
"use client";
+import { useQueryClient } from "@tanstack/react-query";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { IoClose } from "react-icons/io5";
-import {
- getSubmitPath,
- hasCompletedMarriageProfileBasics,
-} from "@/lib/get-submit-path";
-import QuestionCard from "@/components/Componentes/question-card";
-import RequiredStepsCard from "@/components/Componentes/required-steps-card";
import Button from "@/components/Componentes/button";
+import ErrorToast from "@/components/Componentes/error-toast";
+import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
-import { getQuestionAnswersStorageKey } from "@/components/Componentes/question-answer-storage";
-import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import { PageBackground } from "@/components/Componentes/page-background";
-import ErrorToast from "@/components/Componentes/error-toast";
-import { useQueryClient } from "@tanstack/react-query";
-import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start";
-import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
+import { getQuestionAnswersStorageKey } from "@/components/Componentes/question-answer-storage";
+import QuestionCard from "@/components/Componentes/question-card";
+import RequiredStepsCard from "@/components/Componentes/required-steps-card";
+import type { MarriageField } from "@/hooks/marriage/types";
import {
getFormSection,
useFormOverviewQuery,
} from "@/hooks/marriage/use-form-schema";
-import { convertOverviewToFrontendItems } from "@/lib/schema-adapter";
-import type { QuestionListItem } from "@/lib/schema-adapter";
+import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start";
+import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
+import {
+ applyProfilePatchResultToCache,
+ updateMarriageSectionData,
+} from "@/hooks/marriage/use-section-data";
import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back";
+import { getAssessmentLocalProgress } from "@/lib/assessment-progress";
+import {
+ getSubmitPath,
+ hasCompletedMarriageProfileBasics,
+} from "@/lib/get-submit-path";
+import { REQUIRED_PROFILE_SECTION_COUNT } from "@/lib/marriage-profile-contract";
import {
clearMatchStartGrace,
markMatchStarted,
} from "@/lib/match-start-grace";
+import type { QuestionListItem } from "@/lib/schema-adapter";
+import { convertOverviewToFrontendItems } from "@/lib/schema-adapter";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import SectionsRequest from "./sections-request";
-import { getAssessmentLocalProgress } from "@/lib/assessment-progress";
-import { REQUIRED_PROFILE_SECTION_COUNT } from "@/lib/marriage-profile-contract";
export default function QuestionsListPage() {
useCloseServiceOnBack();
@@ -58,7 +63,8 @@ export default function QuestionsListPage() {
const isProfileRedirecting =
profileTargetPath !== null &&
profileTargetPath !== "/questions-list" &&
- (!hasCompletedMarriageProfileBasics(profile) || profile?.can_edit_profile === false);
+ (!hasCompletedMarriageProfileBasics(profile) ||
+ profile?.can_edit_profile === false);
useEffect(() => {
if (isProfileRedirecting && profileTargetPath) {
@@ -153,15 +159,22 @@ export default function QuestionsListPage() {
setDisplayedRequiredSections(completedRequiredSections);
return;
}
- const direction = completedRequiredSections > displayedRequiredSections ? 1 : -1;
- const distance = Math.abs(completedRequiredSections - displayedRequiredSections);
- const interval = window.setInterval(() => {
- setDisplayedRequiredSections((current) => {
- const next = current + direction;
- if (next === completedRequiredSections) window.clearInterval(interval);
- return next;
- });
- }, Math.max(90, Math.floor(500 / distance)));
+ const direction =
+ completedRequiredSections > displayedRequiredSections ? 1 : -1;
+ const distance = Math.abs(
+ completedRequiredSections - displayedRequiredSections,
+ );
+ const interval = window.setInterval(
+ () => {
+ setDisplayedRequiredSections((current) => {
+ const next = current + direction;
+ if (next === completedRequiredSections)
+ window.clearInterval(interval);
+ return next;
+ });
+ },
+ Math.max(90, Math.floor(500 / distance)),
+ );
return () => window.clearInterval(interval);
}, [completedRequiredSections, displayedRequiredSections]);
@@ -186,7 +199,11 @@ export default function QuestionsListPage() {
const progress = sectionProgressBySlug.get(item.slug) ?? 0;
return progress >= 100;
});
- }, [hasValidRequiredContract, requiredQuestionListItems, sectionProgressBySlug]);
+ }, [
+ hasValidRequiredContract,
+ requiredQuestionListItems,
+ sectionProgressBySlug,
+ ]);
const profileStatus = profile?.status;
const isProfileSuspended = profileStatus === "suspended";
@@ -200,12 +217,24 @@ export default function QuestionsListPage() {
const syncPendingAnswers = useCallback(async () => {
if (!overview) return;
- const { updateMarriageSectionData } = await import(
- "@/hooks/marriage/use-section-data"
- );
-
- let currentVersion = overview.version;
+ const pendingSections: Array<{
+ storageKey: string;
+ storedValue: {
+ current_step: number;
+ fields: MarriageField[];
+ pending_keys: string[];
+ pending_sync: boolean;
+ };
+ fields: MarriageField[];
+ slug: string;
+ }> = [];
for (const item of questionListItems) {
+ if (
+ item.slug === "personality_test" ||
+ item.slug === "glasser_5_needs_test"
+ ) {
+ continue;
+ }
const storageKey = getQuestionAnswersStorageKey(item.slug);
const rawValue = window.localStorage.getItem(storageKey);
if (!rawValue) continue;
@@ -218,22 +247,40 @@ export default function QuestionsListPage() {
? storedValue.pending_keys
: storedValue.fields.map((field: { key: string }) => field.key),
);
- for (const field of storedValue.fields) {
- if (!pendingKeys.has(field.key)) continue;
- const result = await updateMarriageSectionData(item.slug, {
- version: currentVersion,
- current_step: storedValue.current_step,
- fields: [field],
- });
- currentVersion = result.version;
- pendingKeys.delete(field.key);
- storedValue.pending_keys = [...pendingKeys];
+ const pendingFields = storedValue.fields.filter(
+ (field: { key: string }) => pendingKeys.has(field.key),
+ );
+ if (pendingFields.length === 0) continue;
+ pendingSections.push({
+ storageKey,
+ storedValue,
+ fields: pendingFields,
+ slug: item.slug,
+ });
+ }
+
+ if (pendingSections.length === 0) return;
+ const result = await updateMarriageSectionData(pendingSections[0].slug, {
+ version: overview.version,
+ current_step: pendingSections[0].storedValue.current_step,
+ fields: pendingSections.flatMap((section) => section.fields),
+ });
+ applyProfilePatchResultToCache(queryClient, locale, result);
+ const cleared = new Set(result.cleared_answer_ids ?? []);
+
+ for (const { storageKey, storedValue } of pendingSections) {
+ storedValue.fields = storedValue.fields.filter(
+ (field: { key: string }) => !cleared.has(field.key),
+ );
+ storedValue.pending_sync = false;
+ storedValue.pending_keys = [];
+ if (storedValue.fields.length === 0) {
+ window.localStorage.removeItem(storageKey);
+ } else {
window.localStorage.setItem(storageKey, JSON.stringify(storedValue));
}
- storedValue.pending_sync = false;
- window.localStorage.setItem(storageKey, JSON.stringify(storedValue));
}
- }, [overview, questionListItems]);
+ }, [locale, overview, queryClient, questionListItems]);
const prefetchSection = useCallback(
(item: QuestionListItem) => {
diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx
index dc0863a..3329edb 100644
--- a/src/components/Componentes/question-answer-storage.tsx
+++ b/src/components/Componentes/question-answer-storage.tsx
@@ -11,7 +11,6 @@ import {
useRef,
useState,
} from "react";
-import type { QuestionField } from "@/lib/schema-adapter";
import { pathParam } from "@/hooks/marriage/path-param";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import type {
@@ -22,10 +21,12 @@ import type {
} from "@/hooks/marriage/types";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
+ applyProfilePatchResultToCache,
useMarriageSectionDataQuery,
useUpdateMarriageSectionDataMutation,
} from "@/hooks/marriage/use-section-data";
import { getApiRequestUrl } from "@/lib/http";
+import type { QuestionField } from "@/lib/schema-adapter";
const STORAGE_VERSION = 2;
@@ -121,7 +122,7 @@ function createQuestionField(
question: QuestionField,
value: MarriageFieldValue,
): MarriageField {
- let option_id = undefined;
+ let option_id: string | string[] | undefined;
if (question.options && Array.isArray(question.options)) {
if (question.type === "checkbox" && Array.isArray(value)) {
@@ -484,14 +485,12 @@ export function QuestionAnswersProvider({
questionsRef.current,
backendFieldsRef.current,
);
- const nextDirtyKey = fullPayload.fields.find((field) =>
+ const pendingFields = fullPayload.fields.filter((field) =>
dirtyKeysRef.current.has(field.key),
- )?.key;
+ );
const payload = {
...fullPayload,
- fields: nextDirtyKey
- ? fullPayload.fields.filter((field) => field.key === nextDirtyKey)
- : [],
+ fields: pendingFields,
version: schemaVersionRef.current,
};
const revision = answersRevisionRef.current;
@@ -504,6 +503,30 @@ export function QuestionAnswersProvider({
if (result?.version) {
schemaVersionRef.current = result.version;
}
+ const cleared = new Set(result?.cleared_answer_ids ?? []);
+ const nextAnswers = { ...answersRef.current };
+ const questionById = new Map(
+ questionsRef.current.map((question) => [question.id, question]),
+ );
+ Object.entries(result?.answers ?? {}).forEach(([key, answer]) => {
+ const question = questionById.get(key);
+ if (question) {
+ nextAnswers[key] = {
+ key,
+ label: question.title,
+ type: question.type,
+ value: answer.option_id ?? answer.value,
+ option_id: answer.option_id,
+ } as MarriageField;
+ }
+ });
+ cleared.forEach((key) => {
+ delete nextAnswers[key];
+ });
+ answersRef.current = nextAnswers;
+ setAnswers(nextAnswers);
+ applyProfilePatchResultToCache(queryClient, locale, result);
+ return undefined;
});
flushPromiseRef.current = request;
@@ -524,7 +547,9 @@ export function QuestionAnswersProvider({
return;
}
- if (nextDirtyKey) dirtyKeysRef.current.delete(nextDirtyKey);
+ pendingFields.forEach((field) => {
+ dirtyKeysRef.current.delete(field.key);
+ });
if (dirtyKeysRef.current.size > 0) {
await flushAnswersRef.current();
@@ -543,7 +568,7 @@ export function QuestionAnswersProvider({
[],
);
},
- [mutateAsync, canEdit],
+ [mutateAsync, canEdit, locale, queryClient],
);
const flushAnswersRef = useRef(flushAnswers);
@@ -572,7 +597,7 @@ export function QuestionAnswersProvider({
const pendingFields = fullPayload.fields.filter((field) =>
dirtyKeysRef.current.has(field.key),
);
- const payload = { ...fullPayload, fields: pendingFields.slice(0, 1) };
+ const payload = { ...fullPayload, fields: pendingFields };
const revision = answersRevisionRef.current;
if (payload.fields.length === 0) {
@@ -614,7 +639,9 @@ export function QuestionAnswersProvider({
}
hasPendingSyncRef.current = false;
- dirtyKeysRef.current.delete(payload.fields[0].key);
+ payload.fields.forEach((field) => {
+ dirtyKeysRef.current.delete(field.key);
+ });
const stillPending = dirtyKeysRef.current.size > 0;
hasPendingSyncRef.current = stillPending;
setHasPendingSync(stillPending);
diff --git a/src/components/Componentes/question-answer.test.tsx b/src/components/Componentes/question-answer.test.tsx
index 1a50e0b..0f99de7 100644
--- a/src/components/Componentes/question-answer.test.tsx
+++ b/src/components/Componentes/question-answer.test.tsx
@@ -1,11 +1,23 @@
-import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react";
-import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { QuestionAnswersProvider, useQuestionAnswers } from "./question-answer-storage";
-import { convertSchemaToFrontendItems } from "@/lib/schema-adapter";
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
-import { useMarriageSectionDataQuery, useUpdateMarriageSectionDataMutation } from "@/hooks/marriage/use-section-data";
+import {
+ useMarriageSectionDataQuery,
+ useUpdateMarriageSectionDataMutation,
+} from "@/hooks/marriage/use-section-data";
+import { convertSchemaToFrontendItems } from "@/lib/schema-adapter";
+import {
+ QuestionAnswersProvider,
+ useQuestionAnswers,
+} from "./question-answer-storage";
vi.mock("@/hooks/marriage/use-form-schema", () => ({
useFormSchemaQuery: vi.fn(),
@@ -14,6 +26,7 @@ vi.mock("@/hooks/marriage/use-profile-main", () => ({
useMarriageProfileQuery: vi.fn(),
}));
vi.mock("@/hooks/marriage/use-section-data", () => ({
+ applyProfilePatchResultToCache: vi.fn(),
useMarriageSectionDataQuery: vi.fn(),
useUpdateMarriageSectionDataMutation: vi.fn(),
}));
@@ -28,8 +41,22 @@ function TestComponent({ slug }: { slug: string }) {
data-testid="set-radio"
onClick={() =>
setAnswerValue(
- { id: "q1", type: "radio", title: "Q1", order: 1, required: true, baseRequired: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "", options: [], range: [0,0] }, options: [{ id: "opt1", value: "A", label: "Option A", order: 1 }] },
- "opt1"
+ {
+ id: "q1",
+ type: "radio",
+ title: "Q1",
+ order: 1,
+ required: true,
+ baseRequired: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "", options: [], range: [0, 0] },
+ options: [
+ { id: "opt1", value: "A", label: "Option A", order: 1 },
+ ],
+ },
+ "opt1",
)
}
>
@@ -40,8 +67,23 @@ function TestComponent({ slug }: { slug: string }) {
data-testid="set-checkbox"
onClick={() =>
setAnswerValue(
- { id: "q2", type: "checkbox", title: "Q2", order: 2, required: true, baseRequired: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "", options: [], range: [0,0] }, options: [{ id: "opt2", value: "B", label: "Option B", order: 1 }, { id: "opt3", value: "C", label: "Option C", order: 2 }] },
- ["opt2", "opt3"]
+ {
+ id: "q2",
+ type: "checkbox",
+ title: "Q2",
+ order: 2,
+ required: true,
+ baseRequired: true,
+ isVisible: true,
+ description: "",
+ tooltip: "",
+ extras: { placeHolder: "", options: [], range: [0, 0] },
+ options: [
+ { id: "opt2", value: "B", label: "Option B", order: 1 },
+ { id: "opt3", value: "C", label: "Option C", order: 2 },
+ ],
+ },
+ ["opt2", "opt3"],
)
}
>
@@ -87,19 +129,35 @@ describe("Question Answer & Schema Integration", () => {
});
it("should send option_id instead of label/value for radio", async () => {
- const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
render(