stabilisation: fix commit
This commit is contained in:
parent
963fdf4f53
commit
a2576c4eae
161 changed files with 10147 additions and 365 deletions
2231
STORYBOOK_AUDIT_REPORT.md
Normal file
2231
STORYBOOK_AUDIT_REPORT.md
Normal file
File diff suppressed because it is too large
Load diff
562
STORYBOOK_ROADMAP.md
Normal file
562
STORYBOOK_ROADMAP.md
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
# 📚 ROADMAP STORYBOOK 100% COVERAGE
|
||||
|
||||
**Projet**: Veza Music Platform
|
||||
**Objectif**: Couverture complète de tous les composants
|
||||
**Durée**: 12 semaines
|
||||
**Date de début**: 3 Février 2026
|
||||
**Date cible de fin**: 27 Avril 2026
|
||||
|
||||
---
|
||||
|
||||
## 📊 Vue d'Ensemble
|
||||
|
||||
| Métrique | Actuel | Cible | Gap |
|
||||
|----------|--------|-------|-----|
|
||||
| **Stories** | 164 | 344 | +180 |
|
||||
| **Composants** | 384 | 384 | - |
|
||||
| **Couverture** | 42% | 100% | +58% |
|
||||
| **Heures estimées** | - | 270h | ~23h/semaine |
|
||||
|
||||
### Progression Hebdomadaire Ciblée
|
||||
|
||||
```
|
||||
Semaine 1 ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░ 47% (+5%)
|
||||
Semaine 2 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░ 52% (+5%)
|
||||
Semaine 3 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░ 57% (+5%)
|
||||
Semaine 4 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░ 62% (+5%)
|
||||
Semaine 5 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░ 67% (+5%)
|
||||
Semaine 6 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 72% (+5%)
|
||||
Semaine 7 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 77% (+5%)
|
||||
Semaine 8 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 82% (+5%)
|
||||
Semaine 9 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 87% (+5%)
|
||||
Semaine 10 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ 92% (+5%)
|
||||
Semaine 11 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 96% (+4%)
|
||||
Semaine 12 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100% (+4%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Milestones
|
||||
|
||||
| Milestone | Date | Couverture | Description |
|
||||
|-----------|------|------------|-------------|
|
||||
| **M1** | 16 Fév 2026 | 52% | Pages critiques + Admin |
|
||||
| **M2** | 9 Mars 2026 | 67% | Core Features (Player, Playlists, Tracks) |
|
||||
| **M3** | 30 Mars 2026 | 82% | Full Features (Upload, Chat, Settings) |
|
||||
| **M4** | 27 Avr 2026 | 100% | Couverture complète |
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 1 - Pages Critiques & Erreurs
|
||||
|
||||
**Dates**: 3-9 Février 2026
|
||||
**Priorité**: 🔴 P0 (Critique)
|
||||
**Heures**: 18h
|
||||
**Objectif**: Couvrir toutes les pages d'erreur et d'authentification
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | NotFoundPage | `src/features/error/pages/NotFoundPage.tsx` | 🟢 Low | 1h | Default, WithSuggestions |
|
||||
| 2 | ServerErrorPage | `src/features/error/pages/ServerErrorPage.tsx` | 🟢 Low | 1h | Default, WithRetry, NetworkError |
|
||||
| 3 | LoginPage | `src/features/auth/pages/LoginPage.tsx` | 🟡 Medium | 2h | Default, WithError, Loading |
|
||||
| 4 | RegisterPage | `src/features/auth/pages/RegisterPage.tsx` | 🟡 Medium | 2h | Default, WithError, Success |
|
||||
| 5 | ForgotPasswordPage | `src/features/auth/pages/ForgotPasswordPage.tsx` | 🟡 Medium | 2h | Default, Sent, Error |
|
||||
| 6 | ResetPasswordPage | `src/features/auth/pages/ResetPasswordPage.tsx` | 🟡 Medium | 2h | Default, Success, InvalidToken |
|
||||
| 7 | VerifyEmailPage | `src/features/auth/pages/VerifyEmailPage.tsx` | 🟢 Low | 1.5h | Pending, Verified, Error |
|
||||
| 8 | ForgotPasswordForm | `src/features/auth/components/ForgotPasswordForm.tsx` | 🟡 Medium | 1.5h | Default, Loading, Success |
|
||||
| 9 | TwoFactorVerify | `src/features/auth/components/TwoFactorVerify.tsx` | 🟡 Medium | 2h | Default, Error, Loading |
|
||||
| 10 | TwoFactorSetup | `src/features/auth/components/TwoFactorSetup.tsx` | 🟡 Medium | 2h | QRCode, Verification, Complete |
|
||||
| 11 | AuthErrorMessage | `src/features/auth/components/AuthErrorMessage.tsx` | 🟢 Low | 1h | Default, Network, Validation |
|
||||
|
||||
### Checklist Sprint 1
|
||||
|
||||
- [ ] NotFoundPage.stories.tsx
|
||||
- [ ] ServerErrorPage.stories.tsx
|
||||
- [ ] LoginPage.stories.tsx
|
||||
- [ ] RegisterPage.stories.tsx
|
||||
- [ ] ForgotPasswordPage.stories.tsx
|
||||
- [ ] ResetPasswordPage.stories.tsx
|
||||
- [ ] VerifyEmailPage.stories.tsx
|
||||
- [ ] ForgotPasswordForm.stories.tsx
|
||||
- [ ] TwoFactorVerify.stories.tsx
|
||||
- [ ] TwoFactorSetup.stories.tsx
|
||||
- [ ] AuthErrorMessage.stories.tsx
|
||||
|
||||
**Couverture attendue**: 42% → 47%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 2 - Dashboard & Admin
|
||||
|
||||
**Dates**: 10-16 Février 2026
|
||||
**Priorité**: 🔴 P0 (Critique)
|
||||
**Heures**: 20h
|
||||
**Objectif**: Couvrir le dashboard et toutes les vues admin
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | DashboardPage | `src/features/dashboard/pages/DashboardPage.tsx` | 🔴 High | 3h | Default, Loading, Empty |
|
||||
| 2 | AdminDashboardView | `src/components/admin/AdminDashboardView.tsx` | 🔴 High | 3h | Default, Loading |
|
||||
| 3 | AdminUsersView | `src/components/admin/AdminUsersView.tsx` | 🔴 High | 3h | Default, Empty, Loading, WithFilters |
|
||||
| 4 | AdminModerationView | `src/components/admin/AdminModerationView.tsx` | 🔴 High | 3h | Default, Queue, Empty |
|
||||
| 5 | AdminSettingsView | `src/components/admin/AdminSettingsView.tsx` | 🟡 Medium | 2h | Default, Saving |
|
||||
| 6 | AdminAuditLogsView | `src/components/admin/AdminAuditLogsView.tsx` | 🟡 Medium | 2h | Default, Filtered, Empty |
|
||||
| 7 | AdminView | `src/components/views/AdminView.tsx` | 🟡 Medium | 2h | Default |
|
||||
| 8 | BanUserModal | `src/components/admin/BanUserModal.tsx` | 🟡 Medium | 1.5h | Default, Confirm |
|
||||
| 9 | UserTableRow | `src/components/admin/UserTableRow.tsx` | 🟢 Low | 1h | Default, Selected, Banned |
|
||||
|
||||
### Checklist Sprint 2
|
||||
|
||||
- [ ] DashboardPage.stories.tsx
|
||||
- [ ] AdminDashboardView.stories.tsx
|
||||
- [ ] AdminUsersView.stories.tsx
|
||||
- [ ] AdminModerationView.stories.tsx
|
||||
- [ ] AdminSettingsView.stories.tsx
|
||||
- [ ] AdminAuditLogsView.stories.tsx
|
||||
- [ ] AdminView.stories.tsx
|
||||
- [ ] BanUserModal.stories.tsx
|
||||
- [ ] UserTableRow.stories.tsx
|
||||
|
||||
**Couverture attendue**: 47% → 52%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 3 - Player & Playback
|
||||
|
||||
**Dates**: 17-23 Février 2026
|
||||
**Priorité**: 🟠 P1 (Haute)
|
||||
**Heures**: 18h
|
||||
**Objectif**: Compléter la couverture du player
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | AudioPlayer | `src/features/player/components/AudioPlayer.tsx` | 🔴 High | 3h | Playing, Paused, Loading, Error |
|
||||
| 2 | FullPlayer | `src/components/player/FullPlayer.tsx` | 🔴 High | 3h | Default, WithQueue, WithLyrics |
|
||||
| 3 | PlayerError | `src/features/player/components/PlayerError.tsx` | 🟢 Low | 1h | NetworkError, FormatError, Generic |
|
||||
| 4 | PlaybackSpeedControl | `src/features/player/components/PlaybackSpeedControl.tsx` | 🟢 Low | 1h | Default, 1x, 1.5x, 2x |
|
||||
| 5 | QueuePanel | `src/components/player/QueuePanel.tsx` | 🟡 Medium | 2h | Default, Empty, Reordering |
|
||||
| 6 | QueueView | `src/components/views/QueueView.tsx` | 🟡 Medium | 2h | Default, Empty |
|
||||
| 7 | SaveQueueAsPlaylistModal | `src/components/player/SaveQueueAsPlaylistModal.tsx` | 🟡 Medium | 1.5h | Default, Saving, Success |
|
||||
| 8 | LyricsPanel | `src/components/studio/LyricsPanel.tsx` | 🟡 Medium | 1.5h | Default, Synced, Empty |
|
||||
|
||||
### Checklist Sprint 3
|
||||
|
||||
- [ ] AudioPlayer.stories.tsx
|
||||
- [ ] FullPlayer.stories.tsx
|
||||
- [ ] PlayerError.stories.tsx
|
||||
- [ ] PlaybackSpeedControl.stories.tsx
|
||||
- [ ] QueuePanel.stories.tsx
|
||||
- [ ] QueueView.stories.tsx
|
||||
- [ ] SaveQueueAsPlaylistModal.stories.tsx
|
||||
- [ ] LyricsPanel.stories.tsx
|
||||
|
||||
**Couverture attendue**: 52% → 57%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 4 - Playlists Complet
|
||||
|
||||
**Dates**: 24 Février - 2 Mars 2026
|
||||
**Priorité**: 🟠 P1 (Haute)
|
||||
**Heures**: 20h
|
||||
**Objectif**: 100% couverture feature playlists
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | PlaylistList | `src/features/playlists/components/PlaylistList.tsx` | 🔴 High | 2.5h | Default, Grid, Empty, Loading |
|
||||
| 2 | PlaylistDetailPage | `src/features/playlists/pages/PlaylistDetailPage.tsx` | 🔴 High | 3h | Default, Loading, NotFound |
|
||||
| 3 | PlaylistListPage | `src/features/playlists/pages/PlaylistListPage.tsx` | 🔴 High | 2.5h | Default, Empty, Loading |
|
||||
| 4 | PlaylistTrackList | `src/features/playlists/components/PlaylistTrackList.tsx` | 🔴 High | 2.5h | Default, Empty, Reordering |
|
||||
| 5 | PlaylistTrackItem | `src/features/playlists/components/PlaylistTrackItem.tsx` | 🟡 Medium | 1.5h | Default, Playing, Selected |
|
||||
| 6 | PlaylistSearch | `src/features/playlists/components/PlaylistSearch.tsx` | 🟡 Medium | 1.5h | Default, WithResults |
|
||||
| 7 | PlaylistRecommendations | `src/features/playlists/components/PlaylistRecommendations.tsx` | 🟡 Medium | 1.5h | Default, Empty |
|
||||
| 8 | AddCollaboratorModal | `src/features/playlists/components/AddCollaboratorModal.tsx` | 🟡 Medium | 1.5h | Default, Searching, Added |
|
||||
| 9 | CollaboratorList | `src/features/playlists/components/CollaboratorList.tsx` | 🟢 Low | 1h | Default, Empty |
|
||||
| 10 | SharePlaylistModal | `src/features/playlists/components/SharePlaylistModal.tsx` | 🟡 Medium | 1.5h | Default, Copied |
|
||||
|
||||
### Checklist Sprint 4
|
||||
|
||||
- [ ] PlaylistList.stories.tsx
|
||||
- [ ] PlaylistDetailPage.stories.tsx
|
||||
- [ ] PlaylistListPage.stories.tsx
|
||||
- [ ] PlaylistTrackList.stories.tsx
|
||||
- [ ] PlaylistTrackItem.stories.tsx
|
||||
- [ ] PlaylistSearch.stories.tsx
|
||||
- [ ] PlaylistRecommendations.stories.tsx
|
||||
- [ ] AddCollaboratorModal.stories.tsx
|
||||
- [ ] CollaboratorList.stories.tsx
|
||||
- [ ] SharePlaylistModal.stories.tsx
|
||||
|
||||
**Couverture attendue**: 57% → 62%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 5 - Tracks & Search
|
||||
|
||||
**Dates**: 3-9 Mars 2026
|
||||
**Priorité**: 🟠 P1 (Haute)
|
||||
**Heures**: 18h
|
||||
**Objectif**: Compléter tracks et recherche
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | TrackDetailPage | `src/features/tracks/pages/TrackDetailPage.tsx` | 🔴 High | 3h | Default, Loading, NotFound |
|
||||
| 2 | TrackSearch | `src/features/tracks/components/TrackSearch.tsx` | 🔴 High | 2.5h | Default, WithResults, NoResults |
|
||||
| 3 | TrackSearchResults | `src/features/tracks/components/TrackSearchResults.tsx` | 🔴 High | 2h | Default, Empty, Loading |
|
||||
| 4 | TrackSearchFilters | `src/features/tracks/components/TrackSearchFilters.tsx` | 🟡 Medium | 1.5h | Default, Applied |
|
||||
| 5 | TrackListContainer | `src/features/tracks/components/TrackListContainer.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 6 | TrackAnalyticsView | `src/components/tracks/TrackAnalyticsView.tsx` | 🔴 High | 2.5h | Default, Loading, Empty |
|
||||
| 7 | SearchPage | `src/features/search/pages/SearchPage.tsx` | 🔴 High | 3h | Default, Results, NoResults, Loading |
|
||||
| 8 | GlobalSearchBar | `src/components/search/GlobalSearchBar.tsx` | 🟡 Medium | 2h | Default, Focused, WithSuggestions |
|
||||
|
||||
### Checklist Sprint 5
|
||||
|
||||
- [ ] TrackDetailPage.stories.tsx
|
||||
- [ ] TrackSearch.stories.tsx
|
||||
- [ ] TrackSearchResults.stories.tsx
|
||||
- [ ] TrackSearchFilters.stories.tsx
|
||||
- [ ] TrackListContainer.stories.tsx
|
||||
- [ ] TrackAnalyticsView.stories.tsx
|
||||
- [ ] SearchPage.stories.tsx
|
||||
- [ ] GlobalSearchBar.stories.tsx
|
||||
|
||||
**Couverture attendue**: 62% → 67%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 6 - Upload & Library
|
||||
|
||||
**Dates**: 10-16 Mars 2026
|
||||
**Priorité**: 🟠 P1 (Haute)
|
||||
**Heures**: 22h
|
||||
**Objectif**: Workflow upload et bibliothèque
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | UploadView | `src/components/views/UploadView.tsx` | 🔴 High | 3h | Default, Uploading, Complete, Error |
|
||||
| 2 | UploadProgressBar | `src/components/upload/UploadProgressBar.tsx` | 🟢 Low | 1h | Default, Complete, Error |
|
||||
| 3 | FileUploadZone | `src/components/upload/FileUploadZone.tsx` | 🟡 Medium | 2h | Default, Dragging, Processing |
|
||||
| 4 | BulkUploadModal | `src/components/upload/BulkUploadModal.tsx` | 🔴 High | 2.5h | Default, Uploading, Complete |
|
||||
| 5 | MetadataEditor | `src/components/upload/MetadataEditor.tsx` | 🔴 High | 2.5h | Default, WithData, Saving |
|
||||
| 6 | MetadataForm | `src/components/upload/metadata/MetadataForm.tsx` | 🟡 Medium | 2h | Default, WithErrors |
|
||||
| 7 | CoverArtUploadModal | `src/components/upload/CoverArtUploadModal.tsx` | 🟡 Medium | 1.5h | Default, Uploading, Preview |
|
||||
| 8 | LibraryPage | `src/features/library/pages/LibraryPage.tsx` | 🔴 High | 2.5h | Default, Empty, Loading |
|
||||
|
||||
### Checklist Sprint 6
|
||||
|
||||
- [ ] UploadView.stories.tsx
|
||||
- [ ] UploadProgressBar.stories.tsx
|
||||
- [ ] FileUploadZone.stories.tsx
|
||||
- [ ] BulkUploadModal.stories.tsx
|
||||
- [ ] MetadataEditor.stories.tsx
|
||||
- [ ] MetadataForm.stories.tsx
|
||||
- [ ] CoverArtUploadModal.stories.tsx
|
||||
- [ ] LibraryPage.stories.tsx
|
||||
|
||||
**Couverture attendue**: 67% → 72%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 7 - Chat & Social
|
||||
|
||||
**Dates**: 17-23 Mars 2026
|
||||
**Priorité**: 🟡 P2 (Moyenne)
|
||||
**Heures**: 18h
|
||||
**Objectif**: Features chat et social
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | ChatPage | `src/features/chat/pages/ChatPage.tsx` | 🔴 High | 3h | Default, Loading, Empty |
|
||||
| 2 | ChatView | `src/components/views/ChatView.tsx` | 🟡 Medium | 2h | Default |
|
||||
| 3 | CreateRoomDialog | `src/features/chat/components/CreateRoomDialog.tsx` | 🟡 Medium | 1.5h | Default, Creating |
|
||||
| 4 | MessageSearch | `src/features/chat/components/MessageSearch.tsx` | 🟡 Medium | 1.5h | Default, Results, NoResults |
|
||||
| 5 | VirtualizedChatMessages | `src/features/chat/components/VirtualizedChatMessages.tsx` | 🔴 High | 2h | Default, Loading |
|
||||
| 6 | SocialView | `src/components/views/SocialView.tsx` | 🟡 Medium | 2h | Default |
|
||||
| 7 | ConnectionsView | `src/components/social/ConnectionsView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
|
||||
| 8 | FeedView | `src/components/views/FeedView.tsx` | 🔴 High | 2h | Default, Empty, Loading |
|
||||
| 9 | CreatePostModal | `src/components/social/CreatePostModal.tsx` | 🟡 Medium | 1.5h | Default, Posting |
|
||||
|
||||
### Checklist Sprint 7
|
||||
|
||||
- [ ] ChatPage.stories.tsx
|
||||
- [ ] ChatView.stories.tsx
|
||||
- [ ] CreateRoomDialog.stories.tsx
|
||||
- [ ] MessageSearch.stories.tsx
|
||||
- [ ] VirtualizedChatMessages.stories.tsx
|
||||
- [ ] SocialView.stories.tsx
|
||||
- [ ] ConnectionsView.stories.tsx
|
||||
- [ ] FeedView.stories.tsx
|
||||
- [ ] CreatePostModal.stories.tsx
|
||||
|
||||
**Couverture attendue**: 72% → 77%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 8 - Settings & Preferences
|
||||
|
||||
**Dates**: 24-30 Mars 2026
|
||||
**Priorité**: 🟡 P2 (Moyenne)
|
||||
**Heures**: 20h
|
||||
**Objectif**: Tous les panneaux de paramètres
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | SettingsPage | `src/features/settings/pages/SettingsPage.tsx` | 🟡 Medium | 2h | Default |
|
||||
| 2 | SettingsView | `src/components/views/SettingsView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 3 | SecuritySettings | `src/components/settings/SecuritySettings.tsx` | 🔴 High | 2.5h | Default, Updating |
|
||||
| 4 | SessionManagement | `src/components/settings/SessionManagement.tsx` | 🟡 Medium | 2h | Default, WithSessions |
|
||||
| 5 | AppearanceSettingsView | `src/components/settings/AppearanceSettingsView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 6 | AccessibilitySettingsView | `src/components/views/AccessibilitySettingsView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 7 | ChangeEmailModal | `src/components/settings/ChangeEmailModal.tsx` | 🟡 Medium | 1.5h | Default, Sending, Sent |
|
||||
| 8 | ChangeUsernameModal | `src/components/settings/ChangeUsernameModal.tsx` | 🟡 Medium | 1.5h | Default, Checking, Available |
|
||||
| 9 | DeleteAccountView | `src/components/settings/DeleteAccountView.tsx` | 🟡 Medium | 1.5h | Default, Confirm |
|
||||
| 10 | DataExportView | `src/components/settings/DataExportView.tsx` | 🟡 Medium | 1.5h | Default, Exporting, Ready |
|
||||
|
||||
### Checklist Sprint 8
|
||||
|
||||
- [ ] SettingsPage.stories.tsx
|
||||
- [ ] SettingsView.stories.tsx
|
||||
- [ ] SecuritySettings.stories.tsx
|
||||
- [ ] SessionManagement.stories.tsx
|
||||
- [ ] AppearanceSettingsView.stories.tsx
|
||||
- [ ] AccessibilitySettingsView.stories.tsx
|
||||
- [ ] ChangeEmailModal.stories.tsx
|
||||
- [ ] ChangeUsernameModal.stories.tsx
|
||||
- [ ] DeleteAccountView.stories.tsx
|
||||
- [ ] DataExportView.stories.tsx
|
||||
|
||||
**Couverture attendue**: 77% → 82%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 9 - Marketplace & Commerce
|
||||
|
||||
**Dates**: 31 Mars - 6 Avril 2026
|
||||
**Priorité**: 🟡 P2 (Moyenne)
|
||||
**Heures**: 22h
|
||||
**Objectif**: Marketplace et commerce complet
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | MarketplaceView | `src/components/views/MarketplaceView.tsx` | 🔴 High | 2.5h | Default, Loading |
|
||||
| 2 | MarketplaceHome | `src/pages/marketplace/MarketplaceHome.tsx` | 🔴 High | 2.5h | Default, Loading |
|
||||
| 3 | ProductDetailView | `src/components/marketplace/ProductDetailView.tsx` | 🔴 High | 2.5h | Default, Loading, OutOfStock |
|
||||
| 4 | CartView | `src/components/views/CartView.tsx` | 🔴 High | 2h | Default, Empty |
|
||||
| 5 | CartItem | `src/components/commerce/CartItem.tsx` | 🟢 Low | 1h | Default, Removing |
|
||||
| 6 | CheckoutView | `src/components/views/CheckoutView.tsx` | 🔴 High | 3h | Default, Processing, Success |
|
||||
| 7 | OrderSummary | `src/components/commerce/OrderSummary.tsx` | 🟡 Medium | 1.5h | Default, WithDiscount |
|
||||
| 8 | LicenceCard | `src/components/commerce/LicenceCard.tsx` | 🟡 Medium | 1.5h | Basic, Pro, Exclusive |
|
||||
| 9 | PurchasesView | `src/components/views/PurchasesView.tsx` | 🟡 Medium | 2h | Default, Empty |
|
||||
|
||||
### Checklist Sprint 9
|
||||
|
||||
- [ ] MarketplaceView.stories.tsx
|
||||
- [ ] MarketplaceHome.stories.tsx
|
||||
- [ ] ProductDetailView.stories.tsx
|
||||
- [ ] CartView.stories.tsx
|
||||
- [ ] CartItem.stories.tsx
|
||||
- [ ] CheckoutView.stories.tsx
|
||||
- [ ] OrderSummary.stories.tsx
|
||||
- [ ] LicenceCard.stories.tsx
|
||||
- [ ] PurchasesView.stories.tsx
|
||||
|
||||
**Couverture attendue**: 82% → 87%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 10 - Views & Layouts
|
||||
|
||||
**Dates**: 7-13 Avril 2026
|
||||
**Priorité**: 🟡 P2 (Moyenne)
|
||||
**Heures**: 18h
|
||||
**Objectif**: Vues restantes
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | DiscoverView | `src/components/views/DiscoverView.tsx` | 🔴 High | 2.5h | Default, Loading |
|
||||
| 2 | ExploreView | `src/components/views/ExploreView.tsx` | 🔴 High | 2.5h | Default |
|
||||
| 3 | ProfileView | `src/components/views/ProfileView.tsx` | 🔴 High | 2.5h | Default, Loading |
|
||||
| 4 | UserProfilePage | `src/features/profile/pages/UserProfilePage.tsx` | 🔴 High | 2.5h | Default, Own, NotFound |
|
||||
| 5 | NotificationsPage | `src/features/notifications/pages/NotificationsPage.tsx` | 🟡 Medium | 2h | Default, Empty |
|
||||
| 6 | NotificationsView | `src/components/views/NotificationsView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 7 | PlaylistsView | `src/components/views/PlaylistsView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 8 | AnalyticsView | `src/components/views/AnalyticsView.tsx` | 🔴 High | 2h | Default, Loading |
|
||||
|
||||
### Checklist Sprint 10
|
||||
|
||||
- [ ] DiscoverView.stories.tsx
|
||||
- [ ] ExploreView.stories.tsx
|
||||
- [ ] ProfileView.stories.tsx
|
||||
- [ ] UserProfilePage.stories.tsx
|
||||
- [ ] NotificationsPage.stories.tsx
|
||||
- [ ] NotificationsView.stories.tsx
|
||||
- [ ] PlaylistsView.stories.tsx
|
||||
- [ ] AnalyticsView.stories.tsx
|
||||
|
||||
**Couverture attendue**: 87% → 92%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 11 - Studio & Education
|
||||
|
||||
**Dates**: 14-20 Avril 2026
|
||||
**Priorité**: 🟢 P3 (Basse)
|
||||
**Heures**: 20h
|
||||
**Objectif**: Studio et éducation
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | StudioView | `src/components/views/StudioView.tsx` | 🔴 High | 2.5h | Default |
|
||||
| 2 | ProjectDetailView | `src/components/studio/ProjectDetailView.tsx` | 🔴 High | 2.5h | Default, Loading |
|
||||
| 3 | CreateProjectModal | `src/components/studio/CreateProjectModal.tsx` | 🟡 Medium | 1.5h | Default, Creating |
|
||||
| 4 | GoLiveView | `src/components/studio/GoLiveView.tsx` | 🔴 High | 2.5h | Setup, Live, Ended |
|
||||
| 5 | LyricsEditorModal | `src/components/studio/LyricsEditorModal.tsx` | 🟡 Medium | 1.5h | Default, Syncing |
|
||||
| 6 | EducationView | `src/components/views/EducationView.tsx` | 🟡 Medium | 2h | Default |
|
||||
| 7 | CourseDetailView | `src/components/education/CourseDetailView.tsx` | 🔴 High | 2h | Default, Enrolled |
|
||||
| 8 | CourseLearningView | `src/components/education/CourseLearningView.tsx` | 🔴 High | 2.5h | Default, Complete |
|
||||
| 9 | LiveView | `src/components/views/LiveView.tsx` | 🟡 Medium | 2h | Default |
|
||||
|
||||
### Checklist Sprint 11
|
||||
|
||||
- [ ] StudioView.stories.tsx
|
||||
- [ ] ProjectDetailView.stories.tsx
|
||||
- [ ] CreateProjectModal.stories.tsx
|
||||
- [ ] GoLiveView.stories.tsx
|
||||
- [ ] LyricsEditorModal.stories.tsx
|
||||
- [ ] EducationView.stories.tsx
|
||||
- [ ] CourseDetailView.stories.tsx
|
||||
- [ ] CourseLearningView.stories.tsx
|
||||
- [ ] LiveView.stories.tsx
|
||||
|
||||
**Couverture attendue**: 92% → 96%
|
||||
|
||||
---
|
||||
|
||||
## 📅 SPRINT 12 - Sprint Final - 100%
|
||||
|
||||
**Dates**: 21-27 Avril 2026
|
||||
**Priorité**: 🟢 P3 (Basse)
|
||||
**Heures**: 25h
|
||||
**Objectif**: Tous les composants restants
|
||||
|
||||
### Composants à Créer
|
||||
|
||||
| # | Composant | Chemin | Effort | Heures | Variants |
|
||||
|---|-----------|--------|--------|--------|----------|
|
||||
| 1 | GearView | `src/components/views/GearView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 2 | EquipmentDetailView | `src/components/inventory/EquipmentDetailView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 3 | AchievementsView | `src/components/gamification/AchievementsView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
|
||||
| 4 | AchievementCard | `src/components/gamification/AchievementCard.tsx` | 🟢 Low | 1h | Locked, Unlocked |
|
||||
| 5 | LeaderboardView | `src/components/gamification/LeaderboardView.tsx` | 🟡 Medium | 1.5h | Default |
|
||||
| 6 | XPBar | `src/components/gamification/XPBar.tsx` | 🟢 Low | 1h | Default, LevelUp |
|
||||
| 7 | DeveloperDashboardView | `src/components/developer/DeveloperDashboardView.tsx` | 🔴 High | 2h | Default |
|
||||
| 8 | APIPlaygroundView | `src/components/developer/APIPlaygroundView.tsx` | 🔴 High | 2.5h | Default |
|
||||
| 9 | CreateAPIKeyModal | `src/components/developer/CreateAPIKeyModal.tsx` | 🟡 Medium | 1.5h | Default, Created |
|
||||
| 10 | WebhooksView | `src/features/webhooks/WebhooksView.tsx` | 🔴 High | 2h | Default, Empty |
|
||||
| 11 | MonitoringDashboard | `src/components/monitoring/MonitoringDashboard.tsx` | 🔴 High | 2.5h | Default |
|
||||
| 12 | WishlistView | `src/components/views/WishlistView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
|
||||
| 13 | FileManagerView | `src/components/views/FileManagerView.tsx` | 🔴 High | 2h | Default |
|
||||
|
||||
### Checklist Sprint 12
|
||||
|
||||
- [ ] GearView.stories.tsx
|
||||
- [ ] EquipmentDetailView.stories.tsx
|
||||
- [ ] AchievementsView.stories.tsx
|
||||
- [ ] AchievementCard.stories.tsx
|
||||
- [ ] LeaderboardView.stories.tsx
|
||||
- [ ] XPBar.stories.tsx
|
||||
- [ ] DeveloperDashboardView.stories.tsx
|
||||
- [ ] APIPlaygroundView.stories.tsx
|
||||
- [ ] CreateAPIKeyModal.stories.tsx
|
||||
- [ ] WebhooksView.stories.tsx
|
||||
- [ ] MonitoringDashboard.stories.tsx
|
||||
- [ ] WishlistView.stories.tsx
|
||||
- [ ] FileManagerView.stories.tsx
|
||||
|
||||
**Couverture attendue**: 96% → 100% 🎉
|
||||
|
||||
---
|
||||
|
||||
## 📊 Tableau de Suivi Global
|
||||
|
||||
| Semaine | Sprint | Stories Ajoutées | Couverture | Status |
|
||||
|---------|--------|------------------|------------|--------|
|
||||
| 1 | Critical Pages | 11 | 47% | ⬜ À faire |
|
||||
| 2 | Dashboard & Admin | 9 | 52% | ⬜ À faire |
|
||||
| 3 | Player & Playback | 8 | 57% | ⬜ À faire |
|
||||
| 4 | Playlists Complete | 10 | 62% | ⬜ À faire |
|
||||
| 5 | Tracks & Search | 8 | 67% | ⬜ À faire |
|
||||
| 6 | Upload & Library | 8 | 72% | ⬜ À faire |
|
||||
| 7 | Chat & Social | 9 | 77% | ⬜ À faire |
|
||||
| 8 | Settings & Preferences | 10 | 82% | ⬜ À faire |
|
||||
| 9 | Marketplace & Commerce | 9 | 87% | ⬜ À faire |
|
||||
| 10 | Views & Layouts | 8 | 92% | ⬜ À faire |
|
||||
| 11 | Studio & Education | 9 | 96% | ⬜ À faire |
|
||||
| 12 | Final Sprint | 13 | 100% | ⬜ À faire |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Commandes Utiles
|
||||
|
||||
### Vérifier la progression
|
||||
|
||||
```bash
|
||||
# Compter les stories actuelles
|
||||
find src -name "*.stories.tsx" | wc -l
|
||||
|
||||
# Vérifier la couverture
|
||||
./scripts/storybook-coverage.sh
|
||||
|
||||
# Lister les composants sans stories
|
||||
./scripts/storybook-coverage.sh 2>/dev/null | grep "^ -"
|
||||
```
|
||||
|
||||
### Générer une story
|
||||
|
||||
```bash
|
||||
# Script de génération automatique
|
||||
./scripts/generate-story.sh src/components/views/UploadView.tsx
|
||||
```
|
||||
|
||||
### Build Storybook
|
||||
|
||||
```bash
|
||||
npm run storybook # Développement
|
||||
npm run build-storybook # Production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **Effort estimé**: Basé sur la complexité du composant et le nombre de variants
|
||||
- **Priorité**: P0 (Critique) → P3 (Basse)
|
||||
- **Heures**: Incluent le temps de test et de revue
|
||||
- **Variants**: Nombre minimum recommandé
|
||||
|
||||
---
|
||||
|
||||
*Document généré le 2 Février 2026*
|
||||
*Fichier JSON associé: `storybook-roadmap.json`*
|
||||
|
|
@ -12,8 +12,20 @@ const config: StorybookConfig = {
|
|||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
getAbsolutePath('@storybook/addon-essentials')
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
],
|
||||
"framework": getAbsolutePath('@storybook/react-vite')
|
||||
"framework": getAbsolutePath('@storybook/react-vite'),
|
||||
"docs": {
|
||||
"defaultName": "Documentation"
|
||||
},
|
||||
"typescript": {
|
||||
"reactDocgen": "react-docgen-typescript",
|
||||
"reactDocgenTypescriptOptions": {
|
||||
"shouldExtractLiteralValuesFromEnum": true,
|
||||
"propFilter": (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
|
||||
},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
95
apps/web/.storybook/preview.tsx
Normal file
95
apps/web/.storybook/preview.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import type { Preview } from '@storybook/react-vite';
|
||||
import '../src/index.css';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToastProvider } from '../src/components/feedback/ToastProvider';
|
||||
|
||||
// Create a client for stories
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Custom viewports for responsive testing
|
||||
const customViewports = {
|
||||
mobile: {
|
||||
name: 'Mobile',
|
||||
styles: {
|
||||
width: '375px',
|
||||
height: '667px',
|
||||
},
|
||||
},
|
||||
tablet: {
|
||||
name: 'Tablet',
|
||||
styles: {
|
||||
width: '768px',
|
||||
height: '1024px',
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
name: 'Desktop',
|
||||
styles: {
|
||||
width: '1440px',
|
||||
height: '900px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
expanded: true,
|
||||
},
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
viewport: {
|
||||
viewports: customViewports,
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'light', value: '#ffffff' },
|
||||
{ name: 'steel', value: '#1a1a2e' },
|
||||
],
|
||||
},
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
toc: true, // Enable table of contents in docs
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
// Global providers decorator
|
||||
(Story, context) => {
|
||||
// Apply dark class based on background selection
|
||||
const isDark = context.globals.backgrounds?.value !== '#ffffff';
|
||||
return (
|
||||
<div className={isDark ? 'dark' : ''}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
],
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
|
@ -81,6 +81,7 @@
|
|||
"@lhci/cli": "^0.12.0",
|
||||
"@openapitools/openapi-generator-cli": "^2.27.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
"@storybook/addon-interactions": "^8.6.15",
|
||||
"@storybook/builder-vite": "^8.6.15",
|
||||
|
|
@ -117,6 +118,7 @@
|
|||
"playwright": "^1.58.1",
|
||||
"prettier": "^3.2.5",
|
||||
"storybook": "^8.6.15",
|
||||
"storybook-dark-mode": "^4.0.2",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.3.3",
|
||||
|
|
|
|||
98
apps/web/scripts/storybook-coverage.sh
Executable file
98
apps/web/scripts/storybook-coverage.sh
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
#!/bin/bash
|
||||
# Storybook Coverage Analysis Script
|
||||
# Compares tsx components to stories and generates a coverage report
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WEB_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
SRC_DIR="$WEB_DIR/src"
|
||||
|
||||
echo "📊 Storybook Coverage Analysis"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
|
||||
# Count all component files (excluding tests and stories)
|
||||
COMPONENT_COUNT=$(find "$SRC_DIR" -name "*.tsx" \
|
||||
! -name "*.stories.tsx" \
|
||||
! -name "*.test.tsx" \
|
||||
! -path "*/__tests__/*" \
|
||||
! -path "*/test-utils/*" \
|
||||
! -name "main.tsx" \
|
||||
! -name "index.tsx" \
|
||||
! -name "routes.tsx" \
|
||||
! -name "App.tsx" \
|
||||
| wc -l | tr -d ' ')
|
||||
|
||||
# Count story files
|
||||
STORY_COUNT=$(find "$SRC_DIR" -name "*.stories.tsx" | wc -l | tr -d ' ')
|
||||
|
||||
# Calculate coverage percentage
|
||||
if [ "$COMPONENT_COUNT" -gt 0 ]; then
|
||||
COVERAGE=$((STORY_COUNT * 100 / COMPONENT_COUNT))
|
||||
else
|
||||
COVERAGE=0
|
||||
fi
|
||||
|
||||
echo "📁 Total Components: $COMPONENT_COUNT"
|
||||
echo "📖 Total Stories: $STORY_COUNT"
|
||||
echo "📈 Coverage: ${COVERAGE}%"
|
||||
echo ""
|
||||
|
||||
# Find components without stories
|
||||
echo "🔍 Components WITHOUT stories:"
|
||||
echo "------------------------------"
|
||||
|
||||
# Get list of component basenames
|
||||
COMPONENTS=$(find "$SRC_DIR" -name "*.tsx" \
|
||||
! -name "*.stories.tsx" \
|
||||
! -name "*.test.tsx" \
|
||||
! -path "*/__tests__/*" \
|
||||
! -path "*/test-utils/*" \
|
||||
! -name "main.tsx" \
|
||||
! -name "index.tsx" \
|
||||
! -name "routes.tsx" \
|
||||
! -name "App.tsx" \
|
||||
-exec basename {} .tsx \; | sort | uniq)
|
||||
|
||||
# Get list of story basenames
|
||||
STORIES=$(find "$SRC_DIR" -name "*.stories.tsx" -exec basename {} .stories.tsx \; | sort | uniq)
|
||||
|
||||
# Find components without stories
|
||||
MISSING=0
|
||||
for component in $COMPONENTS; do
|
||||
if ! echo "$STORIES" | grep -qx "$component"; then
|
||||
# Check if it's likely a component (starts with uppercase)
|
||||
if [[ "$component" =~ ^[A-Z] ]]; then
|
||||
echo " - $component"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "📉 Components missing stories: $MISSING"
|
||||
echo ""
|
||||
|
||||
# Coverage by directory
|
||||
echo "📂 Coverage by Directory:"
|
||||
echo "-------------------------"
|
||||
|
||||
for dir in components features; do
|
||||
if [ -d "$SRC_DIR/$dir" ]; then
|
||||
DIR_COMPONENTS=$(find "$SRC_DIR/$dir" -name "*.tsx" \
|
||||
! -name "*.stories.tsx" \
|
||||
! -name "*.test.tsx" \
|
||||
| wc -l | tr -d ' ')
|
||||
DIR_STORIES=$(find "$SRC_DIR/$dir" -name "*.stories.tsx" | wc -l | tr -d ' ')
|
||||
if [ "$DIR_COMPONENTS" -gt 0 ]; then
|
||||
DIR_COVERAGE=$((DIR_STORIES * 100 / DIR_COMPONENTS))
|
||||
else
|
||||
DIR_COVERAGE=0
|
||||
fi
|
||||
printf " %-15s %3d/%3d (%d%%)\n" "$dir:" "$DIR_STORIES" "$DIR_COMPONENTS" "$DIR_COVERAGE"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ Analysis complete!"
|
||||
55
apps/web/src/components/ErrorBoundary.stories.tsx
Normal file
55
apps/web/src/components/ErrorBoundary.stories.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
|
||||
// Component that throws an error for testing
|
||||
const ErrorThrower = ({ shouldThrow }: { shouldThrow?: boolean }) => {
|
||||
if (shouldThrow) {
|
||||
throw new Error('This is a test error for demonstrating ErrorBoundary');
|
||||
}
|
||||
return <div className="p-4 bg-green-100 text-green-800 rounded">Content rendered successfully!</div>;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ErrorBoundary> = {
|
||||
title: 'Components/ErrorBoundary',
|
||||
component: ErrorBoundary,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
// Prevent Storybook from re-rendering on error
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ErrorBoundary>;
|
||||
|
||||
export const NoError: Story = {
|
||||
render: () => (
|
||||
<ErrorBoundary>
|
||||
<ErrorThrower shouldThrow={false} />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => (
|
||||
<ErrorBoundary>
|
||||
<ErrorThrower shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithCustomFallback: Story = {
|
||||
render: () => (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<div className="p-8 text-center bg-amber-100 text-amber-800 rounded-lg">
|
||||
<h2 className="text-xl font-bold mb-2">Custom Fallback</h2>
|
||||
<p>Something went wrong, but we have a custom fallback UI.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ErrorThrower shouldThrow={true} />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
};
|
||||
83
apps/web/src/components/OfflineIndicator.stories.tsx
Normal file
83
apps/web/src/components/OfflineIndicator.stories.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { WifiOff, Loader2, List } from 'lucide-react';
|
||||
|
||||
// Since OfflineIndicator has complex hook dependencies, we create visual representations
|
||||
// of its different states for documentation purposes.
|
||||
|
||||
const OfflineIndicatorMock = ({ variant }: { variant: 'offline' | 'syncing' | 'hidden' }) => {
|
||||
if (variant === 'hidden') {
|
||||
return (
|
||||
<div className="p-4 bg-muted rounded text-center text-muted-foreground">
|
||||
(Indicator is hidden when online with no pending requests)
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'offline') {
|
||||
return (
|
||||
<div className="bg-kodo-red/90 backdrop-blur-sm text-white px-4 py-2.5 text-sm flex items-center justify-center gap-2 shadow-lg border-b border-kodo-red rounded">
|
||||
<WifiOff className="w-4 h-4" />
|
||||
<span>
|
||||
Mode hors ligne
|
||||
<span className="ml-2 font-semibold">- 3 requêtes en attente</span>
|
||||
</span>
|
||||
<button className="ml-3 px-2 py-1 bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors flex items-center gap-1.5 text-xs font-medium">
|
||||
<List className="w-3.5 h-3.5" />
|
||||
View Queue
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-kodo-cyan/90 backdrop-blur-sm text-kodo-void px-4 py-2.5 text-sm flex items-center justify-center gap-2 shadow-lg border-b border-kodo-steel rounded">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>
|
||||
Synchronisation en cours
|
||||
<span className="ml-2 font-semibold">- 2 requêtes restantes</span>
|
||||
</span>
|
||||
<button className="ml-2 px-2 py-1 bg-kodo-red/20 hover:bg-kodo-red/30 rounded border border-kodo-red/30 transition-colors flex items-center gap-1.5 text-xs font-medium">
|
||||
Clear Queue
|
||||
</button>
|
||||
<button className="ml-2 px-2 py-1 bg-kodo-void/20 hover:bg-kodo-void/30 rounded border border-kodo-void/30 transition-colors flex items-center gap-1.5 text-xs font-medium">
|
||||
<List className="w-3.5 h-3.5" />
|
||||
View Queue
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<typeof OfflineIndicatorMock> = {
|
||||
title: 'Components/OfflineIndicator',
|
||||
component: OfflineIndicatorMock,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Displays the current network status and pending offline requests. The actual component uses hooks for online detection and queue management.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof OfflineIndicatorMock>;
|
||||
|
||||
export const Offline: Story = {
|
||||
args: {
|
||||
variant: 'offline',
|
||||
},
|
||||
};
|
||||
|
||||
export const Syncing: Story = {
|
||||
args: {
|
||||
variant: 'syncing',
|
||||
},
|
||||
};
|
||||
|
||||
export const Hidden: Story = {
|
||||
args: {
|
||||
variant: 'hidden',
|
||||
},
|
||||
};
|
||||
67
apps/web/src/components/admin/AdminAuditLogsView.stories.tsx
Normal file
67
apps/web/src/components/admin/AdminAuditLogsView.stories.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminAuditLogsView } from './AdminAuditLogsView';
|
||||
|
||||
/**
|
||||
* AdminAuditLogsView - Logs d'audit
|
||||
*
|
||||
* Table paginée des logs d'audit avec recherche,
|
||||
* filtrage et détails contextuels.
|
||||
*/
|
||||
const meta: Meta<typeof AdminAuditLogsView> = {
|
||||
title: 'Components/Admin/AdminAuditLogsView',
|
||||
component: AdminAuditLogsView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Journal d\'audit immutable avec recherche et pagination.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec logs chargés.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État avec filtres appliqués.
|
||||
*/
|
||||
export const Filtered: Story = {
|
||||
name: 'Filtré',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Logs filtrés par action ou ressource.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide - aucun log trouvé.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Aucun log',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand aucun log ne correspond aux critères.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
53
apps/web/src/components/admin/AdminDashboardView.stories.tsx
Normal file
53
apps/web/src/components/admin/AdminDashboardView.stories.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminDashboardView } from './AdminDashboardView';
|
||||
|
||||
/**
|
||||
* AdminDashboardView - Centre de commande admin
|
||||
*
|
||||
* Vue principale d'administration avec métriques en temps réel,
|
||||
* visualisation du trafic, queue de modération et logs système.
|
||||
*/
|
||||
const meta: Meta<typeof AdminDashboardView> = {
|
||||
title: 'Components/Admin/AdminDashboardView',
|
||||
component: AdminDashboardView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec données chargées.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement initial.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le spinner pendant le chargement des données admin.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminModerationView } from './AdminModerationView';
|
||||
|
||||
/**
|
||||
* AdminModerationView - Queue de modération
|
||||
*
|
||||
* Interface de traitement des signalements avec onglets
|
||||
* pour pending/reviewed/resolved et actions de modération.
|
||||
*/
|
||||
const meta: Meta<typeof AdminModerationView> = {
|
||||
title: 'Components/Admin/AdminModerationView',
|
||||
component: AdminModerationView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Queue de modération avec actions ban, resolve, dismiss et warning.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec rapports en attente.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Vue de la queue avec rapports en attente.
|
||||
*/
|
||||
export const Queue: Story = {
|
||||
name: 'Queue de modération',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Liste des signalements en attente de traitement.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide - tous les rapports traités.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Queue vide',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand tous les signalements ont été traités.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
53
apps/web/src/components/admin/AdminSettingsView.stories.tsx
Normal file
53
apps/web/src/components/admin/AdminSettingsView.stories.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminSettingsView } from './AdminSettingsView';
|
||||
|
||||
/**
|
||||
* AdminSettingsView - Paramètres système
|
||||
*
|
||||
* Interface de configuration système avec feature flags,
|
||||
* mode maintenance et annonces globales.
|
||||
*/
|
||||
const meta: Meta<typeof AdminSettingsView> = {
|
||||
title: 'Components/Admin/AdminSettingsView',
|
||||
component: AdminSettingsView,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Configuration système admin avec feature flags et mode maintenance.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut des paramètres.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de sauvegarde en cours.
|
||||
*/
|
||||
export const Saving: Story = {
|
||||
name: 'Sauvegarde',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Feedback visuel pendant la sauvegarde des paramètres.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
81
apps/web/src/components/admin/AdminUsersView.stories.tsx
Normal file
81
apps/web/src/components/admin/AdminUsersView.stories.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminUsersView } from './AdminUsersView';
|
||||
|
||||
/**
|
||||
* AdminUsersView - Grille d'identités utilisateurs
|
||||
*
|
||||
* Vue de gestion des utilisateurs avec recherche, filtrage,
|
||||
* et actions de modération (ban, suppression, rôles).
|
||||
*/
|
||||
const meta: Meta<typeof AdminUsersView> = {
|
||||
title: 'Components/Admin/AdminUsersView',
|
||||
component: AdminUsersView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Interface de gestion des utilisateurs avec table paginée et actions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec liste d'utilisateurs.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide - aucun utilisateur trouvé.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Aucun utilisateur',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand la recherche ne retourne aucun résultat.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Spinner affiché pendant le chargement des utilisateurs.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État avec filtres appliqués.
|
||||
*/
|
||||
export const WithFilters: Story = {
|
||||
name: 'Avec filtres',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Démonstration des filtres par rôle et statut.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
117
apps/web/src/components/admin/UserTableRow.stories.tsx
Normal file
117
apps/web/src/components/admin/UserTableRow.stories.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { UserTableRow } from './UserTableRow';
|
||||
import { User } from '@/types';
|
||||
|
||||
// Mock user data
|
||||
const createMockUser = (overrides: Partial<User> = {}): User => ({
|
||||
id: 'usr_abc123def456',
|
||||
username: 'demo_artist',
|
||||
email: 'demo@veza.music',
|
||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=demo',
|
||||
status: 'online',
|
||||
role: 'user',
|
||||
roles: ['user', 'artist'],
|
||||
tier: 'Pro',
|
||||
created_at: '2025-01-15',
|
||||
last_login_at: '2026-02-02',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/**
|
||||
* UserTableRow - Ligne de tableau utilisateur
|
||||
*
|
||||
* Composant de ligne affichant les informations d'un utilisateur
|
||||
* avec menu d'actions contextuel.
|
||||
*/
|
||||
const meta: Meta<typeof UserTableRow> = {
|
||||
title: 'Components/Admin/UserTableRow',
|
||||
component: UserTableRow,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Ligne de tableau utilisateur avec avatar, statut, rôles et menu d\'actions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
user: createMockUser(),
|
||||
onBan: fn(),
|
||||
onDelete: fn(),
|
||||
onEditRole: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
user: {
|
||||
description: 'Objet utilisateur à afficher',
|
||||
},
|
||||
onBan: {
|
||||
action: 'onBan',
|
||||
description: 'Callback pour suspendre l\'utilisateur',
|
||||
},
|
||||
onDelete: {
|
||||
action: 'onDelete',
|
||||
description: 'Callback pour supprimer l\'utilisateur',
|
||||
},
|
||||
onEditRole: {
|
||||
action: 'onEditRole',
|
||||
description: 'Callback pour modifier les rôles',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4">
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
<Story />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Utilisateur standard en ligne.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Ligne sélectionnée avec menu ouvert.
|
||||
*/
|
||||
export const Selected: Story = {
|
||||
name: 'Sélectionné',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'État de la ligne quand le menu d\'actions est ouvert.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Utilisateur banni/suspendu.
|
||||
*/
|
||||
export const Banned: Story = {
|
||||
name: 'Suspendu',
|
||||
args: {
|
||||
user: createMockUser({
|
||||
username: 'banned_user',
|
||||
status: 'busy',
|
||||
roles: ['banned'],
|
||||
}),
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affichage d\'un utilisateur suspendu.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { BanUserModal } from './BanUserModal';
|
||||
|
||||
/**
|
||||
* BanUserModal - Modal de suspension d'utilisateur
|
||||
*
|
||||
* Modal permettant de configurer et confirmer la suspension
|
||||
* d'un utilisateur avec raison, durée et notes internes.
|
||||
*/
|
||||
const meta: Meta<typeof BanUserModal> = {
|
||||
title: 'Components/Admin/Modals/BanUserModal',
|
||||
component: BanUserModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Modal de suspension avec options temporaire/permanent et raisons prédéfinies.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
username: 'troublemaker_user',
|
||||
onClose: fn(),
|
||||
onConfirm: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
username: {
|
||||
control: 'text',
|
||||
description: 'Nom d\'utilisateur à suspendre',
|
||||
},
|
||||
onClose: {
|
||||
action: 'onClose',
|
||||
description: 'Callback appelé à la fermeture',
|
||||
},
|
||||
onConfirm: {
|
||||
action: 'onConfirm',
|
||||
description: 'Callback appelé à la confirmation avec (reason, details, duration)',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la modal.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Étape de confirmation avant suspension.
|
||||
*/
|
||||
export const Confirm: Story = {
|
||||
name: 'Confirmation',
|
||||
args: {
|
||||
username: 'spammer_account',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Prêt à confirmer la suspension de l\'utilisateur.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TrackAnalyticsView } from './TrackAnalyticsView';
|
||||
|
||||
const meta: Meta<typeof TrackAnalyticsView> = {
|
||||
title: 'Components/Analytics/TrackAnalyticsView',
|
||||
component: TrackAnalyticsView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
22
apps/web/src/components/commerce/OrderSummary.stories.tsx
Normal file
22
apps/web/src/components/commerce/OrderSummary.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { OrderSummary } from './OrderSummary';
|
||||
|
||||
const meta: Meta<typeof OrderSummary> = {
|
||||
title: 'Components/Commerce/OrderSummary',
|
||||
component: OrderSummary,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 max-w-md">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const WithDiscount: Story = { name: 'Avec réduction' };
|
||||
|
|
@ -7,7 +7,7 @@ const meta = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="h-[400px] p-4 bg-kodo-ink">
|
||||
<div className="w-full max-w-3xl h-[400px] p-4 bg-kodo-background">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { StatCard } from './StatCard';
|
||||
import { Users, DollarSign, Activity } from 'lucide-react';
|
||||
import { Activity, Music, Users, DollarSign } from 'lucide-react';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Dashboard/StatCard',
|
||||
component: StatCard,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-[300px] h-[200px] p-4 bg-kodo-background">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof StatCard>;
|
||||
|
||||
export default meta;
|
||||
|
|
@ -13,32 +20,32 @@ type Story = StoryObj<typeof meta>;
|
|||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Total Users',
|
||||
value: '12,345',
|
||||
icon: <Users className="w-5 h-5 text-kodo-cyan" />,
|
||||
trend: '+12%',
|
||||
label: 'Total Plays',
|
||||
value: '1.2M',
|
||||
icon: <Music className="w-5 h-5 text-white" />,
|
||||
trend: '+12.5',
|
||||
color: 'cyan',
|
||||
sparklineData: [50, 60, 55, 70, 65, 80, 75, 90],
|
||||
sparklineData: [40, 30, 45, 50, 60, 75, 80],
|
||||
},
|
||||
};
|
||||
|
||||
export const NegativeTrend: Story = {
|
||||
args: {
|
||||
label: 'Revenue',
|
||||
value: '$4,200',
|
||||
icon: <DollarSign className="w-5 h-5 text-kodo-red" />,
|
||||
trend: '-5%',
|
||||
value: '$432.50',
|
||||
icon: <DollarSign className="w-5 h-5 text-white" />,
|
||||
trend: '-5.2',
|
||||
color: 'red',
|
||||
sparklineData: [90, 80, 70, 60, 50, 40],
|
||||
sparklineData: [80, 75, 70, 65, 60, 55, 50],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoTrend: Story = {
|
||||
args: {
|
||||
label: 'Active Sessions',
|
||||
value: '342',
|
||||
icon: <Activity className="w-5 h-5 text-kodo-lime" />,
|
||||
color: 'lime',
|
||||
sparklineData: [10, 20, 15, 25, 30, 20, 40],
|
||||
label: 'Followers',
|
||||
value: '5,432',
|
||||
icon: <Users className="w-5 h-5 text-white" />,
|
||||
color: 'magenta',
|
||||
sparklineData: [10, 20, 15, 25, 30, 40, 50],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { APIPlaygroundView } from './APIPlaygroundView';
|
||||
|
||||
const meta: Meta<typeof APIPlaygroundView> = {
|
||||
title: 'Components/Developer/APIPlaygroundView',
|
||||
component: APIPlaygroundView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DeveloperDashboardView } from './DeveloperDashboardView';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
const meta: Meta<typeof DeveloperDashboardView> = {
|
||||
title: 'Components/Developer/DeveloperDashboardView',
|
||||
component: DeveloperDashboardView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
22
apps/web/src/components/developer/WebhooksView.stories.tsx
Normal file
22
apps/web/src/components/developer/WebhooksView.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { WebhooksView } from './WebhooksView';
|
||||
|
||||
const meta: Meta<typeof WebhooksView> = {
|
||||
title: 'Components/Developer/WebhooksView',
|
||||
component: WebhooksView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CreateAPIKeyModal } from './CreateAPIKeyModal';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
const meta: Meta<typeof CreateAPIKeyModal> = {
|
||||
title: 'Components/Developer/Modals/CreateAPIKeyModal',
|
||||
component: CreateAPIKeyModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Created: Story = { name: 'Clé créée' };
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CourseDetailView } from './CourseDetailView';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const meta: Meta<typeof CourseDetailView> = {
|
||||
title: 'Components/Education/CourseDetailView',
|
||||
component: CourseDetailView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Enrolled: Story = { name: 'Inscrit' };
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CourseLearningView } from './CourseLearningView';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const meta: Meta<typeof CourseLearningView> = {
|
||||
title: 'Components/Education/CourseLearningView',
|
||||
component: CourseLearningView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Complete: Story = { name: 'Terminé' };
|
||||
91
apps/web/src/components/filters/FilterBar.stories.tsx
Normal file
91
apps/web/src/components/filters/FilterBar.stories.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { FilterBar } from './FilterBar';
|
||||
|
||||
const mockFilters = {
|
||||
filters: [
|
||||
{
|
||||
id: 'genre',
|
||||
label: 'Genre',
|
||||
type: 'select' as const,
|
||||
options: [
|
||||
{ label: 'All Genres', value: '' },
|
||||
{ label: 'Electronic', value: 'electronic' },
|
||||
{ label: 'Hip Hop', value: 'hiphop' },
|
||||
{ label: 'Rock', value: 'rock' },
|
||||
],
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
id: 'year',
|
||||
label: 'Year',
|
||||
type: 'select' as const,
|
||||
options: [
|
||||
{ label: 'All Years', value: '' },
|
||||
{ label: '2024', value: '2024' },
|
||||
{ label: '2023', value: '2023' },
|
||||
{ label: '2022', value: '2022' },
|
||||
],
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
onFilterChange: () => { },
|
||||
};
|
||||
|
||||
const mockSort = {
|
||||
options: [
|
||||
{ label: 'Newest', value: 'newest' },
|
||||
{ label: 'Oldest', value: 'oldest' },
|
||||
{ label: 'Most Popular', value: 'popular' },
|
||||
{ label: 'A-Z', value: 'alpha' },
|
||||
],
|
||||
value: 'newest',
|
||||
onChange: () => { },
|
||||
};
|
||||
|
||||
const meta: Meta<typeof FilterBar> = {
|
||||
title: 'Components/Filters/FilterBar',
|
||||
component: FilterBar,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FilterBar>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
filters: mockFilters,
|
||||
sort: mockSort,
|
||||
},
|
||||
};
|
||||
|
||||
export const FiltersOnly: Story = {
|
||||
args: {
|
||||
filters: mockFilters,
|
||||
},
|
||||
};
|
||||
|
||||
export const SortOnly: Story = {
|
||||
args: {
|
||||
sort: mockSort,
|
||||
},
|
||||
};
|
||||
|
||||
export const NotCollapsible: Story = {
|
||||
args: {
|
||||
filters: mockFilters,
|
||||
sort: mockSort,
|
||||
collapsible: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const CollapsedByDefault: Story = {
|
||||
args: {
|
||||
filters: mockFilters,
|
||||
sort: mockSort,
|
||||
collapsible: true,
|
||||
defaultOpen: false,
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AchievementCard } from './AchievementCard';
|
||||
|
||||
const meta: Meta<typeof AchievementCard> = {
|
||||
title: 'Components/Gamification/AchievementCard',
|
||||
component: AchievementCard,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 max-w-sm">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Locked: Story = { name: 'Verrouillé' };
|
||||
export const Unlocked: Story = { name: 'Déverrouillé' };
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AchievementsView } from './AchievementsView';
|
||||
|
||||
const meta: Meta<typeof AchievementsView> = {
|
||||
title: 'Components/Gamification/AchievementsView',
|
||||
component: AchievementsView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LeaderboardView } from './LeaderboardView';
|
||||
|
||||
const meta: Meta<typeof LeaderboardView> = {
|
||||
title: 'Components/Gamification/LeaderboardView',
|
||||
component: LeaderboardView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
22
apps/web/src/components/gamification/XPBar.stories.tsx
Normal file
22
apps/web/src/components/gamification/XPBar.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { XPBar } from './XPBar';
|
||||
|
||||
const meta: Meta<typeof XPBar> = {
|
||||
title: 'Components/Gamification/XPBar',
|
||||
component: XPBar,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 w-full">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const LevelUp: Story = { name: 'Montée de niveau' };
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { EquipmentDetailView } from './EquipmentDetailView';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
const meta: Meta<typeof EquipmentDetailView> = {
|
||||
title: 'Components/Inventory/EquipmentDetailView',
|
||||
component: EquipmentDetailView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { PlaylistsView } from './PlaylistsView';
|
||||
|
||||
const meta: Meta<typeof PlaylistsView> = {
|
||||
title: 'Components/Library/Playlists/PlaylistsView',
|
||||
component: PlaylistsView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { QueueView } from './QueueView';
|
||||
|
||||
/**
|
||||
* QueueView - Vue de la file d'attente
|
||||
*
|
||||
* Vue pleine page de la queue avec gestion
|
||||
* complète des tracks.
|
||||
*/
|
||||
const meta: Meta<typeof QueueView> = {
|
||||
title: 'Components/Library/Playlists/QueueView',
|
||||
component: QueueView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Vue complète de la queue avec actions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec tracks.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Vide',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand la queue est vide.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { SaveQueueAsPlaylistModal } from './SaveQueueAsPlaylistModal';
|
||||
|
||||
/**
|
||||
* SaveQueueAsPlaylistModal - Modal de sauvegarde de queue
|
||||
*
|
||||
* Modal permettant de sauvegarder la queue actuelle
|
||||
* comme nouvelle playlist.
|
||||
*/
|
||||
const meta: Meta<typeof SaveQueueAsPlaylistModal> = {
|
||||
title: 'Components/Library/Playlists/SaveQueueAsPlaylistModal',
|
||||
component: SaveQueueAsPlaylistModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Modal de sauvegarde de queue en playlist.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onClose: fn(),
|
||||
onSave: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de sauvegarde en cours.
|
||||
*/
|
||||
export const Saving: Story = {
|
||||
name: 'Sauvegarde',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Spinner pendant la création de la playlist.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sauvegarde réussie.
|
||||
*/
|
||||
export const Success: Story = {
|
||||
name: 'Succès',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message de confirmation après création.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
23
apps/web/src/components/marketplace/LicenceCard.stories.tsx
Normal file
23
apps/web/src/components/marketplace/LicenceCard.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LicenceCard } from './LicenceCard';
|
||||
|
||||
const meta: Meta<typeof LicenceCard> = {
|
||||
title: 'Components/Marketplace/LicenceCard',
|
||||
component: LicenceCard,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 max-w-sm">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Basic: Story = { name: 'Basic' };
|
||||
export const Pro: Story = { name: 'Pro' };
|
||||
export const Exclusive: Story = { name: 'Exclusive' };
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProductDetailView } from './ProductDetailView';
|
||||
|
||||
const meta: Meta<typeof ProductDetailView> = {
|
||||
title: 'Components/Marketplace/ProductDetailView',
|
||||
component: ProductDetailView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
export const OutOfStock: Story = { name: 'Rupture de stock' };
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MonitoringDashboard } from './MonitoringDashboard';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
const meta: Meta<typeof MonitoringDashboard> = {
|
||||
title: 'Components/Monitoring/MonitoringDashboard',
|
||||
component: MonitoringDashboard,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
54
apps/web/src/components/navigation/Breadcrumbs.stories.tsx
Normal file
54
apps/web/src/components/navigation/Breadcrumbs.stories.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Breadcrumbs } from './Breadcrumbs';
|
||||
import { FileText, Music } from 'lucide-react';
|
||||
|
||||
const meta: Meta<typeof Breadcrumbs> = {
|
||||
title: 'Components/Navigation/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Breadcrumbs>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ label: 'Library', href: '/library' },
|
||||
{ label: 'Playlists', href: '/library/playlists' },
|
||||
{ label: 'Summer Vibes' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ label: 'Documents', href: '/docs', icon: <FileText className="h-4 w-4" /> },
|
||||
{ label: 'Music', href: '/docs/music', icon: <Music className="h-4 w-4" /> },
|
||||
{ label: 'Track Details' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutHome: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ label: 'Settings', href: '/settings' },
|
||||
{ label: 'Privacy' },
|
||||
],
|
||||
showHome: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ label: 'Dashboard' },
|
||||
],
|
||||
showHome: false,
|
||||
},
|
||||
};
|
||||
83
apps/web/src/components/navigation/Pagination.stories.tsx
Normal file
83
apps/web/src/components/navigation/Pagination.stories.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Pagination } from './Pagination';
|
||||
import { useState } from 'react';
|
||||
|
||||
const meta: Meta<typeof Pagination> = {
|
||||
title: 'Components/Navigation/Pagination',
|
||||
component: Pagination,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onPageChange: { action: 'onPageChange' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Pagination>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
currentPage: 1,
|
||||
totalPages: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const MiddlePage: Story = {
|
||||
args: {
|
||||
currentPage: 5,
|
||||
totalPages: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const LastPage: Story = {
|
||||
args: {
|
||||
currentPage: 10,
|
||||
totalPages: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const FewPages: Story = {
|
||||
args: {
|
||||
currentPage: 2,
|
||||
totalPages: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFirstLast: Story = {
|
||||
args: {
|
||||
currentPage: 5,
|
||||
totalPages: 20,
|
||||
showFirstLast: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithItemsInfo: Story = {
|
||||
args: {
|
||||
currentPage: 2,
|
||||
totalPages: 10,
|
||||
totalItems: 95,
|
||||
itemsPerPage: 10,
|
||||
showItemsInfo: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Interactive story
|
||||
const InteractivePagination = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
return (
|
||||
<Pagination
|
||||
currentPage={page}
|
||||
totalPages={15}
|
||||
onPageChange={setPage}
|
||||
totalItems={150}
|
||||
itemsPerPage={10}
|
||||
showItemsInfo
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: () => <InteractivePagination />,
|
||||
};
|
||||
71
apps/web/src/components/player/FullPlayer.stories.tsx
Normal file
71
apps/web/src/components/player/FullPlayer.stories.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { FullPlayer } from './FullPlayer';
|
||||
|
||||
/**
|
||||
* FullPlayer - Lecteur plein écran
|
||||
*
|
||||
* Vue immersive du lecteur avec artwork, waveform,
|
||||
* paroles et contrôles avancés.
|
||||
*/
|
||||
const meta: Meta<typeof FullPlayer> = {
|
||||
title: 'Components/Player/FullPlayer',
|
||||
component: FullPlayer,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Lecteur audio plein écran avec artwork et waveform.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onClose: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Avec panneau de queue visible.
|
||||
*/
|
||||
export const WithQueue: Story = {
|
||||
name: 'Avec Queue',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le panneau de queue à côté du player.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Avec paroles affichées.
|
||||
*/
|
||||
export const WithLyrics: Story = {
|
||||
name: 'Avec Paroles',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche les paroles synchronisées.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
67
apps/web/src/components/player/LyricsPanel.stories.tsx
Normal file
67
apps/web/src/components/player/LyricsPanel.stories.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LyricsPanel } from './LyricsPanel';
|
||||
|
||||
/**
|
||||
* LyricsPanel - Panneau de paroles
|
||||
*
|
||||
* Affichage des paroles synchronisées avec
|
||||
* la lecture audio actuelle.
|
||||
*/
|
||||
const meta: Meta<typeof LyricsPanel> = {
|
||||
title: 'Components/Player/LyricsPanel',
|
||||
component: LyricsPanel,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Panneau de paroles synchronisées avec le player.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-md">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec paroles.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Paroles synchronisées en cours.
|
||||
*/
|
||||
export const Synced: Story = {
|
||||
name: 'Synchronisé',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Paroles avec mise en surbrillance synchronisée.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide - aucune parole.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Vide',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand aucune parole n\'est disponible.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
68
apps/web/src/components/player/QueuePanel.stories.tsx
Normal file
68
apps/web/src/components/player/QueuePanel.stories.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { QueuePanel } from './QueuePanel';
|
||||
|
||||
/**
|
||||
* QueuePanel - Panneau de file d'attente
|
||||
*
|
||||
* Liste des tracks en file d'attente avec
|
||||
* réorganisation par drag-and-drop.
|
||||
*/
|
||||
const meta: Meta<typeof QueuePanel> = {
|
||||
title: 'Components/Player/QueuePanel',
|
||||
component: QueuePanel,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Panneau de queue avec drag-and-drop.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-md">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec tracks.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Vide',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand la queue est vide.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de réorganisation.
|
||||
*/
|
||||
export const Reordering: Story = {
|
||||
name: 'Réorganisation',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Démonstration du drag-and-drop.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
23
apps/web/src/components/search/GlobalSearchBar.stories.tsx
Normal file
23
apps/web/src/components/search/GlobalSearchBar.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { GlobalSearchBar } from './GlobalSearchBar';
|
||||
|
||||
const meta: Meta<typeof GlobalSearchBar> = {
|
||||
title: 'Components/Search/GlobalSearchBar',
|
||||
component: GlobalSearchBar,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-8 w-96">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Focused: Story = { name: 'Focus' };
|
||||
export const WithSuggestions: Story = { name: 'Avec suggestions' };
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AccessibilitySettingsView } from './AccessibilitySettingsView';
|
||||
|
||||
/**
|
||||
* AccessibilitySettingsView - Paramètres d'accessibilité
|
||||
*
|
||||
* Section des options d'accessibilité : contraste,
|
||||
* taille de police, réduction des animations.
|
||||
*/
|
||||
const meta: Meta<typeof AccessibilitySettingsView> = {
|
||||
title: 'Components/Settings/Accessibility/AccessibilitySettingsView',
|
||||
component: AccessibilitySettingsView,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Paramètres d\'accessibilité avec contraste et taille de police.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut des paramètres d'accessibilité.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AccountSettings } from './AccountSettings';
|
||||
|
||||
/**
|
||||
* AccountSettings - Paramètres du compte
|
||||
*
|
||||
* Section principale des paramètres de compte avec
|
||||
* gestion email, nom d'utilisateur et suppression.
|
||||
*/
|
||||
const meta: Meta<typeof AccountSettings> = {
|
||||
title: 'Components/Settings/Account/AccountSettings',
|
||||
component: AccountSettings,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Paramètres de compte avec actions email, username, suppression.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ChangeEmailModal } from './ChangeEmailModal';
|
||||
|
||||
/**
|
||||
* ChangeEmailModal - Modal de changement d'email
|
||||
*
|
||||
* Modal permettant à l'utilisateur de changer son adresse email
|
||||
* avec validation et envoi de confirmation.
|
||||
*/
|
||||
const meta: Meta<typeof ChangeEmailModal> = {
|
||||
title: 'Components/Settings/Account/ChangeEmailModal',
|
||||
component: ChangeEmailModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Modal de changement d\'email avec validation.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onClose: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la modal.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État d'envoi en cours.
|
||||
*/
|
||||
export const Sending: Story = {
|
||||
name: 'Envoi',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Spinner affiché pendant l\'envoi de la demande.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État envoyé avec succès.
|
||||
*/
|
||||
export const Sent: Story = {
|
||||
name: 'Envoyé',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message de confirmation après envoi réussi.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { ChangeUsernameModal } from './ChangeUsernameModal';
|
||||
|
||||
/**
|
||||
* ChangeUsernameModal - Modal de changement de nom d'utilisateur
|
||||
*
|
||||
* Modal permettant de changer le nom d'utilisateur avec
|
||||
* vérification de disponibilité en temps réel.
|
||||
*/
|
||||
const meta: Meta<typeof ChangeUsernameModal> = {
|
||||
title: 'Components/Settings/Account/ChangeUsernameModal',
|
||||
component: ChangeUsernameModal,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Modal de changement de nom d\'utilisateur avec vérification.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onClose: fn(),
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la modal.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de vérification de disponibilité.
|
||||
*/
|
||||
export const Checking: Story = {
|
||||
name: 'Vérification',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Spinner pendant la vérification de disponibilité.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État nom disponible.
|
||||
*/
|
||||
export const Available: Story = {
|
||||
name: 'Disponible',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Indicateur vert quand le nom est disponible.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DeleteAccountView } from './DeleteAccountView';
|
||||
|
||||
/**
|
||||
* DeleteAccountView - Vue de suppression de compte
|
||||
*
|
||||
* Section permettant la suppression définitive du compte
|
||||
* avec avertissements et confirmation multiple.
|
||||
*/
|
||||
const meta: Meta<typeof DeleteAccountView> = {
|
||||
title: 'Components/Settings/Account/DeleteAccountView',
|
||||
component: DeleteAccountView,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Zone de suppression de compte avec confirmation obligatoire.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de confirmation.
|
||||
*/
|
||||
export const Confirm: Story = {
|
||||
name: 'Confirmation',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Modal de confirmation avant suppression définitive.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AppearanceSettingsView } from './AppearanceSettingsView';
|
||||
|
||||
/**
|
||||
* AppearanceSettingsView - Paramètres d'apparence
|
||||
*
|
||||
* Section des préférences visuelles : thème, couleur d'accent,
|
||||
* animations et densité de l'interface.
|
||||
*/
|
||||
const meta: Meta<typeof AppearanceSettingsView> = {
|
||||
title: 'Components/Settings/Appearance/AppearanceSettingsView',
|
||||
component: AppearanceSettingsView,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Paramètres d\'apparence avec thème, couleurs et animations.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut des paramètres d'apparence.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DataExportView } from './DataExportView';
|
||||
|
||||
const meta: Meta<typeof DataExportView> = {
|
||||
title: 'Components/Settings/Data/DataExportView',
|
||||
component: DataExportView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Exporting: Story = { name: 'Export en cours' };
|
||||
export const Ready: Story = { name: 'Prêt' };
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SecuritySettings } from './SecuritySettings';
|
||||
|
||||
/**
|
||||
* SecuritySettings - Paramètres de sécurité
|
||||
*
|
||||
* Section des paramètres de sécurité incluant gestion
|
||||
* des mots de passe, 2FA, et sessions actives.
|
||||
*/
|
||||
const meta: Meta<typeof SecuritySettings> = {
|
||||
title: 'Components/Settings/Security/SecuritySettings',
|
||||
component: SecuritySettings,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Paramètres de sécurité avec 2FA, mot de passe et sessions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut des paramètres de sécurité.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de mise à jour en cours.
|
||||
*/
|
||||
export const Updating: Story = {
|
||||
name: 'Mise à jour',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Feedback visuel pendant la mise à jour des paramètres de sécurité.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SessionManagement } from './SessionManagement';
|
||||
|
||||
/**
|
||||
* SessionManagement - Gestion des sessions
|
||||
*
|
||||
* Composant affichant les sessions actives avec
|
||||
* option de déconnexion individuelle ou globale.
|
||||
*/
|
||||
const meta: Meta<typeof SessionManagement> = {
|
||||
title: 'Components/Settings/Security/SessionManagement',
|
||||
component: SessionManagement,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Liste des sessions actives avec actions de déconnexion.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec sessions actives.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État avec plusieurs sessions.
|
||||
*/
|
||||
export const WithSessions: Story = {
|
||||
name: 'Avec sessions',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche plusieurs sessions actives sur différents appareils.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { TwoFactorSetup } from './TwoFactorSetup';
|
||||
|
||||
/**
|
||||
* TwoFactorSetup - Configuration 2FA
|
||||
*
|
||||
* Assistant multi-étapes pour configurer l'authentification
|
||||
* à deux facteurs avec QR code et codes de backup.
|
||||
*/
|
||||
const meta: Meta<typeof TwoFactorSetup> = {
|
||||
title: 'Components/Settings/Security/TwoFactorSetup',
|
||||
component: TwoFactorSetup,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Assistant de configuration 2FA en 3 étapes: choix de méthode, scan QR code, codes de backup.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onBack: fn(),
|
||||
onComplete: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
onBack: {
|
||||
description: 'Callback appelé quand l\'utilisateur revient en arrière',
|
||||
action: 'onBack',
|
||||
},
|
||||
onComplete: {
|
||||
description: 'Callback appelé quand la configuration est terminée',
|
||||
action: 'onComplete',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="max-w-2xl mx-auto p-4 bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Étape 1 - Choix de la méthode d'authentification.
|
||||
*/
|
||||
export const Step1_ChooseMethod: Story = {
|
||||
name: 'Étape 1: Choix de méthode',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Première étape: choisir entre Authenticator App (TOTP) ou SMS.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Étape 2 - Scan du QR code et vérification.
|
||||
*/
|
||||
export const Step2_QRCode: Story = {
|
||||
name: 'Étape 2: QR Code',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Cliquer sur "Authenticator App" pour passer à l'étape 2
|
||||
const totpOption = canvas.getByText(/authenticator app/i);
|
||||
await userEvent.click(totpOption);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Deuxième étape: scanner le QR code et entrer le code de vérification.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Étape 3 - Affichage des codes de backup.
|
||||
*/
|
||||
export const Step3_BackupCodes: Story = {
|
||||
name: 'Étape 3: Codes de backup',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Naviguer vers l'étape 3 (simulation)
|
||||
// Note: Cela nécessite que l'API mock retourne des données valides
|
||||
const totpOption = canvas.getByText(/authenticator app/i);
|
||||
await userEvent.click(totpOption);
|
||||
|
||||
// Attendre le QR code et entrer un code
|
||||
// Cette simulation est limitée sans mocks complets
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Troisième étape: sauvegarder les codes de backup avec options copier/télécharger.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
24
apps/web/src/components/social/CreatePostModal.stories.tsx
Normal file
24
apps/web/src/components/social/CreatePostModal.stories.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { CreatePostModal } from './CreatePostModal';
|
||||
|
||||
const meta: Meta<typeof CreatePostModal> = {
|
||||
title: 'Components/Social/CreatePostModal',
|
||||
component: CreatePostModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Posting: Story = { name: 'Envoi' };
|
||||
21
apps/web/src/components/social/ExploreView.stories.tsx
Normal file
21
apps/web/src/components/social/ExploreView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ExploreView } from './ExploreView';
|
||||
|
||||
const meta: Meta<typeof ExploreView> = {
|
||||
title: 'Components/Social/ExploreView',
|
||||
component: ExploreView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
23
apps/web/src/components/social/FeedView.stories.tsx
Normal file
23
apps/web/src/components/social/FeedView.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { FeedView } from './FeedView';
|
||||
|
||||
const meta: Meta<typeof FeedView> = {
|
||||
title: 'Components/Social/FeedView',
|
||||
component: FeedView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ConnectionsView } from './ConnectionsView';
|
||||
|
||||
const meta: Meta<typeof ConnectionsView> = {
|
||||
title: 'Components/Social/ConnectionsView',
|
||||
component: ConnectionsView,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
23
apps/web/src/components/studio/GoLiveView.stories.tsx
Normal file
23
apps/web/src/components/studio/GoLiveView.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { GoLiveView } from './GoLiveView';
|
||||
|
||||
const meta: Meta<typeof GoLiveView> = {
|
||||
title: 'Components/Studio/GoLiveView',
|
||||
component: GoLiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Setup: Story = { name: 'Configuration' };
|
||||
export const Live: Story = { name: 'En direct' };
|
||||
export const Ended: Story = { name: 'Terminé' };
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CreateProjectModal } from './CreateProjectModal';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
const meta: Meta<typeof CreateProjectModal> = {
|
||||
title: 'Components/Studio/Projects/CreateProjectModal',
|
||||
component: CreateProjectModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Creating: Story = { name: 'Création' };
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProjectDetailView } from './ProjectDetailView';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const meta: Meta<typeof ProjectDetailView> = {
|
||||
title: 'Components/Studio/Projects/ProjectDetailView',
|
||||
component: ProjectDetailView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
25
apps/web/src/components/upload/BulkUploadModal.stories.tsx
Normal file
25
apps/web/src/components/upload/BulkUploadModal.stories.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { BulkUploadModal } from './BulkUploadModal';
|
||||
|
||||
const meta: Meta<typeof BulkUploadModal> = {
|
||||
title: 'Components/Upload/BulkUploadModal',
|
||||
component: BulkUploadModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Uploading: Story = { name: 'Upload en cours' };
|
||||
export const Complete: Story = { name: 'Terminé' };
|
||||
23
apps/web/src/components/upload/FileUploadZone.stories.tsx
Normal file
23
apps/web/src/components/upload/FileUploadZone.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { FileUploadZone } from './FileUploadZone';
|
||||
|
||||
const meta: Meta<typeof FileUploadZone> = {
|
||||
title: 'Components/Upload/FileUploadZone',
|
||||
component: FileUploadZone,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-8 w-[500px]">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Dragging: Story = { name: 'Drag en cours' };
|
||||
export const Processing: Story = { name: 'Traitement' };
|
||||
23
apps/web/src/components/upload/UploadProgressBar.stories.tsx
Normal file
23
apps/web/src/components/upload/UploadProgressBar.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { UploadProgressBar } from './UploadProgressBar';
|
||||
|
||||
const meta: Meta<typeof UploadProgressBar> = {
|
||||
title: 'Components/Upload/UploadProgressBar',
|
||||
component: UploadProgressBar,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-8 w-96">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Complete: Story = { name: 'Terminé' };
|
||||
export const Error: Story = { name: 'Erreur' };
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { CoverArtUploadModal } from './CoverArtUploadModal';
|
||||
|
||||
const meta: Meta<typeof CoverArtUploadModal> = {
|
||||
title: 'Components/Upload/CoverArtUploadModal',
|
||||
component: CoverArtUploadModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Uploading: Story = { name: 'Upload' };
|
||||
export const Preview: Story = { name: 'Prévisualisation' };
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LyricsEditorModal } from './LyricsEditorModal';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
const meta: Meta<typeof LyricsEditorModal> = {
|
||||
title: 'Components/Upload/Metadata/LyricsEditorModal',
|
||||
component: LyricsEditorModal,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Syncing: Story = { name: 'Synchronisation' };
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MetadataEditor } from './MetadataEditor';
|
||||
|
||||
const meta: Meta<typeof MetadataEditor> = {
|
||||
title: 'Components/Upload/MetadataEditor',
|
||||
component: MetadataEditor,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen max-w-2xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const WithData: Story = { name: 'Avec données' };
|
||||
export const Saving: Story = { name: 'Sauvegarde' };
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MetadataForm } from './MetadataForm';
|
||||
|
||||
const meta: Meta<typeof MetadataForm> = {
|
||||
title: 'Components/Upload/Metadata/MetadataForm',
|
||||
component: MetadataForm,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 max-w-2xl">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const WithErrors: Story = { name: 'Avec erreurs' };
|
||||
49
apps/web/src/components/views/AdminView.stories.tsx
Normal file
49
apps/web/src/components/views/AdminView.stories.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminView } from './AdminView';
|
||||
|
||||
/**
|
||||
* AdminView - Vue principale d'administration
|
||||
*
|
||||
* Layout avec sidebar de navigation et zone de contenu
|
||||
* pour basculer entre les différentes vues admin.
|
||||
*/
|
||||
const meta: Meta<typeof AdminView> = {
|
||||
title: 'Components/Views/AdminView',
|
||||
component: AdminView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Vue conteneur admin avec navigation sidebar vers sous-vues.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
currentSubView: {
|
||||
control: 'select',
|
||||
options: ['dashboard', 'users', 'moderation', 'audit', 'settings'],
|
||||
description: 'Sous-vue à afficher par défaut',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Vue dashboard par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut (Dashboard)',
|
||||
args: {
|
||||
currentSubView: 'dashboard',
|
||||
},
|
||||
};
|
||||
22
apps/web/src/components/views/AnalyticsView.stories.tsx
Normal file
22
apps/web/src/components/views/AnalyticsView.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AnalyticsView } from './AnalyticsView';
|
||||
|
||||
const meta: Meta<typeof AnalyticsView> = {
|
||||
title: 'Components/Views/AnalyticsView',
|
||||
component: AnalyticsView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
21
apps/web/src/components/views/ChatView.stories.tsx
Normal file
21
apps/web/src/components/views/ChatView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ChatView } from './ChatView';
|
||||
|
||||
const meta: Meta<typeof ChatView> = {
|
||||
title: 'Components/Views/ChatView',
|
||||
component: ChatView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
22
apps/web/src/components/views/DiscoverView.stories.tsx
Normal file
22
apps/web/src/components/views/DiscoverView.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { DiscoverView } from './DiscoverView';
|
||||
|
||||
const meta: Meta<typeof DiscoverView> = {
|
||||
title: 'Components/Views/DiscoverView',
|
||||
component: DiscoverView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
21
apps/web/src/components/views/EducationView.stories.tsx
Normal file
21
apps/web/src/components/views/EducationView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { EducationView } from './EducationView';
|
||||
|
||||
const meta: Meta<typeof EducationView> = {
|
||||
title: 'Components/Views/EducationView',
|
||||
component: EducationView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
24
apps/web/src/components/views/GearView.stories.tsx
Normal file
24
apps/web/src/components/views/GearView.stories.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { GearView } from './GearView';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
const meta: Meta<typeof GearView> = {
|
||||
title: 'Components/Views/GearView',
|
||||
component: GearView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
21
apps/web/src/components/views/LiveView.stories.tsx
Normal file
21
apps/web/src/components/views/LiveView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LiveView } from './LiveView';
|
||||
|
||||
const meta: Meta<typeof LiveView> = {
|
||||
title: 'Components/Views/LiveView',
|
||||
component: LiveView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
21
apps/web/src/components/views/NotificationsView.stories.tsx
Normal file
21
apps/web/src/components/views/NotificationsView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { NotificationsView } from './NotificationsView';
|
||||
|
||||
const meta: Meta<typeof NotificationsView> = {
|
||||
title: 'Components/Views/NotificationsView',
|
||||
component: NotificationsView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
22
apps/web/src/components/views/ProfileView.stories.tsx
Normal file
22
apps/web/src/components/views/ProfileView.stories.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProfileView } from './ProfileView';
|
||||
|
||||
const meta: Meta<typeof ProfileView> = {
|
||||
title: 'Components/Views/ProfileView',
|
||||
component: ProfileView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
23
apps/web/src/components/views/PurchasesView.stories.tsx
Normal file
23
apps/web/src/components/views/PurchasesView.stories.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { PurchasesView } from './PurchasesView';
|
||||
|
||||
const meta: Meta<typeof PurchasesView> = {
|
||||
title: 'Components/Views/PurchasesView',
|
||||
component: PurchasesView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
49
apps/web/src/components/views/SettingsView.stories.tsx
Normal file
49
apps/web/src/components/views/SettingsView.stories.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SettingsView } from './SettingsView';
|
||||
|
||||
/**
|
||||
* SettingsView - Vue principale des paramètres
|
||||
*
|
||||
* Interface à onglets pour naviguer entre les différentes
|
||||
* sections de paramètres (profil, compte, apparence, sécurité, etc.)
|
||||
*/
|
||||
const meta: Meta<typeof SettingsView> = {
|
||||
title: 'Components/Views/SettingsView',
|
||||
component: SettingsView,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Vue avec navigation par onglets vers toutes les sections de paramètres.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
initialTab: {
|
||||
control: 'select',
|
||||
options: ['profile', 'account', 'appearance', 'accessibility', 'security', 'integrations', 'cloud', 'backups', 'data', 'audio', 'notifications'],
|
||||
description: 'Onglet à afficher par défaut',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec onglet Profile.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut (Profile)',
|
||||
args: {
|
||||
initialTab: 'profile',
|
||||
},
|
||||
};
|
||||
21
apps/web/src/components/views/SocialView.stories.tsx
Normal file
21
apps/web/src/components/views/SocialView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { SocialView } from './SocialView';
|
||||
|
||||
const meta: Meta<typeof SocialView> = {
|
||||
title: 'Components/Views/SocialView',
|
||||
component: SocialView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
21
apps/web/src/components/views/StudioView.stories.tsx
Normal file
21
apps/web/src/components/views/StudioView.stories.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { StudioView } from './StudioView';
|
||||
|
||||
const meta: Meta<typeof StudioView> = {
|
||||
title: 'Components/Views/StudioView',
|
||||
component: StudioView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
24
apps/web/src/components/views/UploadView.stories.tsx
Normal file
24
apps/web/src/components/views/UploadView.stories.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { UploadView } from './UploadView';
|
||||
|
||||
const meta: Meta<typeof UploadView> = {
|
||||
title: 'Components/Views/UploadView',
|
||||
component: UploadView,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Uploading: Story = { name: 'Upload en cours' };
|
||||
export const Complete: Story = { name: 'Terminé' };
|
||||
export const Error: Story = { name: 'Erreur' };
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CartProvider, useCart } from './CartContext';
|
||||
import { ReactNode } from 'react';
|
||||
import { Product, ProductLicense } from '@/types';
|
||||
import { ToastProvider } from './ToastContext';
|
||||
|
||||
// Mock useToast via ToastProvider
|
||||
const mockAddToast = vi.fn();
|
||||
|
||||
vi.mock('./ToastContext', async () => {
|
||||
const actual = await vi.importActual('./ToastContext');
|
||||
return {
|
||||
...actual,
|
||||
ToastProvider: ({ children }: { children: ReactNode }) => children,
|
||||
useToast: () => ({
|
||||
addToast: mockAddToast,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const mockProduct: Product = {
|
||||
id: '1',
|
||||
title: 'Test Product',
|
||||
price: 9.99,
|
||||
currency: 'USD',
|
||||
type: 'track',
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<CartProvider>{children}</CartProvider>
|
||||
);
|
||||
|
||||
describe('CartContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should provide cart context', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current).toHaveProperty('cart');
|
||||
expect(result.current).toHaveProperty('addToCart');
|
||||
expect(result.current).toHaveProperty('removeFromCart');
|
||||
expect(result.current).toHaveProperty('clearCart');
|
||||
});
|
||||
|
||||
it('should have empty cart initially', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
expect(result.current.cart).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add product to cart', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.addToCart(mockProduct);
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(1);
|
||||
expect(result.current.cart[0].id).toBe(mockProduct.id);
|
||||
expect(result.current.cart[0].title).toBe(mockProduct.title);
|
||||
});
|
||||
|
||||
it('should remove product from cart', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.addToCart(mockProduct);
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(1);
|
||||
|
||||
const cartId = result.current.cart[0].cartId;
|
||||
|
||||
act(() => {
|
||||
result.current.removeFromCart(cartId);
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should clear cart', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.addToCart(mockProduct);
|
||||
result.current.addToCart({ ...mockProduct, id: '2' });
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
result.current.clearCart();
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should add product with license', () => {
|
||||
const { result } = renderHook(() => useCart(), { wrapper });
|
||||
|
||||
const license: ProductLicense = {
|
||||
id: 'std',
|
||||
name: 'Standard',
|
||||
price: 9.99,
|
||||
features: ['Royalty Free'],
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.addToCart(mockProduct, license);
|
||||
});
|
||||
|
||||
expect(result.current.cart).toHaveLength(1);
|
||||
expect(result.current.cart[0].selectedLicense).toEqual(license);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { CartItem, Product, ProductLicense } from '../types';
|
||||
import { useToast } from '@/components/feedback/ToastProvider';
|
||||
|
||||
interface CartContextType {
|
||||
cart: CartItem[];
|
||||
addToCart: (product: Product, license?: ProductLicense) => void;
|
||||
removeFromCart: (cartId: string) => void;
|
||||
clearCart: () => void;
|
||||
cartTotal: number;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
const CartContext = createContext<CartContextType | undefined>(undefined);
|
||||
|
||||
export const useCart = () => {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) {
|
||||
throw new Error('useCart must be used within a CartProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { addToast } = useToast();
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
|
||||
const addToCart = (product: Product, license?: ProductLicense) => {
|
||||
// Generate a unique ID for the cart item (productID + licenseID)
|
||||
// This allows adding the same product with different licenses
|
||||
const licenseId = license ? license.id : 'standard';
|
||||
const existingItem = cart.find(
|
||||
(item) =>
|
||||
item.id === product.id && item.selectedLicense?.id === license?.id,
|
||||
);
|
||||
|
||||
if (existingItem) {
|
||||
addToast('Item already in cart', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem: CartItem = {
|
||||
...product,
|
||||
cartId: `${product.id}-${licenseId}-${Date.now()}`,
|
||||
selectedLicense: license,
|
||||
};
|
||||
|
||||
setCart((prev) => [...prev, newItem]);
|
||||
addToast(`${product.title} added to cart`, 'success');
|
||||
};
|
||||
|
||||
const removeFromCart = (cartId: string) => {
|
||||
setCart((prev) => prev.filter((item) => item.cartId !== cartId));
|
||||
};
|
||||
|
||||
const clearCart = () => {
|
||||
setCart([]);
|
||||
};
|
||||
|
||||
const cartTotal = cart.reduce((acc, item) => {
|
||||
const price = item.selectedLicense
|
||||
? item.selectedLicense.price
|
||||
: item.price;
|
||||
return acc + price;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<CartContext.Provider
|
||||
value={{
|
||||
cart,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
cartTotal,
|
||||
itemCount: cart.length,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AuthErrorMessage } from './AuthErrorMessage';
|
||||
|
||||
/**
|
||||
* AuthErrorMessage - Affichage d'erreurs d'authentification
|
||||
*
|
||||
* Composant wrapper pour afficher les erreurs d'authentification
|
||||
* de manière cohérente avec le design system.
|
||||
*
|
||||
* @deprecated Considérez d'utiliser ErrorDisplay directement pour les nouveaux développements.
|
||||
*/
|
||||
const meta: Meta<typeof AuthErrorMessage> = {
|
||||
title: 'Features/Auth/AuthErrorMessage',
|
||||
component: AuthErrorMessage,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Composant d\'affichage des erreurs d\'authentification. Utilise ErrorDisplay en interne.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
message: {
|
||||
control: 'text',
|
||||
description: 'Message d\'erreur à afficher',
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
description: 'Classes CSS additionnelles',
|
||||
},
|
||||
id: {
|
||||
control: 'text',
|
||||
description: 'ID unique pour l\'accessibilité',
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="w-[400px] p-4">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Message d'erreur par défaut.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
args: {
|
||||
message: 'Une erreur est survenue lors de l\'authentification.',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Erreur de réseau.
|
||||
*/
|
||||
export const Network: Story = {
|
||||
name: 'Erreur réseau',
|
||||
args: {
|
||||
message: 'Impossible de se connecter au serveur. Vérifiez votre connexion internet.',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Erreur de validation.
|
||||
*/
|
||||
export const Validation: Story = {
|
||||
name: 'Erreur de validation',
|
||||
args: {
|
||||
message: 'Email ou mot de passe incorrect.',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Message vide - ne rend rien.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Vide (pas de rendu)',
|
||||
args: {
|
||||
message: '',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Quand le message est vide, le composant ne rend rien (retourne null).',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Avec ID personnalisé pour l'accessibilité.
|
||||
*/
|
||||
export const WithCustomId: Story = {
|
||||
name: 'Avec ID personnalisé',
|
||||
args: {
|
||||
message: 'Session expirée. Veuillez vous reconnecter.',
|
||||
id: 'login-error-message',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'L\'ID peut être utilisé pour lier le message d\'erreur à un champ de formulaire via aria-describedby.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
40
apps/web/src/features/auth/components/AuthLayout.stories.tsx
Normal file
40
apps/web/src/features/auth/components/AuthLayout.stories.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AuthLayout } from './AuthLayout';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const meta: Meta<typeof AuthLayout> = {
|
||||
title: 'Features/Auth/AuthLayout',
|
||||
component: AuthLayout,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AuthLayout>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Welcome Back',
|
||||
subtitle: 'Please sign in to continue',
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
<div className="h-10 bg-gray-100 rounded w-full border border-gray-200 flex items-center px-3 text-gray-400">Email</div>
|
||||
<div className="h-10 bg-gray-100 rounded w-full border border-gray-200 flex items-center px-3 text-gray-400">Password</div>
|
||||
<div className="h-10 bg-primary rounded w-full flex items-center justify-center text-white">Sign In</div>
|
||||
</div>
|
||||
),
|
||||
footerLinks: [
|
||||
{ label: 'Forgot Password?', to: '/forgot-password' },
|
||||
{ label: "Don't have an account? Sign up", to: '/register' }
|
||||
]
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { ForgotPasswordForm } from './ForgotPasswordForm';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* ForgotPasswordForm - Formulaire de récupération de mot de passe
|
||||
*
|
||||
* Composant de formulaire autonome pour demander un lien de
|
||||
* réinitialisation de mot de passe.
|
||||
*/
|
||||
const meta: Meta<typeof ForgotPasswordForm> = {
|
||||
title: 'Features/Auth/ForgotPasswordForm',
|
||||
component: ForgotPasswordForm,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Formulaire de demande de réinitialisation de mot de passe.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<div className="w-[400px]">
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut du formulaire.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement pendant l'envoi.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir l'email
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
await userEvent.type(emailInput, 'user@veza.music');
|
||||
|
||||
// Soumettre pour voir le loader
|
||||
const submitButton = canvas.getByRole('button', { name: /envoyer/i });
|
||||
await userEvent.click(submitButton);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Montre le spinner pendant l\'envoi de la demande.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de succès après envoi.
|
||||
*/
|
||||
export const Success: Story = {
|
||||
name: 'Succès - Email envoyé',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message de confirmation après envoi réussi.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { TwoFactorVerify } from './TwoFactorVerify';
|
||||
|
||||
/**
|
||||
* TwoFactorVerify - Vérification 2FA
|
||||
*
|
||||
* Composant pour saisir le code de vérification 2FA
|
||||
* lors de la connexion avec authentification à deux facteurs.
|
||||
*/
|
||||
const meta: Meta<typeof TwoFactorVerify> = {
|
||||
title: 'Features/Auth/TwoFactorVerify',
|
||||
component: TwoFactorVerify,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Formulaire de vérification 2FA avec support des codes de backup.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
onSuccess: fn(),
|
||||
onCancel: fn(),
|
||||
},
|
||||
argTypes: {
|
||||
onSuccess: {
|
||||
description: 'Callback appelé avec le code après vérification réussie',
|
||||
action: 'onSuccess',
|
||||
},
|
||||
onCancel: {
|
||||
description: 'Callback appelé quand l\'utilisateur annule',
|
||||
action: 'onCancel',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut - saisie du code TOTP.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État avec erreur de vérification.
|
||||
*/
|
||||
export const WithError: Story = {
|
||||
name: 'Avec erreur',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Saisir un code invalide
|
||||
const codeInput = canvas.getByLabelText(/verification code/i);
|
||||
await userEvent.type(codeInput, '123456');
|
||||
|
||||
// Cliquer sur Verify
|
||||
const verifyButton = canvas.getByRole('button', { name: /verify/i });
|
||||
await userEvent.click(verifyButton);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message d\'erreur après un code invalide.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement pendant la vérification.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Saisir un code
|
||||
const codeInput = canvas.getByLabelText(/verification code/i);
|
||||
await userEvent.type(codeInput, '123456');
|
||||
|
||||
// Cliquer sur Verify pour voir le spinner
|
||||
const verifyButton = canvas.getByRole('button', { name: /verify/i });
|
||||
await userEvent.click(verifyButton);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Montre le spinner pendant la vérification.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mode backup code.
|
||||
*/
|
||||
export const BackupCode: Story = {
|
||||
name: 'Code de backup',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Cliquer sur "Use a backup code"
|
||||
const backupLink = canvas.getByRole('button', { name: /use a backup code/i });
|
||||
await userEvent.click(backupLink);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Interface pour utiliser un code de backup au lieu du TOTP.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
100
apps/web/src/features/auth/pages/ForgotPasswordPage.stories.tsx
Normal file
100
apps/web/src/features/auth/pages/ForgotPasswordPage.stories.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { ForgotPasswordPage } from './ForgotPasswordPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Create a mocked QueryClient for stories
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* ForgotPasswordPage - Page de récupération de mot de passe
|
||||
*
|
||||
* Permet aux utilisateurs de demander un lien de réinitialisation
|
||||
* de leur mot de passe par email.
|
||||
*/
|
||||
const meta: Meta<typeof ForgotPasswordPage> = {
|
||||
title: 'Pages/Auth/ForgotPasswordPage',
|
||||
component: ForgotPasswordPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page de demande de réinitialisation de mot de passe.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut - formulaire de demande.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État après envoi réussi du lien.
|
||||
*/
|
||||
export const Sent: Story = {
|
||||
name: 'Email envoyé',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir l'email
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
await userEvent.type(emailInput, 'user@veza.music');
|
||||
|
||||
// Soumettre le formulaire
|
||||
const submitButton = canvas.getByRole('button', { name: /envoyer/i });
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Le message de succès s'affichera après la mutation
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message de confirmation après envoi du lien.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État d'erreur - email non trouvé ou erreur serveur.
|
||||
*/
|
||||
export const WithError: Story = {
|
||||
name: 'Avec erreur',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Saisir un email invalide
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
await userEvent.type(emailInput, 'invalid-email');
|
||||
|
||||
// Déclencher la validation
|
||||
await userEvent.tab();
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Montre les erreurs de validation du formulaire.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
109
apps/web/src/features/auth/pages/LoginPage.stories.tsx
Normal file
109
apps/web/src/features/auth/pages/LoginPage.stories.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent, expect, fn } from '@storybook/test';
|
||||
import { LoginPage } from './LoginPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Create a mocked QueryClient for stories
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* LoginPage - Page de connexion
|
||||
*
|
||||
* Page complète de connexion avec formulaire email/mot de passe,
|
||||
* boutons OAuth, et liens vers l'inscription et la récupération de mot de passe.
|
||||
*/
|
||||
const meta: Meta<typeof LoginPage> = {
|
||||
title: 'Pages/Auth/LoginPage',
|
||||
component: LoginPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page de connexion avec authentification par email/mot de passe et OAuth.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la page de connexion.
|
||||
* Formulaire vide prêt à être rempli.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Simulation d'une erreur d'authentification.
|
||||
* Montre le message d'erreur après une tentative de connexion échouée.
|
||||
*/
|
||||
export const WithError: Story = {
|
||||
name: 'Avec erreur',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir les champs avec des données invalides
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
const passwordInput = canvas.getByLabelText(/password/i);
|
||||
|
||||
await userEvent.type(emailInput, 'test@example.com');
|
||||
await userEvent.type(passwordInput, 'wrongpassword');
|
||||
|
||||
// Soumettre le formulaire
|
||||
const submitButton = canvas.getByRole('button', { name: /sign in/i });
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Note: Dans Storybook, l'erreur viendra du mock de useLogin
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Démontre l\'affichage d\'un message d\'erreur après une tentative de connexion échouée.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement pendant la soumission du formulaire.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir le formulaire
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
const passwordInput = canvas.getByLabelText(/password/i);
|
||||
|
||||
await userEvent.type(emailInput, 'user@veza.music');
|
||||
await userEvent.type(passwordInput, 'SecurePass123!');
|
||||
|
||||
// Soumettre immédiatement pour montrer l'état de chargement
|
||||
const submitButton = canvas.getByRole('button', { name: /sign in/i });
|
||||
await userEvent.click(submitButton);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Montre l\'état de chargement avec le spinner pendant la soumission.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
115
apps/web/src/features/auth/pages/RegisterPage.stories.tsx
Normal file
115
apps/web/src/features/auth/pages/RegisterPage.stories.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { RegisterPage } from './RegisterPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Create a mocked QueryClient for stories
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* RegisterPage - Page d'inscription
|
||||
*
|
||||
* Page complète d'inscription avec formulaire de création de compte,
|
||||
* validation en temps réel, indicateur de force du mot de passe,
|
||||
* et vérification de disponibilité du nom d'utilisateur.
|
||||
*/
|
||||
const meta: Meta<typeof RegisterPage> = {
|
||||
title: 'Pages/Auth/RegisterPage',
|
||||
component: RegisterPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page d\'inscription avec validation complète et vérification email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la page d'inscription.
|
||||
* Formulaire vide prêt à être rempli.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Simulation d'une erreur d'inscription.
|
||||
* Par exemple, nom d'utilisateur déjà pris.
|
||||
*/
|
||||
export const WithError: Story = {
|
||||
name: 'Avec erreur',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir les champs
|
||||
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
|
||||
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
|
||||
|
||||
await userEvent.type(usernameInput, 'existinguser');
|
||||
await userEvent.type(emailInput, 'new@veza.music');
|
||||
await userEvent.type(passwordInput, 'SecurePass123!@#');
|
||||
await userEvent.type(confirmPasswordInput, 'SecurePass123!@#');
|
||||
|
||||
// L'erreur de disponibilité sera affichée via le hook useUsernameAvailability
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Démontre l\'affichage d\'erreurs de validation en temps réel.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de succès après inscription.
|
||||
* Affiche le message de vérification email.
|
||||
*/
|
||||
export const Success: Story = {
|
||||
name: 'Succès - Vérification email',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir tous les champs avec des données valides
|
||||
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
|
||||
const emailInput = canvas.getByLabelText(/email/i);
|
||||
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
|
||||
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
|
||||
const termsCheckbox = canvas.getByRole('checkbox');
|
||||
|
||||
await userEvent.type(usernameInput, 'newartist');
|
||||
await userEvent.type(emailInput, 'artist@veza.music');
|
||||
await userEvent.type(passwordInput, 'SuperSecure123!@#');
|
||||
await userEvent.type(confirmPasswordInput, 'SuperSecure123!@#');
|
||||
await userEvent.click(termsCheckbox);
|
||||
|
||||
// Le succès sera affiché après la mutation réussie
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Montre le message de succès avec instructions de vérification email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
118
apps/web/src/features/auth/pages/ResetPasswordPage.stories.tsx
Normal file
118
apps/web/src/features/auth/pages/ResetPasswordPage.stories.tsx
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { ResetPasswordPage } from './ResetPasswordPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* ResetPasswordPage - Page de réinitialisation de mot de passe
|
||||
*
|
||||
* Permet aux utilisateurs de définir un nouveau mot de passe
|
||||
* après avoir cliqué sur le lien envoyé par email.
|
||||
*/
|
||||
const meta: Meta<typeof ResetPasswordPage> = {
|
||||
title: 'Pages/Auth/ResetPasswordPage',
|
||||
component: ResetPasswordPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page de réinitialisation de mot de passe avec indicateur de force.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut avec un token valide.
|
||||
* Affiche le formulaire de nouveau mot de passe.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut (token valide)',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/reset-password?token=valid-token-123']}>
|
||||
<Routes>
|
||||
<Route path="/reset-password" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* État de succès après réinitialisation.
|
||||
*/
|
||||
export const Success: Story = {
|
||||
name: 'Succès',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/reset-password?token=valid-token-123']}>
|
||||
<Routes>
|
||||
<Route path="/reset-password" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Remplir les champs
|
||||
const passwordInput = canvas.getByLabelText(/nouveau mot de passe/i);
|
||||
const confirmInput = canvas.getByLabelText(/confirmer le mot de passe/i);
|
||||
|
||||
await userEvent.type(passwordInput, 'NewSecurePass123!@#');
|
||||
await userEvent.type(confirmInput, 'NewSecurePass123!@#');
|
||||
|
||||
// Soumettre
|
||||
const submitButton = canvas.getByRole('button', { name: /réinitialiser/i });
|
||||
await userEvent.click(submitButton);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message de succès après réinitialisation du mot de passe.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État avec token invalide ou expiré.
|
||||
*/
|
||||
export const InvalidToken: Story = {
|
||||
name: 'Token invalide',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/reset-password']}>
|
||||
<Routes>
|
||||
<Route path="/reset-password" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message d\'erreur quand le token est manquant ou invalide.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
109
apps/web/src/features/auth/pages/VerifyEmailPage.stories.tsx
Normal file
109
apps/web/src/features/auth/pages/VerifyEmailPage.stories.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { VerifyEmailPage } from './VerifyEmailPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* VerifyEmailPage - Page de vérification d'email
|
||||
*
|
||||
* Vérifie automatiquement l'email de l'utilisateur via le token
|
||||
* reçu par email et affiche le statut approprié.
|
||||
*/
|
||||
const meta: Meta<typeof VerifyEmailPage> = {
|
||||
title: 'Pages/Auth/VerifyEmailPage',
|
||||
component: VerifyEmailPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page de vérification d\'email avec états de chargement, succès et erreur.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État de vérification en cours (spinner).
|
||||
*/
|
||||
export const Pending: Story = {
|
||||
name: 'Vérification en cours',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/verify-email?token=verification-token-123']}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche un spinner pendant la vérification de l\'email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État de succès - email vérifié.
|
||||
*/
|
||||
export const Verified: Story = {
|
||||
name: 'Email vérifié',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/verify-email?token=valid-verified-token']}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche le message de succès après vérification.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État d'erreur - token invalide ou expiré.
|
||||
*/
|
||||
export const Error: Story = {
|
||||
name: 'Erreur de vérification',
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/verify-email']}>
|
||||
<Routes>
|
||||
<Route path="/verify-email" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche l\'erreur avec options de réessayer et renvoyer l\'email.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
import { CreateRoomDialog } from './CreateRoomDialog';
|
||||
|
||||
const meta: Meta<typeof CreateRoomDialog> = {
|
||||
title: 'Features/Chat/CreateRoomDialog',
|
||||
component: CreateRoomDialog,
|
||||
parameters: { layout: 'centered' },
|
||||
tags: ['autodocs'],
|
||||
args: { onClose: fn() },
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Creating: Story = { name: 'Création' };
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MessageSearch } from './MessageSearch';
|
||||
|
||||
const meta: Meta<typeof MessageSearch> = {
|
||||
title: 'Features/Chat/MessageSearch',
|
||||
component: MessageSearch,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 w-96">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Results: Story = { name: 'Résultats' };
|
||||
export const NoResults: Story = { name: 'Sans résultats' };
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { VirtualizedChatMessages } from './VirtualizedChatMessages';
|
||||
|
||||
const meta: Meta<typeof VirtualizedChatMessages> = {
|
||||
title: 'Features/Chat/VirtualizedChatMessages',
|
||||
component: VirtualizedChatMessages,
|
||||
parameters: { layout: 'padded' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
36
apps/web/src/features/chat/pages/ChatPage.stories.tsx
Normal file
36
apps/web/src/features/chat/pages/ChatPage.stories.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ChatPage } from './ChatPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const meta: Meta<typeof ChatPage> = {
|
||||
title: 'Pages/Chat/ChatPage',
|
||||
component: ChatPage,
|
||||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = { name: 'Par défaut' };
|
||||
export const Loading: Story = { name: 'Chargement' };
|
||||
export const Empty: Story = { name: 'Vide' };
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* DashboardPage - Tableau de bord utilisateur
|
||||
*
|
||||
* Page principale du tableau de bord avec statistiques,
|
||||
* activité récente, tracks récents et actions rapides.
|
||||
*/
|
||||
const meta: Meta<typeof DashboardPage> = {
|
||||
title: 'Pages/Dashboard/DashboardPage',
|
||||
component: DashboardPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Tableau de bord utilisateur avec vue d\'ensemble des statistiques et activités.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut du dashboard avec données chargées.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* État de chargement des tracks récents.
|
||||
*/
|
||||
export const Loading: Story = {
|
||||
name: 'Chargement',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Affiche les squelettes de chargement pour les tracks récents.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* État vide - aucun track dans la bibliothèque.
|
||||
*/
|
||||
export const Empty: Story = {
|
||||
name: 'Bibliothèque vide',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Message affiché quand l\'utilisateur n\'a pas de tracks.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
49
apps/web/src/features/error/pages/NotFoundPage.stories.tsx
Normal file
49
apps/web/src/features/error/pages/NotFoundPage.stories.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
|
||||
/**
|
||||
* NotFoundPage - Page d'erreur 404
|
||||
*
|
||||
* Affiche une page d'erreur conviviale lorsqu'une page n'est pas trouvée,
|
||||
* avec des liens rapides vers les sections principales de l'application.
|
||||
*/
|
||||
const meta: Meta<typeof NotFoundPage> = {
|
||||
title: 'Pages/Error/NotFoundPage',
|
||||
component: NotFoundPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page 404 améliorée avec messages utiles et actions de récupération.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la page 404.
|
||||
* Affiche le code d'erreur, un message explicatif, des boutons d'action
|
||||
* et des liens rapides vers les sections principales.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Même affichage avec les suggestions - les suggestions
|
||||
* sont toujours visibles dans ce composant.
|
||||
*/
|
||||
export const WithSuggestions: Story = {
|
||||
name: 'Avec suggestions',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'La page inclut toujours des suggestions pour aider l\'utilisateur à continuer sa navigation.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent, expect } from '@storybook/test';
|
||||
import ServerErrorPage from './ServerErrorPage';
|
||||
|
||||
/**
|
||||
* ServerErrorPage - Page d'erreur 500
|
||||
*
|
||||
* Affiche une page d'erreur serveur avec options de récupération,
|
||||
* informations de statut et détails techniques.
|
||||
*/
|
||||
const meta: Meta<typeof ServerErrorPage> = {
|
||||
title: 'Pages/Error/ServerErrorPage',
|
||||
component: ServerErrorPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page d\'erreur 500 améliorée avec messages utiles et actions de récupération.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* État par défaut de la page d'erreur serveur.
|
||||
* Affiche le code d'erreur 500, un message explicatif,
|
||||
* des boutons d'action et des informations d'aide.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
name: 'Par défaut',
|
||||
};
|
||||
|
||||
/**
|
||||
* Démonstration du comportement du bouton "Réessayer".
|
||||
* Utilise une play function pour simuler le clic.
|
||||
*/
|
||||
export const WithRetry: Story = {
|
||||
name: 'Avec action Réessayer',
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Trouver le bouton Réessayer
|
||||
const retryButton = canvas.getByRole('button', { name: /réessayer/i });
|
||||
|
||||
// Vérifier que le bouton existe
|
||||
await expect(retryButton).toBeInTheDocument();
|
||||
|
||||
// Cliquer sur le bouton pour voir l'état de chargement
|
||||
await userEvent.click(retryButton);
|
||||
|
||||
// Vérifier que le texte change en "Réessai..."
|
||||
await expect(canvas.getByText(/réessai/i)).toBeInTheDocument();
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simule un clic sur le bouton Réessayer pour montrer l\'animation de chargement.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Variante pour erreur réseau.
|
||||
* Le même composant est utilisé, les détails techniques
|
||||
* affichent les informations du navigateur.
|
||||
*/
|
||||
export const NetworkError: Story = {
|
||||
name: 'Erreur réseau',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Représentation d\'une erreur réseau - même composant avec contexte différent.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LibraryManager } from './LibraryManager';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
|
||||
const meta = {
|
||||
title: 'Features/Library/LibraryManager',
|
||||
component: LibraryManager,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<Story />
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof LibraryManager>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// NOTE: LibraryManager fetches data on mount via apiClient.
|
||||
// Msw (Mock Service Worker) would be ideal here.
|
||||
// For now, we rely on the fact that if fetch fails, it shows an empty state or error state, which IS a valid visual test.
|
||||
// We can't easily patch apiClient here without a proper test harness.
|
||||
|
||||
export const Default: Story = {};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue