Skip to main content

Vue d’ensemble

La feature Tools est le cœur de Kit’Asso : un catalogue d’outils numériques adaptés aux besoins des associations. Statistiques :
  • 35+ outils référencés
  • 4 tiers de pricing (Gratuit, Freemium, Payant, Entreprise)
  • Système de catégorisation par domaine
  • Comparaison côte à côte (max 2 outils)
  • Favoris avec localStorage

Architecture technique

Composants principaux

src/features/tools/
├── modals/
│   └── ToolModal/
│       ├── index.tsx           # Container principal
│       ├── ToolForm.tsx        # Formulaire d'édition
│       ├── ToolPreview.tsx     # Aperçu détaillé
│       ├── types.ts            # Types du modal
│       └── useToolModal.ts     # Logique métier

src/components/sections/
├── VirtualizedToolsGrid.tsx    # Grille virtualisée (react-window)
└── ToolCard.tsx                # Carte outil individuelle

API Layer (GraphQL)

Toutes les opérations passent par apiCall() / apiCallVoid() + nhost.graphql.request() :
// src/api/tools.ts
export const toolsApi = {
  list: () => Promise<EnhancedTool[]>,
  getById: (id: string) => Promise<EnhancedTool>,
  create: (data: ToolInsert) => Promise<Tool>,
  update: (id: string, data: ToolUpdate) => Promise<Tool>,
  delete: (id: string) => Promise<void>,
  uploadLogo: (file: File) => Promise<string>
};
Query GraphQL type :
query GetTools {
  tools(order_by: { name: asc }) {
    id name description pricing_tier
    category_id logo_url website_url
    category { name }
    tool_features {
      filter { id value filter_type }
    }
  }
}

Fonctionnalités détaillées

1. Recherche avancée

Implémentation : src/hooks/useToolFilters.ts
const {
  filteredTools,
  searchTerm,
  setSearchTerm,
  selectedPricing,
  setSelectedPricing,
  selectedCategory,
  setSelectedCategory
} = useToolFilters(tools);
Caractéristiques :
  • Debounce de 300ms pour la recherche texte
  • Filtrage multi-critères (pricing + catégorie)
  • Combinaison ET des filtres
  • Performance optimisée avec useMemo

2. Grille virtualisée

Composant : VirtualizedToolsGrid.tsx Utilise react-window pour le rendu performant. Seuls les éléments visibles dans le viewport sont rendus. Configuration :
<FixedSizeGrid
  columnCount={getColumnCount()} // 1-4 selon viewport
  rowCount={Math.ceil(tools.length / columnCount)}
  columnWidth={320}
  rowHeight={380}
  height={800}
  width={containerWidth}
>
  {({ columnIndex, rowIndex, style }) => (
    <ToolCard tool={tools[index]} style={style} />
  )}
</FixedSizeGrid>

3. Mode comparaison

Route : /compare Logique : src/hooks/useToolComparison.ts
  • Sélection max : 2 outils
  • Affichage côte à côte des caractéristiques (pricing, catégorie, features, description, liens)

4. Système de favoris

Hook : src/hooks/useFavorites.ts Stockage : LocalStorage (clé : kitasso_favorites)
const {
  favorites,      // Set<string> des IDs
  addFavorite,    // (id: string) => void
  removeFavorite, // (id: string) => void
  toggleFavorite, // (id: string) => void
  isFavorite,     // (id: string) => boolean
  count           // number
} = useFavorites();

5. Upload de logos

Backend : Nhost Storage
// Upload vers Nhost Storage
const { error, fileMetadata } = await nhost.storage.upload({
  file,
  bucketId: 'default',
});

// URL publique
const publicUrl = `https://${nhostSubdomain}.storage.${nhostRegion}.nhost.run/v1/files/${fileMetadata.id}`;

Base de données

Table tools

CREATE TABLE tools (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT UNIQUE NOT NULL,
  description TEXT NOT NULL,
  pricing_tier TEXT CHECK (pricing_tier IN (
    'Gratuit', 'Freemium', 'Payant', 'Entreprise'
  )),
  category_id UUID REFERENCES categories(id) ON DELETE SET NULL,
  logo_url TEXT,
  website_url TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Relations via GraphQL

Catégorie (Many-to-One) :
query { tools { id name category { name } } }
Features (Many-to-Many via tool_features) :
query { tools { id name tool_features { filter { id value } } } }

Permissions Hasura

  • public : SELECT autorisé sur toutes les lignes
  • admin : SELECT, INSERT, UPDATE, DELETE autorisés

Types TypeScript

// src/api/types.ts
export interface Tool {
  id: string;
  name: string;
  description: string;
  pricing_tier: 'Gratuit' | 'Freemium' | 'Payant' | 'Entreprise';
  category_id: string;
  logo_url: string | null;
  website_url: string | null;
  created_at: string;
}

export interface EnhancedTool extends Tool {
  category_name: string;
  features: string[];
}

export interface ToolInsert {
  name: string;
  description: string;
  pricing_tier: Tool['pricing_tier'];
  category_id: string;
  logo_url?: string;
  website_url?: string;
}

export type ToolUpdate = Partial<ToolInsert>;

Bonnes pratiques

✅ À faire

// Toujours passer par l'API layer
import { toolsApi } from '@/api';
const tools = await toolsApi.list();

// Gérer les erreurs
try {
  await toolsApi.create(data);
} catch (error) {
  if (error instanceof ApiError) {
    // Erreur typée avec message
  }
}

❌ À éviter

// NE PAS appeler nhost.graphql directement dans les composants
const result = await nhost.graphql.request({ query: '...' });

// NE PAS ignorer les erreurs
await toolsApi.create(data); // Sans try/catch

Performance

Métriques cibles :
  • Temps de chargement initial : < 2s
  • Temps de recherche : < 100ms
  • FPS scroll : 60
  • Mémoire utilisée : < 50MB
Optimisations :
  • Virtualisation avec react-window
  • Debounce sur recherche (300ms)
  • Memoization avec React.memo
  • Lazy loading des images
  • Code splitting du modal admin

Ressources

Workflows

Parcours guidés étape par étape

Tool Packs

Collections curées d’outils

API Layer

Architecture API GraphQL

Testing

Écrire des tests pour vos features