Terminado sprint 2A
This commit is contained in:
53
lib/conditional.ts
Normal file
53
lib/conditional.ts
Normal 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
27
lib/storage.ts
Normal 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
17
lib/supabase.ts
Normal 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
52
lib/types.ts
Normal 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>
|
||||
Reference in New Issue
Block a user