Skip to main content

Vue d’ensemble

Les Tool Packs sont des collections curées d’outils regroupés par thème ou cas d’usage. Ils simplifient le choix en proposant des ensembles cohérents et testés. Objectif : Éviter la paralysie du choix en proposant des sélections prêtes à l’emploi. Exemples de packs :
  • Starter Pack (essentiels pour débuter)
  • Communication & Email
  • Automatisation
  • Paiements & Dons
  • Sites Web

Architecture technique

Tables de base de données

Table tool_packs

CREATE TABLE tool_packs (
  id UUID PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT NOT NULL,
  difficulty TEXT,
  icon TEXT DEFAULT 'Package',
  color TEXT DEFAULT 'blue',
  display_order INTEGER DEFAULT 0,
  status TEXT CHECK (status IN ('active', 'draft')) DEFAULT 'active',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Table pack_tools (Jointure Many-to-Many)

CREATE TABLE pack_tools (
  id UUID PRIMARY KEY,
  pack_id UUID REFERENCES tool_packs(id) ON DELETE CASCADE,
  tool_id UUID REFERENCES tools(id) ON DELETE CASCADE,
  display_order INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(pack_id, tool_id)
);

API Layer (GraphQL)

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

export const packsApi = {
  /**
   * Liste tous les packs actifs avec nombre d'outils
   */
  async list(): Promise<Array<ToolPack & { tool_count: number }>> {
    const query = `
      query GetPacks {
        tool_packs(
          where: { status: { _eq: "active" } }
          order_by: { display_order: asc }
        ) {
          id title description difficulty icon color
          display_order status
          pack_tools_aggregate { aggregate { count } }
        }
      }
    `;
    return apiCall(
      () => nhost.graphql.request({ query }),
      'packs.list'
    );
  },

  /**
   * Récupère un pack avec ses outils enrichis
   */
  async getById(id: string): Promise<ToolPack & { tools: EnhancedTool[] }> {
    const query = `
      query GetPack($id: uuid!) {
        tool_packs_by_pk(id: $id) {
          id title description difficulty icon color
          pack_tools(order_by: { display_order: asc }) {
            display_order
            tool {
              id name description pricing_tier logo_url website_url
              category { name }
            }
          }
        }
      }
    `;
    return apiCall(
      () => nhost.graphql.request({ query, variables: { id } }),
      'packs.getById'
    );
  },

  /**
   * Ajoute un outil au pack
   */
  async addTool(packId: string, toolId: string, displayOrder?: number): Promise<void> {
    const mutation = `
      mutation AddPackTool($object: pack_tools_insert_input!) {
        insert_pack_tools_one(object: $object) { id }
      }
    `;
    await apiCallVoid(
      () => nhost.graphql.request({
        query: mutation,
        variables: { object: { pack_id: packId, tool_id: toolId, display_order: displayOrder || 0 } }
      }),
      'packs.addTool'
    );
  },

  /**
   * Retire un outil du pack
   */
  async removeTool(packId: string, toolId: string): Promise<void> {
    const mutation = `
      mutation RemovePackTool($packId: uuid!, $toolId: uuid!) {
        delete_pack_tools(where: {
          pack_id: { _eq: $packId },
          tool_id: { _eq: $toolId }
        }) { affected_rows }
      }
    `;
    await apiCallVoid(
      () => nhost.graphql.request({
        query: mutation,
        variables: { packId, toolId }
      }),
      'packs.removeTool'
    );
  },
};

Structure d’un Tool Pack

Métadonnées

interface ToolPack {
  id: string;
  title: string;           // "Starter Pack"
  description: string;     // "Essentiels pour débuter"
  difficulty?: string;     // "débutant", "intermédiaire", "expert"
  icon: string;            // "Rocket" (nom d'icône Lucide)
  color: string;           // "blue", "green", "orange", etc.
  display_order: number;
  status: 'active' | 'draft';
}

Composants UI

PackCard

// src/components/toolpacks/PackCard.tsx
import { getIconComponent } from '@/utils/icons';

export function PackCard({ pack, onClick }: PackCardProps) {
  const Icon = getIconComponent(pack.icon);

  const colorClasses = {
    blue: 'bg-blue-100 text-blue-600',
    green: 'bg-green-100 text-green-600',
    orange: 'bg-orange-100 text-orange-600',
    purple: 'bg-purple-100 text-purple-600',
    red: 'bg-red-100 text-red-600',
  };

  return (
    <div onClick={onClick} className="p-6 border rounded-lg hover:shadow-lg transition cursor-pointer">
      <div className={`inline-flex p-3 rounded-lg mb-4 ${colorClasses[pack.color]}`}>
        <Icon className="w-6 h-6" />
      </div>
      <h3 className="font-semibold text-lg mb-2">{pack.title}</h3>
      <p className="text-gray-600 text-sm mb-4">{pack.description}</p>
      <span className="text-gray-500 text-sm">
        {pack.tool_count} outil{pack.tool_count > 1 ? 's' : ''}
      </span>
    </div>
  );
}

Permissions Hasura

  • public : SELECT des packs actifs et de leurs pack_tools
  • admin : CRUD complet sur tool_packs et pack_tools

Bonnes pratiques

✅ À faire

  • 3-7 outils par pack (ni trop, ni trop peu)
  • Cohérence thématique forte
  • Outils testés et approuvés
  • Mix de pricing (gratuit + freemium)
  • Starter Pack en premier dans l’affichage

❌ À éviter

  • Packs trop larges (“Tous les outils”)
  • Doublons entre packs
  • Outils obsolètes ou fermés
  • Descriptions vagues

Ressources

Database Schema

Structure des tables tool_packs

API Layer

Utiliser l’API GraphQL

Admin Dashboard

Créer et gérer des packs

Quiz Feature

Recommandations via quiz