Skip to main content

Vue d’ensemble

Le système de quiz permet aux associations d’évaluer leur maturité numérique via un questionnaire interactif. Basé sur leurs réponses, le système génère des recommandations personnalisées d’outils et de packs adaptés à leurs besoins. Caractéristiques principales :
  • Questions à choix unique, multiple ou échelle
  • Logique conditionnelle pour recommandations ciblées
  • Parcours progressif avec barre de progression
  • Capture d’email optionnelle pour suivi
  • Tracking des réponses avec potentiel analytique
  • Interface publique + gestion admin complète

Architecture technique

Tables impliquées

Le système quiz utilise 5 tables PostgreSQL gérées via Hasura GraphQL : 1. quizzes — Définitions des quiz
interface Quiz {
  id: string;
  title: string;
  description: string | null;
  slug: string;         // Pour routing (ex: /quiz/maturite-numerique)
  is_active: boolean;
  created_at: string;
  updated_at: string;
}
2. quiz_questions — Questions du quiz
interface QuizQuestion {
  id: string;
  quiz_id: string;
  question_text: string;
  question_type: 'single' | 'multiple' | 'scale';
  order_index: number;
  is_required: boolean;
  help_text: string | null;
}
3. quiz_answers — Options de réponse
interface QuizAnswer {
  id: string;
  question_id: string;
  answer_text: string;
  answer_value: string;   // Valeur pour logique conditionnelle
  order_index: number;
}
4. quiz_recommendations — Règles de recommandation
interface QuizRecommendation {
  id: string;
  quiz_id: string;
  condition_logic: Record<string, any>;  // JSONB avec conditions
  recommended_pack_ids: string[];
  recommended_tool_ids: string[];
  recommendation_text: string | null;
  priority: number;
}
5. quiz_responses — Soumissions utilisateurs
interface QuizResponse {
  id: string;
  quiz_id: string;
  answers: Record<string, any>;   // JSONB des réponses
  recommended_pack_ids: string[];
  recommended_tool_ids: string[];
  email: string | null;
  created_at: string;
}

API Layer (GraphQL)

Fichier : src/api/quiz.ts
import { apiCall } from './base';
import { nhost } from './client';

// Récupérer un quiz actif par slug
const quiz = await quizApi.getBySlug('maturite-numerique');

// Soumettre une réponse (mutation GraphQL)
const response = await quizApi.submitResponse({
  quizId: quiz.id,
  answers: { q1: 'beginner', q2: ['email', 'social'] },
  email: 'contact@association.fr'  // Optionnel
});

// Récupérer les recommandations
const recommendations = await quizApi.getRecommendations(
  quiz.id,
  response.answers
);
Exemple de query GraphQL :
query GetQuizBySlug($slug: String!) {
  quizzes(where: { slug: { _eq: $slug }, is_active: { _eq: true } }) {
    id title description slug
    quiz_questions(order_by: { order_index: asc }) {
      id question_text question_type is_required help_text
      quiz_answers(order_by: { order_index: asc }) {
        id answer_text answer_value
      }
    }
  }
}

Logique conditionnelle

Structure de condition_logic

Le champ JSONB condition_logic dans quiz_recommendations permet de définir des règles complexes :
{
  "operator": "AND",
  "conditions": [
    { "question_id": "q1_maturity", "operator": "equals", "value": "beginner" },
    { "question_id": "q2_tools", "operator": "contains", "value": "email" }
  ]
}
Opérateurs supportés :
  • equals : Valeur exacte
  • contains : Contient la valeur (pour choix multiples)
  • greater_than / less_than : Pour échelles

Évaluation des conditions

Fichier : src/features/quiz/utils/evaluateConditions.ts
export function evaluateConditions(
  conditions: ConditionLogic,
  answers: Record<string, any>
): boolean {
  if (conditions.operator === 'AND') {
    return conditions.conditions.every(cond =>
      evaluateSingleCondition(cond, answers)
    );
  }
  if (conditions.operator === 'OR') {
    return conditions.conditions.some(cond =>
      evaluateSingleCondition(cond, answers)
    );
  }
  return false;
}

Composants UI

QuestionCard

// src/features/quiz/components/QuestionCard.tsx
export function QuestionCard({ question, onAnswer }) {
  const [selected, setSelected] = useState(
    question.question_type === 'multiple' ? [] : ''
  );

  return (
    <div className="bg-white p-8 rounded-xl shadow-md">
      <h2 className="text-2xl font-bold mb-2">{question.question_text}</h2>
      {question.help_text && (
        <p className="text-gray-600 mb-6">{question.help_text}</p>
      )}
      <div className="space-y-3">
        {question.answers.map(answer => (
          <AnswerOption key={answer.id} answer={answer} type={question.question_type}
            selected={selected} onSelect={setSelected} />
        ))}
      </div>
      <button
        onClick={() => onAnswer(selected)}
        disabled={!selected || (Array.isArray(selected) && selected.length === 0)}
        className="mt-6 w-full py-3 bg-gradient-to-r from-indigo-600 to-blue-500
                   text-white font-semibold rounded-lg disabled:opacity-50"
      >
        Suivant
      </button>
    </div>
  );
}

QuizResults

// src/features/quiz/components/QuizResults.tsx
export function QuizResults({ results }) {
  return (
    <div className="space-y-8">
      {results.recommendation_text && (
        <div className="bg-blue-50 border-l-4 border-blue-500 p-6 rounded-r-lg">
          <p className="text-gray-700">{results.recommendation_text}</p>
        </div>
      )}
      {results.recommended_packs.length > 0 && (
        <section>
          <h2 className="text-2xl font-bold mb-4">Packs recommandés</h2>
          <div className="grid md:grid-cols-2 gap-4">
            {results.recommended_packs.map(pack => (
              <PackCard key={pack.id} pack={pack} />
            ))}
          </div>
        </section>
      )}
    </div>
  );
}

Permissions Hasura

  • public : SELECT sur quizzes (actifs), quiz_questions, quiz_answers
  • public : INSERT sur quiz_responses (soumission des réponses)
  • admin : CRUD complet sur toutes les tables quiz

Bonnes pratiques

✅ À faire

  • Valider que toutes les questions requises ont une réponse avant soumission
  • Trier les recommandations par priorité
  • Garder les réponses dans le state React (pas localStorage)
  • Charger uniquement le quiz demandé par slug (pas tous les quiz)

❌ À éviter

  • Stocker les réponses dans localStorage (perte si navigateur fermé)
  • Charger tous les quiz en même temps
  • Oublier la validation côté serveur (Hasura permissions)

Ressources

Admin Dashboard

Gestion complète des quiz

Database Schema

Structure des tables quiz

API Layer

Utilisation de l’API GraphQL

Tool Packs

Packs recommandés dans les résultats