Browse Source
feat: implement interactive questions-list page with local progress tracking and sync-before-submission logic
front-test-2
feat: implement interactive questions-list page with local progress tracking and sync-before-submission logic
front-test-2
18 changed files with 672 additions and 416 deletions
-
132src/app/questions-list/[slug]/answer-pace-sheet.tsx
-
9src/app/questions-list/[slug]/question-detail-client.tsx
-
12src/app/questions-list/page.tsx
-
18src/app/questions-list/sections-request.tsx
-
18src/components/Componentes/progress-helper.ts
-
104src/components/Componentes/question-answer-storage.tsx
-
20src/components/Componentes/question-exit-navigation-button.tsx
-
150src/components/Componentes/question-phone.tsx
-
151src/components/Componentes/question-section-flow.tsx
-
28src/components/Componentes/question-snap-list.tsx
-
13src/components/Componentes/silent-reloader.tsx
-
10src/components/Componentes/token-switcher.tsx
-
128src/hooks/marriage/use-form-schema.ts
-
5src/hooks/marriage/use-profile-main.ts
-
140src/hooks/marriage/use-section-data.ts
-
36src/hooks/marriage/use-sections.ts
-
23src/lib/first-entry-helper.ts
-
11src/lib/get-submit-path.ts
@ -0,0 +1,128 @@ |
|||||
|
"use client"; |
||||
|
|
||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
||||
|
import { http } from "@/lib/http"; |
||||
|
import type { MutationOptions, QueryOptions } from "./options"; |
||||
|
import { marriageQueryKeys } from "./query-keys"; |
||||
|
|
||||
|
export interface FormOption { |
||||
|
id: string; |
||||
|
value: string; |
||||
|
label: string; |
||||
|
} |
||||
|
|
||||
|
export interface FormQuestion { |
||||
|
id: string; |
||||
|
type: string; |
||||
|
title: string; |
||||
|
description: string; |
||||
|
tooltip: string; |
||||
|
placeholder: string; |
||||
|
required: boolean; |
||||
|
show_guardian_notice: boolean; |
||||
|
validation: Record<string, any>; |
||||
|
ui_config: Record<string, any>; |
||||
|
logic: Record<string, any> | null; |
||||
|
is_visible: boolean; |
||||
|
options: FormOption[]; |
||||
|
} |
||||
|
|
||||
|
export interface FormCard { |
||||
|
id: string; |
||||
|
title: string; |
||||
|
questions: FormQuestion[]; |
||||
|
} |
||||
|
|
||||
|
export interface FormSection { |
||||
|
id: string; |
||||
|
title: string; |
||||
|
icon: string; |
||||
|
is_required: boolean; |
||||
|
estimated_minutes: number; |
||||
|
cards: FormCard[]; |
||||
|
} |
||||
|
|
||||
|
export interface FormProgressInfo { |
||||
|
current_step: number; |
||||
|
total_steps: number; |
||||
|
completion_percent: number; |
||||
|
} |
||||
|
|
||||
|
export interface FormSchemaResponse { |
||||
|
form_id: string; |
||||
|
version: number; |
||||
|
sections: FormSection[]; |
||||
|
answers: Record<string, { value: any; option_id: string }>; |
||||
|
progress: { |
||||
|
current_step: number; |
||||
|
total_steps: number; |
||||
|
completion_percent: number; |
||||
|
sections_progress: Record<string, FormProgressInfo>; |
||||
|
}; |
||||
|
is_completed?: boolean; |
||||
|
} |
||||
|
|
||||
|
export async function getFormSchema(formId: string, locale: string): Promise<FormSchemaResponse> { |
||||
|
const { data } = await http.get<FormSchemaResponse>( |
||||
|
`/api/marriage/forms/${formId}/?lang=${locale}` |
||||
|
); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
export function useFormSchemaQuery<TData = FormSchemaResponse>( |
||||
|
formId: string, |
||||
|
locale: string, |
||||
|
options?: QueryOptions<FormSchemaResponse, TData> |
||||
|
) { |
||||
|
return useQuery({ |
||||
|
staleTime: 0, |
||||
|
refetchOnMount: "always", |
||||
|
refetchOnWindowFocus: true, |
||||
|
...options, |
||||
|
queryFn: () => getFormSchema(formId, locale), |
||||
|
queryKey: ["marriage", "form-schema", formId, locale], |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export interface SaveAnswersPayload { |
||||
|
version: number; |
||||
|
answers: Array<{ |
||||
|
question_id: string; |
||||
|
value: any; |
||||
|
option_id?: string; |
||||
|
}>; |
||||
|
} |
||||
|
|
||||
|
export async function saveFormAnswers(formId: string, payload: SaveAnswersPayload): Promise<FormSchemaResponse> { |
||||
|
const { data } = await http.put<FormSchemaResponse>( |
||||
|
`/api/marriage/forms/${formId}/answers/`, |
||||
|
payload |
||||
|
); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
export function useSaveFormAnswersMutation( |
||||
|
formId: string, |
||||
|
options?: MutationOptions<FormSchemaResponse, SaveAnswersPayload> |
||||
|
) { |
||||
|
const queryClient = useQueryClient(); |
||||
|
|
||||
|
return useMutation({ |
||||
|
...options, |
||||
|
mutationFn: (payload) => saveFormAnswers(formId, payload), |
||||
|
onSuccess: async (data, variables, onMutateResult, context) => { |
||||
|
await Promise.all([ |
||||
|
queryClient.invalidateQueries({ |
||||
|
queryKey: ["marriage", "form-schema", formId], |
||||
|
}), |
||||
|
queryClient.invalidateQueries({ |
||||
|
queryKey: marriageQueryKeys.profile(), |
||||
|
}), |
||||
|
queryClient.invalidateQueries({ |
||||
|
queryKey: marriageQueryKeys.sections(), |
||||
|
}), |
||||
|
]); |
||||
|
await options?.onSuccess?.(data, variables, onMutateResult, context); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
const FIRST_ENTRY_COMPLETED_KEY = "marriage:first-entry-completed"; |
||||
|
|
||||
|
export function isFirstEntryCompleted(): boolean { |
||||
|
if (typeof window === "undefined") { |
||||
|
return false; |
||||
|
} |
||||
|
try { |
||||
|
return window.localStorage.getItem(FIRST_ENTRY_COMPLETED_KEY) === "true"; |
||||
|
} catch { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function markFirstEntryCompleted(): void { |
||||
|
if (typeof window === "undefined") { |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
window.localStorage.setItem(FIRST_ENTRY_COMPLETED_KEY, "true"); |
||||
|
} catch { |
||||
|
// ignore
|
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue