Terminado sprint 2A

This commit is contained in:
markosbenitez
2026-06-02 10:27:52 -03:00
parent 418c91fb12
commit 0f376a8b75
81 changed files with 5379 additions and 46 deletions

53
lib/conditional.ts Normal file
View File

@@ -0,0 +1,53 @@
import type { ConditionalRule, FormAnswers, Question } from './types'
/** Returns true if the question should be shown given the current answers. */
export function isQuestionVisible(
question: Question,
answers: FormAnswers
): boolean {
const rules = question.conditional_rules
if (!rules || rules.length === 0) return true
// All rules must pass (AND semantics)
return rules.every((rule) => evaluateRule(rule, answers))
}
function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean {
if (rule.op === 'eq') {
return answers[rule.if_question] === rule.value
}
return false
}
/**
* Returns the codes of questions that will become hidden when `triggerCode`
* changes to `newValue`. Used to clear those answers reactively.
*/
export function getHiddenCodes(
triggerCode: string,
newValue: string | string[] | number | null,
questions: Question[],
currentAnswers: FormAnswers
): string[] {
const projected = { ...currentAnswers, [triggerCode]: newValue }
return questions
.filter((q) => {
if (!q.conditional_rules?.length) return false
const dependsOnTrigger = q.conditional_rules.some(
(r) => r.if_question === triggerCode
)
if (!dependsOnTrigger) return false
return !isQuestionVisible(q, projected)
})
.map((q) => q.code)
}
/** All trigger codes in the survey (questions that have dependents). */
export function getTriggerCodes(questions: Question[]): Set<string> {
const triggers = new Set<string>()
for (const q of questions) {
for (const rule of q.conditional_rules ?? []) {
triggers.add(rule.if_question)
}
}
return triggers
}

27
lib/storage.ts Normal file
View File

@@ -0,0 +1,27 @@
import type { FormAnswers } from './types'
const PREFIX = 'survey_draft_'
// Only store answer data — nothing identifiable about the respondent.
export function saveDraft(surveyId: string, answers: FormAnswers): void {
try {
localStorage.setItem(PREFIX + surveyId, JSON.stringify(answers))
} catch {
// Storage quota or private-mode — silently ignore
}
}
export function loadDraft(surveyId: string): FormAnswers {
try {
const raw = localStorage.getItem(PREFIX + surveyId)
return raw ? (JSON.parse(raw) as FormAnswers) : {}
} catch {
return {}
}
}
export function clearDraft(surveyId: string): void {
try {
localStorage.removeItem(PREFIX + surveyId)
} catch {}
}

17
lib/supabase.ts Normal file
View File

@@ -0,0 +1,17 @@
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
// Browser-side client using the anon key (public, respects RLS)
// NEVER put SUPABASE_SERVICE_ROLE_KEY in a NEXT_PUBLIC_ variable or in this file.
export function createClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (!url || !anonKey) {
throw new Error(
'Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY. ' +
'Copy .env.example to .env.local and fill in the values from your Supabase project.'
)
}
return createSupabaseClient(url, anonKey)
}

52
lib/types.ts Normal file
View File

@@ -0,0 +1,52 @@
export type QuestionType =
| 'single_choice'
| 'multiple_choice'
| 'rating'
| 'text_long'
| 'text_short'
| 'section'
export interface QuestionOption {
value: string
label: string
}
export interface RatingOptions {
min: number
max: number
min_label: string
max_label: string
}
export interface ConditionalRule {
if_question: string
op: 'eq'
value: string
action: 'show'
}
export interface Question {
id: string
survey_id: string
code: string
type: QuestionType
prompt: string
position: number
is_sensitive: boolean
required: boolean
max_select: number | null
options: QuestionOption[] | RatingOptions | null
conditional_rules: ConditionalRule[] | null
}
export interface Survey {
id: string
title: string
status: string
is_anonymous: boolean
k_threshold: number
questions: Question[]
}
// Answers keyed by question code
export type FormAnswers = Record<string, string | string[] | number | null>