464 lines
19 KiB
Markdown
464 lines
19 KiB
Markdown
|
|
# Rapport d'Exécution Technique — Veza v1.0
|
|||
|
|
|
|||
|
|
**Date** : 2025-02-10
|
|||
|
|
**Équipe** : Lead Frontend Architect (ex-Spotify), Design Engineer (ex-Discord), Performance Expert
|
|||
|
|
**Objectif** : Valider, affiner et prioriser l'exécution des points de l'audit exhaustif pour atteindre une qualité "Premium Audio Platform".
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Verdict Technique Immédiat — Fichiers Critiques
|
|||
|
|
|
|||
|
|
### 1.1 `client.ts` (2238 lignes)
|
|||
|
|
|
|||
|
|
| Critère | Verdict | Détail |
|
|||
|
|
|---------|---------|--------|
|
|||
|
|
| **Maintenabilité** | ⛔ Critique | Fichier monolithique. 5 classes, 2 interceptors, retry, refresh, validation, cache, offline, déduplication. Impossibilité de tester unitairement des parties isolées. |
|
|||
|
|
| **Architecture** | ⚠️ Acceptable | Logique correcte (retry, refresh, CSRF, validation Zod). Mais tout dans un seul fichier. |
|
|||
|
|
| **Risque** | Moyen | Refactoring sans changer les types exportés possible. Dépendances circulaires à éviter. |
|
|||
|
|
|
|||
|
|
**Pourquoi ça fait amateur :** Un fichier de 2200+ lignes suggère un manque de discipline architecturale. Les géants (Spotify, Discord) découpent : interceptors dans des modules, retry dans un utilitaire, refresh dans un service auth dédié.
|
|||
|
|
|
|||
|
|
**Comment font les géants :** Architecture en couches. `apiClient` = instance axios + composition d'interceptors. Chaque interceptor est un fichier < 150 lignes. Les helpers (retry, sanitize) sont des modules testables.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.2 `AuthContext.tsx` (108 lignes)
|
|||
|
|
|
|||
|
|
| Critère | Verdict | Détail |
|
|||
|
|
|---------|---------|--------|
|
|||
|
|
| **Rôle** | Redondant | Duplique authStore + useUser. `authService` utilisé directement (authStore utilise `api/auth`). |
|
|||
|
|
| **Usage app** | ❌ Aucun | App ne wrap pas avec `AuthContext.Provider`. Utilisé uniquement dans **Storybook** (decorators). |
|
|||
|
|
| **Conflit** | Oui | 3 fichiers app utilisent `useAuth` de AuthContext : `ProfileView`, `useAuthView`, `useEditProfile`. Ces composants **crashent** si rendus hors Storybook (pas de Provider). |
|
|||
|
|
|
|||
|
|
**Bug confirmé :** `EditProfile` (Settings > Profile) utilise `useEditProfile` → `useAuth` (AuthContext). L'app n'a pas `AuthContext.Provider`. Donc **Settings > Profile tab = crash** en production.
|
|||
|
|
|
|||
|
|
**Pourquoi ça fait amateur :** Double source de vérité. Storybook fonctionne (AuthProvider), l'app non. Composants qui marchent en Storybook mais crashent en prod.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.3 `authStore.ts` (328 lignes)
|
|||
|
|
|
|||
|
|
| Critère | Verdict | Détail |
|
|||
|
|
|---------|---------|--------|
|
|||
|
|
| **Design** | ✅ Solide | Zustand + persist + broadcastSync. Pas de `user` (délégué à React Query via `useUser`). |
|
|||
|
|
| **API** | Propre | `login`, `register`, `logout`, `refreshUser`, `logoutLocal`. Types explicites. |
|
|||
|
|
| **État** | Source principale | Utilisé par App, Header, Sidebar, LoginForm, ProtectedRoute, client.ts (401 → logoutLocal). |
|
|||
|
|
|
|||
|
|
**Points forts :** Gestion httpOnly cookies, refresh avec préservation d'état sur erreurs réseau, sync cross-tabs.
|
|||
|
|
|
|||
|
|
**Pourquoi c'est bien :** Une seule source pour `isAuthenticated`, `isLoading`, `error`. User via React Query (cache, déduplication).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 1.4 `GlobalPlayer.tsx` (301 lignes)
|
|||
|
|
|
|||
|
|
| Critère | Verdict | Détail |
|
|||
|
|
|---------|---------|--------|
|
|||
|
|
| **UI** | ✅ Correcte | Barre glass, gradients, layout responsive. |
|
|||
|
|
| **Waveform** | ❌ Décoratif | `Waveform` utilise `Math.random()` pour hauteurs. Pas de lecture audio. Animation `eq-bounce` décorative. |
|
|||
|
|
| **Problèmes** | Variés | `z-[100]` arbitraire. `translate-y-[200%]` arbitraire. Waveform visible uniquement sur `xl`. |
|
|||
|
|
|
|||
|
|
**Pourquoi ça fait amateur :** Spotify/SoundCloud : waveform = visualisation réelle du signal audio. Veza : barres qui bougent au hasard, indépendantes du son.
|
|||
|
|
|
|||
|
|
**Comment font les géants :** Web Audio API (AnalyserNode) ou Wavesurfer.js pour extraire les données audio et dessiner le waveform. Le seek est possible en cliquant sur le waveform.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Analyse de la Dette Structurelle (Deep Dive)
|
|||
|
|
|
|||
|
|
### 2.1 Auth Dual-Source — Zones de conflit
|
|||
|
|
|
|||
|
|
| Fichier | Import | Comportement |
|
|||
|
|
|---------|--------|--------------|
|
|||
|
|
| `ProtectedRoute.tsx` | `useAuth` (features), `useAuthStore` | OK — useAuth = useAuthStore + useUser |
|
|||
|
|
| `PublicRoute.tsx` | Idem | OK |
|
|||
|
|
| `ProfileView.tsx` | `useAuth` (context) | ❌ Crashe hors Storybook |
|
|||
|
|
| `useAuthView.ts` | `useAuth` (context) | ❌ AuthView utilisé en Storybook seulement |
|
|||
|
|
| `useEditProfile.ts` | `useAuth` (context) | ❌ **EditProfile utilisé dans Settings → CRASH** |
|
|||
|
|
|
|||
|
|
**Plan de migration — Fichier par fichier :**
|
|||
|
|
|
|||
|
|
| Ordre | Fichier | Action |
|
|||
|
|
|-------|---------|--------|
|
|||
|
|
| 1 | `useEditProfile.ts` | Remplacer `import { useAuth } from '@/context/AuthContext'` par `import { useUser } from '@/features/auth/hooks/useUser'`. Utiliser `const { data: user } = useUser()`. |
|
|||
|
|
| 2 | `ProfileView.tsx` | Idem. `const { data: currentUser } = useUser()` à la place de `useAuth()`. |
|
|||
|
|
| 3 | `useAuthView.ts` | `useAuth()` appelé sans destructuring (juste pour vérifier contexte). Remplacer par `useAuthStore()` + `useUser()` si besoin, ou supprimer l'appel si inutile. |
|
|||
|
|
| 4 | `StorybookDecorator` | Remplacer `AuthProvider` (context) par un mock `useAuthStore.setState` ou laisser QueryClient + MSW (useUser fonctionne avec MSW). |
|
|||
|
|
| 5 | `AuthContext.tsx` | Supprimer le fichier. |
|
|||
|
|
| 6 | `AuthContext.test.tsx` | Supprimer ou migrer vers tests authStore. |
|
|||
|
|
|
|||
|
|
**Diff conceptuel pour useEditProfile :**
|
|||
|
|
|
|||
|
|
```diff
|
|||
|
|
- import { useAuth } from '@/context/AuthContext';
|
|||
|
|
+ import { useUser } from '@/features/auth/hooks/useUser';
|
|||
|
|
|
|||
|
|
export function useEditProfile() {
|
|||
|
|
- const { user } = useAuth();
|
|||
|
|
+ const { data: user } = useUser();
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2.2 Client API Monolithique — Schéma de découpage
|
|||
|
|
|
|||
|
|
**Structure proposée :**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
services/api/
|
|||
|
|
├── client.ts # Instance axios + attachement interceptors (150 lignes max)
|
|||
|
|
├── clientConfig.ts # API_TIMEOUTS, baseURL, création instance
|
|||
|
|
├── interceptors/
|
|||
|
|
│ ├── request.ts # CSRF, X-API-Version, validation request (120 lignes)
|
|||
|
|
│ ├── response.ts # Unwrap, validation response, cache, rate limit (200 lignes)
|
|||
|
|
│ └── error.ts # Retry, refresh, 401, toast, offline (250 lignes)
|
|||
|
|
├── lib/
|
|||
|
|
│ ├── validationMetrics.ts # ValidationMetricsTracker, ValidationAlerting
|
|||
|
|
│ ├── networkFailureTracker.ts
|
|||
|
|
│ ├── retry.ts # isRetryableError, getRetryDelay, exponential backoff
|
|||
|
|
│ └── sanitize.ts # sanitizeForLogging, getRequestId
|
|||
|
|
├── auth/
|
|||
|
|
│ ├── refreshQueue.ts # failedQueue, processQueue, isRefreshing
|
|||
|
|
│ └── index.ts
|
|||
|
|
└── index.ts # Re-exports apiClient, deduplicatedApiClient
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Garantie types :** `apiClient` reste exporté avec la même signature. Les interceptors sont attachés dans `client.ts`. Les types générés (`ApiResponse`, etc.) ne changent pas.
|
|||
|
|
|
|||
|
|
**Ordre de migration :**
|
|||
|
|
1. Extraire `ValidationMetricsTracker` + `ValidationAlerting` → `lib/validationMetrics.ts`
|
|||
|
|
2. Extraire `NetworkFailureTracker` + helpers → `lib/networkFailureTracker.ts`
|
|||
|
|
3. Extraire retry logic → `lib/retry.ts`
|
|||
|
|
4. Extraire request interceptor → `interceptors/request.ts`
|
|||
|
|
5. Extraire response success → `interceptors/response.ts`
|
|||
|
|
6. Extraire error handler → `interceptors/error.ts`
|
|||
|
|
7. Réduire `client.ts` à l'assemblage.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2.3 Correctifs CSS — Analyse et intégration
|
|||
|
|
|
|||
|
|
**fix-input-focus.css :**
|
|||
|
|
- **Problème :** Tailwind `focus:` applique ring/border sur tous les inputs (souris + clavier). Souhait : `focus-visible` uniquement (clavier).
|
|||
|
|
- **Cause :** Composant `Input` (input.tsx) utilise `focus-visible:ring-2` etc. Les navigateurs/situations où `:focus-visible` ne s'applique pas correctement sont comblés par le fix.
|
|||
|
|
- **Solution :** Intégrer dans le composant Input. Ne pas utiliser `focus:` dans les classes Tailwind. Utiliser uniquement `focus-visible:`. Ajouter dans `index.css` :
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* Design System: focus only for keyboard (focus-visible) */
|
|||
|
|
@layer base {
|
|||
|
|
input:focus:not(:focus-visible),
|
|||
|
|
textarea:focus:not(:focus-visible),
|
|||
|
|
select:focus:not(:focus-visible) {
|
|||
|
|
outline: none;
|
|||
|
|
--tw-ring-width: 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Puis supprimer `fix-input-focus.css`.
|
|||
|
|
|
|||
|
|
**fix-login-form.css (391 lignes) :**
|
|||
|
|
- **Problème :** Formulaires (login, register) ont des inputs/boutons qui se rétrécissent en flex, des largeurs incorrectes, autofill jaune.
|
|||
|
|
- **Cause racine :**
|
|||
|
|
1. **Autofill** : `-webkit-autofill` garde un fond jaune. Fix : `box-shadow: inset` pour masquer.
|
|||
|
|
2. **Flex shrink** : Parents en `flex` avec `space-y-*` provoquent `min-width: 0` sur enfants. Les inputs n'ont pas `min-w-0` ou `flex-shrink-0` selon le besoin.
|
|||
|
|
3. **Largeur** : `w-full` ne suffit pas si parent a `min-width: 0` et flex.
|
|||
|
|
|
|||
|
|
- **Solution :** Intégrer dans les primitives du Design System :
|
|||
|
|
|
|||
|
|
1. **Autofill** — Dans `index.css` ou `design-system.css` :
|
|||
|
|
```css
|
|||
|
|
@layer base {
|
|||
|
|
input:-webkit-autofill,
|
|||
|
|
input:-webkit-autofill:hover,
|
|||
|
|
input:-webkit-autofill:focus {
|
|||
|
|
-webkit-box-shadow: 0 0 0 1000px var(--background) inset;
|
|||
|
|
-webkit-text-fill-color: var(--foreground);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Input** — Dans `components/ui/input.tsx`, ajouter `min-w-0` et `flex-1` si dans un flex :
|
|||
|
|
```tsx
|
|||
|
|
className={cn(
|
|||
|
|
"flex h-11 w-full min-w-0 rounded-xl ...",
|
|||
|
|
// ...
|
|||
|
|
)}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **AuthInput** — Vérifier qu'il a `w-full min-w-0` et que le parent form utilise `flex flex-col` avec `space-y-4` sans conflit.
|
|||
|
|
|
|||
|
|
4. **Boutons submit** — S'assurer que `AuthButton` a `w-full min-w-0 flex-shrink-0`.
|
|||
|
|
|
|||
|
|
5. Supprimer `fix-login-form.css` une fois les primitives corrigées.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Analyse "Perception Premium" (UI/UX)
|
|||
|
|
|
|||
|
|
### 3.1 Anatomie du Player — Waveform réactif
|
|||
|
|
|
|||
|
|
**État actuel :** 24 barres avec `height: 20 + Math.random() * 80` et animation CSS. Aucun lien avec l'audio.
|
|||
|
|
|
|||
|
|
**Stack technique pour waveform réactif :**
|
|||
|
|
|
|||
|
|
| Option | Complexité | Qualité | Commentaire |
|
|||
|
|
|--------|------------|---------|-------------|
|
|||
|
|
| **Web Audio API** | Moyenne | Élevée | AnalyserNode + getByteFrequencyData. Barres = niveaux de fréquence. |
|
|||
|
|
| **Wavesurfer.js** | Faible | Très élevée | Waveform canvas, seek intégré. Dépendance ~50kb. |
|
|||
|
|
| **Peaks.js (BBC)** | Moyenne | Élevée | Pré-calcul server-side. Nécessite backend. |
|
|||
|
|
|
|||
|
|
**Recommandation :** **Wavesurfer.js** pour le player expanded (vue détaillée). Pour la barre compacte, garder un EQ animé (Web Audio API) plus léger.
|
|||
|
|
|
|||
|
|
**Implémentation Web Audio API (EQ bars) :**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// useAudioAnalyser.ts
|
|||
|
|
const useAudioAnalyser = (audioElement: HTMLAudioElement | null, playing: boolean) => {
|
|||
|
|
const [levels, setLevels] = useState<number[]>(Array(24).fill(0));
|
|||
|
|
const analyserRef = useRef<AnalyserNode | null>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!audioElement) return;
|
|||
|
|
const ctx = new AudioContext();
|
|||
|
|
const source = ctx.createMediaElementSource(audioElement);
|
|||
|
|
const analyser = ctx.createAnalyser();
|
|||
|
|
analyser.fftSize = 256;
|
|||
|
|
source.connect(analyser);
|
|||
|
|
analyser.connect(ctx.destination);
|
|||
|
|
analyserRef.current = analyser;
|
|||
|
|
|
|||
|
|
const data = new Uint8Array(analyser.frequencyBinCount);
|
|||
|
|
let raf: number;
|
|||
|
|
|
|||
|
|
const update = () => {
|
|||
|
|
if (!analyserRef.current || !playing) return;
|
|||
|
|
analyserRef.current.getByteFrequencyData(data);
|
|||
|
|
const step = Math.floor(data.length / 24);
|
|||
|
|
const newLevels = Array.from({ length: 24 }, (_, i) => data[i * step] / 255);
|
|||
|
|
setLevels(newLevels);
|
|||
|
|
raf = requestAnimationFrame(update);
|
|||
|
|
};
|
|||
|
|
if (playing) update();
|
|||
|
|
return () => cancelAnimationFrame(raf);
|
|||
|
|
}, [audioElement, playing]);
|
|||
|
|
|
|||
|
|
return levels;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.2 Recherche Header — Plan d'implémentation
|
|||
|
|
|
|||
|
|
**État actuel :** Input avec placeholder "What do you want to play?" sans `onChange` ni navigation.
|
|||
|
|
|
|||
|
|
**Plan :**
|
|||
|
|
|
|||
|
|
1. **State** : Créer `useHeaderSearch()` ou utiliser un store `uiStore.searchQuery` (optionnel).
|
|||
|
|
2. **Navigation** : Sur submit (Enter) ou clic sur suggestion → `navigate(\`/search?q=${encodeURIComponent(query)}\`)`.
|
|||
|
|
3. **Composant** : Remplacer l'input par un composant qui :
|
|||
|
|
- `onKeyDown` (Enter) → navigate
|
|||
|
|
- `onChange` → setQuery (pour suggestions futures)
|
|||
|
|
- Optionnel : dropdown avec suggestions (debounced search API)
|
|||
|
|
|
|||
|
|
**Diff conceptuel :**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// Header.tsx
|
|||
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
|
|||
|
|
const handleSearch = (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (searchQuery.trim()) {
|
|||
|
|
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
<form onSubmit={handleSearch} className="flex-1 max-w-md">
|
|||
|
|
<input
|
|||
|
|
value={searchQuery}
|
|||
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|||
|
|
placeholder="What do you want to play?"
|
|||
|
|
// ...
|
|||
|
|
/>
|
|||
|
|
</form>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Temps estimé : 2–4 h.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3.3 Skeleton & Transitions — Routes sans LoadingState
|
|||
|
|
|
|||
|
|
| Route | Composant | État actuel | Skeleton proposé |
|
|||
|
|
|-------|------------|-------------|-------------------|
|
|||
|
|
| `/dashboard` | DashboardPage | Present | Déjà OK |
|
|||
|
|
| `/library` | LibraryPage | Mix | LibraryPageSkeleton (grille cartes) |
|
|||
|
|
| `/marketplace` | MarketplaceHome | Spinner générique | MarketplaceSkeleton (grille produits) |
|
|||
|
|
| `/search` | SearchPage | SearchPageSkeleton | OK |
|
|||
|
|
| `/playlists` | PlaylistRoutes | Variable | PlaylistGridSkeleton |
|
|||
|
|
| `/sell` | SellerDashboardView | Spinner | SellerDashboardSkeleton (cards + table) |
|
|||
|
|
| `/profile` | UserProfilePage | UserProfilePageSkeleton | OK |
|
|||
|
|
| `/settings` | SettingsPage | SettingsPageSkeleton | OK |
|
|||
|
|
| `/chat` | ChatPage | Variable | ChatSkeleton (sidebar + messages) |
|
|||
|
|
|
|||
|
|
**Structure type MarketplaceSkeleton :**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
export function MarketplaceSkeleton() {
|
|||
|
|
return (
|
|||
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|||
|
|
{Array.from({ length: 8 }).map((i) => (
|
|||
|
|
<Card key={i} className="overflow-hidden">
|
|||
|
|
<Skeleton className="aspect-square w-full" />
|
|||
|
|
<CardContent className="p-4">
|
|||
|
|
<Skeleton className="h-5 w-3/4 mb-2" />
|
|||
|
|
<Skeleton className="h-4 w-1/2" />
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Audit de Rigueur (Design System)
|
|||
|
|
|
|||
|
|
### 4.1 Fichiers avec valeurs arbitraires
|
|||
|
|
|
|||
|
|
Commande : `node scripts/report-arbitrary-values.mjs`
|
|||
|
|
|
|||
|
|
Résultat du grep manuel : **~40 fichiers** avec `w-[`, `h-[`, `gap-[`, `rounded-[`, etc.
|
|||
|
|
|
|||
|
|
Exemples critiques :
|
|||
|
|
- `GlobalPlayer.tsx` : `z-[100]`, `translate-y-[200%]`, `duration-[var(--duration-normal)]`
|
|||
|
|
- `PlayerControls.tsx` : `text-[8px]` (badge repeat)
|
|||
|
|
- `ChatMessage.tsx`, `Header.tsx`, etc.
|
|||
|
|
|
|||
|
|
### 4.2 Règle ESLint pour bloquer les arbitraires
|
|||
|
|
|
|||
|
|
**Option A — eslint-plugin-tailwindcss :**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
// .eslintrc ou eslint.config.js
|
|||
|
|
{
|
|||
|
|
"plugins": ["tailwindcss"],
|
|||
|
|
"rules": {
|
|||
|
|
"tailwindcss/no-arbitrary-value": "warn"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Option B — Règle custom :**
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
// eslint-rules/no-arbitrary-tailwind.js
|
|||
|
|
module.exports = {
|
|||
|
|
meta: {
|
|||
|
|
type: 'suggestion',
|
|||
|
|
docs: { description: 'Disallow arbitrary values in Tailwind classes' },
|
|||
|
|
schema: [],
|
|||
|
|
},
|
|||
|
|
create(context) {
|
|||
|
|
const arbitraryPattern = /\b(w|h|gap|p|m|rounded|text|top|left|right|bottom|z)-\[[^\]]+\]/;
|
|||
|
|
return {
|
|||
|
|
JSXAttribute(node) {
|
|||
|
|
if (node.name.name === 'className' && node.value?.type === 'Literal') {
|
|||
|
|
const value = node.value.value || '';
|
|||
|
|
if (arbitraryPattern.test(value)) {
|
|||
|
|
context.report({
|
|||
|
|
node: node.value,
|
|||
|
|
message: 'Avoid arbitrary Tailwind values. Use design tokens or Tailwind scale.',
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Option C — Tailwind config (recommended) :**
|
|||
|
|
|
|||
|
|
Dans `tailwind.config.ts`, restreindre les valeurs arbitraires via `safelist` ou en n'utilisant que des classes prédéfinies. Approche plus stricte.
|
|||
|
|
|
|||
|
|
**Recommandation :** `eslint-plugin-tailwindcss` avec `no-arbitrary-value: "warn"` en premier. Migration progressive. Puis passer à `"error"` une fois les 40 fichiers corrigés.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Rapport d'Exécution Immédiate — Format "Pourquoi / Comment / Diff"
|
|||
|
|
|
|||
|
|
### 5.1 Recherche Header non fonctionnelle
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| **Pourquoi ça fait amateur** | L'utilisateur voit une barre de recherche évidente (comme Spotify) mais rien ne se passe. Perception de feature incomplète. |
|
|||
|
|
| **Comment font les géants** | Spotify : recherche globale, navigation vers /search, suggestions en temps réel. Discord : command palette Cmd+K. |
|
|||
|
|
| **Diff conceptuel** | Voir section 3.2. Ajouter `value`, `onChange`, `onSubmit` → `navigate('/search?q=' + query)`. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.2 EditProfile crash (AuthContext)
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| **Pourquoi ça fait amateur** | Un onglet Settings (Profile) qui crashe = bug critique. L'utilisateur ne peut pas éditer son profil. |
|
|||
|
|
| **Comment font les géants** | Une seule source d'auth. Pas de Context qui n'existe que dans les tests. |
|
|||
|
|
| **Diff conceptuel** | `useEditProfile` : remplacer `useAuth` (context) par `useUser` (React Query). 1 ligne modifiée. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.3 Waveform décoratif
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| **Pourquoi ça fait amateur** | Les barres bougent au hasard. L'utilisateur ne voit pas le lien avec la musique. SoundCloud/Spotify : waveform = audio réel. |
|
|||
|
|
| **Comment font les géants** | Web Audio API ou Wavesurfer. Visualisation = données audio réelles. |
|
|||
|
|
| **Diff conceptuel** | Intégrer `useAudioAnalyser` avec `AnalyserNode`. Remplacer `Math.random()` par `levels[i]` du canal fréquentiel. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.4 fix-input-focus.css
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| **Pourquoi ça fait amateur** | Un fichier "fix" indique que le design system n'a pas résolu le problème à la source. |
|
|||
|
|
| **Comment font les géants** | Focus géré dans les composants. `:focus-visible` uniquement. Pas de patch global. |
|
|||
|
|
| **Diff conceptuel** | Ajouter règle `@layer base` dans index.css. Supprimer fix-input-focus.css. Vérifier Input n'utilise pas `focus:` sans `visible`. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.5 fix-login-form.css (391 lignes)
|
|||
|
|
|
|||
|
|
| | |
|
|||
|
|
|---|---|
|
|||
|
|
| **Pourquoi ça fait amateur** | 391 lignes de correctifs avec `!important` = guerre de spécificité. Layout non maîtrisé. |
|
|||
|
|
| **Comment font les géants** | Formulaires avec primitives correctes : `min-w-0`, `flex-1`, autofill géré en amont. |
|
|||
|
|
| **Diff conceptuel** | Intégrer autofill dans design-system.css. Corriger Input, AuthInput, AuthButton avec `min-w-0`. Supprimer fix-login-form.css. |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Priorisation Exécution v1.0
|
|||
|
|
|
|||
|
|
| Priorité | Action | Impact | Effort | Risque |
|
|||
|
|
|----------|--------|--------|--------|--------|
|
|||
|
|
| P0 | Migrer useEditProfile, ProfileView, useAuthView vers useUser | Fix crash Settings | 2 h | Faible |
|
|||
|
|
| P0 | Supprimer AuthContext, mettre à jour Storybook | Une source auth | 2 h | Moyen |
|
|||
|
|
| P1 | Recherche Header fonctionnelle | UX immédiate | 2–4 h | Faible |
|
|||
|
|
| P1 | Intégrer fix-input-focus dans design system | Réduction dette | 2 h | Faible |
|
|||
|
|
| P2 | Découper client.ts (phases 1–3) | Maintenabilité | 2–3 j | Moyen |
|
|||
|
|
| P2 | fix-login-form → primitives | Réduction dette | 1 j | Moyen |
|
|||
|
|
| P3 | Waveform réactif (Web Audio API) | Polish player | 1–2 j | Moyen |
|
|||
|
|
| P3 | Skeletons Marketplace, SellerDashboard | Perception loaded | 4 h | Faible |
|
|||
|
|
| P4 | ESLint no-arbitrary-value | Rigueur long terme | 2 h | Faible |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Prochaine étape recommandée :** Exécuter P0 (migration AuthContext → useUser) pour éliminer le crash critique avant toute autre action.
|