120 lines · 3.8 KB · TypeScript
RawDownload
import type { UserInputQuestion } from "@t3tools/contracts"; export interface PendingUserInputDraftAnswer {  selectedOptionLabel?: string;  customAnswer?: string;} export interface PendingUserInputProgress {  questionIndex: number;  activeQuestion: UserInputQuestion | null;  activeDraft: PendingUserInputDraftAnswer | undefined;  selectedOptionLabel: string | undefined;  customAnswer: string;  resolvedAnswer: string | null;  usingCustomAnswer: boolean;  answeredQuestionCount: number;  isLastQuestion: boolean;  isComplete: boolean;  canAdvance: boolean;} function normalizeDraftAnswer(value: string | undefined): string | null {  if (typeof value !== "string") {    return null;  }   const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : null;} export function resolvePendingUserInputAnswer(  draft: PendingUserInputDraftAnswer | undefined,): string | null {  const customAnswer = normalizeDraftAnswer(draft?.customAnswer);  if (customAnswer) {    return customAnswer;  }   return normalizeDraftAnswer(draft?.selectedOptionLabel);} export function setPendingUserInputCustomAnswer(  draft: PendingUserInputDraftAnswer | undefined,  customAnswer: string,): PendingUserInputDraftAnswer {  const selectedOptionLabel =    customAnswer.trim().length > 0 ? undefined : draft?.selectedOptionLabel;   return {    customAnswer,    ...(selectedOptionLabel ? { selectedOptionLabel } : {}),  };} export function buildPendingUserInputAnswers(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): Record<string, string> | null {  const answers: Record<string, string> = {};   for (const question of questions) {    const answer = resolvePendingUserInputAnswer(draftAnswers[question.id]);    if (!answer) {      return null;    }    answers[question.id] = answer;  }   return answers;} export function countAnsweredPendingUserInputQuestions(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): number {  return questions.reduce((count, question) => {    return resolvePendingUserInputAnswer(draftAnswers[question.id]) ? count + 1 : count;  }, 0);} export function findFirstUnansweredPendingUserInputQuestionIndex(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): number {  const unansweredIndex = questions.findIndex(    (question) => !resolvePendingUserInputAnswer(draftAnswers[question.id]),  );   return unansweredIndex === -1 ? Math.max(questions.length - 1, 0) : unansweredIndex;} export function derivePendingUserInputProgress(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,  questionIndex: number,): PendingUserInputProgress {  const normalizedQuestionIndex =    questions.length === 0 ? 0 : Math.max(0, Math.min(questionIndex, questions.length - 1));  const activeQuestion = questions[normalizedQuestionIndex] ?? null;  const activeDraft = activeQuestion ? draftAnswers[activeQuestion.id] : undefined;  const resolvedAnswer = resolvePendingUserInputAnswer(activeDraft);  const customAnswer = activeDraft?.customAnswer ?? "";  const answeredQuestionCount = countAnsweredPendingUserInputQuestions(questions, draftAnswers);  const isLastQuestion =    questions.length === 0 ? true : normalizedQuestionIndex >= questions.length - 1;   return {    questionIndex: normalizedQuestionIndex,    activeQuestion,    activeDraft,    selectedOptionLabel: activeDraft?.selectedOptionLabel,    customAnswer,    resolvedAnswer,    usingCustomAnswer: customAnswer.trim().length > 0,    answeredQuestionCount,    isLastQuestion,    isComplete: buildPendingUserInputAnswers(questions, draftAnswers) !== null,    canAdvance: Boolean(resolvedAnswer),  };}