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
-
22src/components/Componentes/question-exit-navigation-button.tsx
-
222src/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
-
14src/components/Componentes/token-switcher.tsx
-
128src/hooks/marriage/use-form-schema.ts
-
5src/hooks/marriage/use-profile-main.ts
-
142src/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