chore(storybook): improve configuration and cleanup
This commit is contained in:
parent
a2576c4eae
commit
d2ae91ac25
35 changed files with 398 additions and 445 deletions
|
|
@ -16,6 +16,7 @@ const config: StorybookConfig = {
|
|||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
getAbsolutePath('@storybook/addon-interactions'),
|
||||
],
|
||||
"staticDirs": ['../public'],
|
||||
"framework": getAbsolutePath('@storybook/react-vite'),
|
||||
"docs": {
|
||||
"defaultName": "Documentation"
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import type { Preview } from '@storybook/react-vite';
|
||||
import '../src/index.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
|
@ -1,9 +1,15 @@
|
|||
import type { Preview } from '@storybook/react-vite';
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../src/index.css';
|
||||
import '../src/styles/design-system.css';
|
||||
import '../src/styles/global-effects.css';
|
||||
import '../src/styles/header.css';
|
||||
import '../src/lib/i18n'; // Initialize i18n
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from '../src/components/theme/ThemeProvider';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToastProvider } from '../src/components/feedback/ToastProvider';
|
||||
import { AudioProvider } from '../src/context/AudioContext';
|
||||
|
||||
// Create a client for stories
|
||||
const queryClient = new QueryClient({
|
||||
|
|
@ -50,9 +56,6 @@ const preview: Preview = {
|
|||
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: {
|
||||
|
|
@ -77,15 +80,19 @@ const preview: Preview = {
|
|||
// 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>
|
||||
<ThemeProvider defaultTheme={isDark ? 'dark' : 'light'}>
|
||||
<div className={isDark ? 'dark' : ''}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastProvider>
|
||||
<AudioProvider>
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
</AudioProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ See `eslint.config.js` for full rule configuration.
|
|||
|
||||
## Documentation
|
||||
|
||||
- **Architecture**: See `docs/` directory
|
||||
- **Architecture Guide**: `docs/ARCHITECTURE.md` (MUST READ)
|
||||
- **Component Usage**: `src/components/COMPONENT_USAGE.md`
|
||||
- **Color Usage**: `src/styles/COLOR_USAGE.md`
|
||||
- **Typography**: `src/styles/TYPOGRAPHY_GUIDE.md`
|
||||
|
|
|
|||
120
apps/web/docs/ARCHITECTURE.md
Normal file
120
apps/web/docs/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Veza Frontend Architecture Guide
|
||||
|
||||
**Status:** Living Document
|
||||
**Version:** 2.0 (Post-Audit 2026)
|
||||
|
||||
This document outlines the architectural principles, patterns, and rules that govern the Veza frontend. It supersedes previous ad-hoc audit reports.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Philosophy: "Visual First"
|
||||
|
||||
> "If it can't be rendered in Storybook, it is architecturally broken."
|
||||
|
||||
We follow a **Storybook-Driven Development** (SDD) approach.
|
||||
- **Isolation:** Every component must be renderable in isolation. Dependencies (Providers, Router, Store) must be explicit.
|
||||
- **Verification:** Storybook is our primary verification tool for UI logic and layout.
|
||||
|
||||
---
|
||||
|
||||
## 2. State Management Strategy
|
||||
|
||||
We distinguish three types of state. Mixing them is strictly forbidden.
|
||||
|
||||
### 2.1. Server State (Data) -> `React Query`
|
||||
Data that belongs to the backend (Users, Tracks, Playlists).
|
||||
- **Tool:** `@tanstack/react-query`
|
||||
- **Rule:** Never copy server data into a global store (Zustand) unless strictly necessary for client-side manipulation (e.g., a complex audio editor buffer).
|
||||
- **Caching:** Managed automatically by query keys.
|
||||
|
||||
### 2.2. Client Global State (App) -> `Zustand`
|
||||
Data that is truly global to the client session (Auth Token, Shopping Cart, Audio Player Status).
|
||||
- **Tool:** `zustand`
|
||||
- **Rule:** Use atomic selectors to prevent render-thrashing.
|
||||
- **Structure:**
|
||||
- `authStore`: User session.
|
||||
- `cartStore`: E-commerce state (Items, Total).
|
||||
- `playerStore`: Audio playback state.
|
||||
|
||||
**Legacy Note:** `React Context` is BANNED for high-frequency state updates. It is reserved for dependency injection (Theme, i18n).
|
||||
|
||||
### 2.3. UI Local State -> `useState` / `useReducer`
|
||||
Ephemeral state specific to a component (Modal Open/Close, Form Inputs).
|
||||
- **Rule:** If it doesn't need to persist when navigating away, it stays local.
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Engineering
|
||||
|
||||
We adhere to the **Smart vs Dumb** (Container vs Presentational) separation to ensure testability.
|
||||
|
||||
### 3.1. Dumb Components (UI)
|
||||
- **Role:** Render props into HTML. Emit events via callbacks.
|
||||
- **Dependencies:** ZERO. No explicit side-effects, no API calls, no Context consumers (except Theme).
|
||||
- **Testing:** Storybook.
|
||||
|
||||
```tsx
|
||||
// ✅ Correct Dumb Component
|
||||
export const ProductCard = ({ title, price, onAddToCart }: Props) => (
|
||||
<div onClick={onAddToCart}>{title} - {price}</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2. Smart Components (Containers)
|
||||
- **Role:** Wire data to UI.
|
||||
- **Dependencies:** Allowed (`useQuery`, `useCartStore`, `useParams`).
|
||||
- **Testing:** Integration Tests (MSW + Storybook play functions).
|
||||
|
||||
```tsx
|
||||
// ✅ Correct Smart Component
|
||||
export const ProductCardContainer = ({ id }) => {
|
||||
const { data } = useProduct(id);
|
||||
const addToCart = useCartStore(s => s.addItem);
|
||||
return <ProductCard title={data.title} onAddToCart={() => addToCart(data)} />;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Design System & Styling
|
||||
|
||||
We use **Tailwind CSS** with a rigorous Design System (Kodo).
|
||||
|
||||
- **Tokens Only:** Do not use arbitrary values (e.g., `w-[350px]`). Use design tokens (`w-sidebar`).
|
||||
- **Dark Mode:** All UI/Layout components must implement `dark:` variants.
|
||||
- **Icons:** `lucide-react`. Icons must inherit color via `currentColor`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Storybook Usage
|
||||
|
||||
Storybook is not optional. It is the definition of "Done".
|
||||
|
||||
### 5.1. Decorators
|
||||
Use granular decorators from `src/stories/decorators.tsx` instead of global wrapping in `preview.tsx`.
|
||||
- `withToast`: Injects ToastProvider.
|
||||
- `withRouter`: Injects MemoryRouter.
|
||||
- `withStoreState`: Mocks Zustand state.
|
||||
|
||||
### 5.2. Interaction Testing
|
||||
Critical user flows (e.g., Add to Cart) must have a `.play` function in their story to verify interaction without manual testing.
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing Pyramid
|
||||
|
||||
1. **Unit (Vitest):** Utilities, Store Reducers, Hooks.
|
||||
2. **Integration (Storybook + Vitest):** Component wiring, Props interface.
|
||||
3. **E2E (Playwright):** Critical Paths (Login, Checkout, Signup).
|
||||
|
||||
---
|
||||
|
||||
## 7. Anti-Patterns (Dos & Don'ts)
|
||||
|
||||
| ❌ Don't | ✅ Do |
|
||||
| :--- | :--- |
|
||||
| `useContext(CartContext)` | `useCartStore(selector)` |
|
||||
| `w-[17px]` | `w-4` or `w-5` (stick to grid) |
|
||||
| Props Drilling (> 3 levels) | Composition (Slots) or Context (if static) |
|
||||
| API calls in `useEffect` | `useQuery` |
|
||||
| `any` type | Generated types from OpenAPI |
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminDashboardView } from './AdminDashboardView';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
/**
|
||||
* AdminDashboardView - Centre de commande admin
|
||||
|
|
@ -21,9 +22,11 @@ const meta: Meta<typeof AdminDashboardView> = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AdminModerationView } from './AdminModerationView';
|
||||
import { ToastProvider } from '../../components/feedback/ToastProvider';
|
||||
|
||||
/**
|
||||
* AdminModerationView - Queue de modération
|
||||
|
|
@ -21,9 +22,11 @@ const meta: Meta<typeof AdminModerationView> = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||
import { DashboardLayout } from './DashboardLayout';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const meta = {
|
||||
|
|
@ -12,9 +14,9 @@ const meta = {
|
|||
(Story) => (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<ToastProvider>
|
||||
<Story />
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||
import { Header } from './Header';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const meta = {
|
||||
|
|
@ -12,7 +14,7 @@ const meta = {
|
|||
(Story) => (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<ToastProvider>
|
||||
<div className="h-screen bg-kodo-void">
|
||||
{/* Header is fixed, so we need a container that mimics the body */}
|
||||
<Story />
|
||||
|
|
@ -20,7 +22,7 @@ const meta = {
|
|||
<h1>Page Content</h1>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Navbar } from './Navbar';
|
||||
import { ThemeProvider } from '../../context/ThemeContext';
|
||||
import { ThemeProvider } from '../theme/ThemeProvider';
|
||||
|
||||
|
||||
const meta: Meta<typeof Navbar> = {
|
||||
|
|
@ -12,7 +12,7 @@ const meta: Meta<typeof Navbar> = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider>
|
||||
<ThemeProvider defaultTheme="dark">
|
||||
<div className="min-h-[200px] bg-background">
|
||||
<Story />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useCartStore } from '../../stores/cartStore';
|
||||
import { useTheme } from '../../context/ThemeContext';
|
||||
import { useTheme } from '../theme/ThemeProvider';
|
||||
import { Notification } from '../../types';
|
||||
import { NotificationBell } from '../notifications/NotificationBell';
|
||||
|
||||
|
|
@ -52,7 +52,11 @@ const mockNotifications: Notification[] = [
|
|||
];
|
||||
|
||||
export const Navbar: React.FC<NavbarProps> = ({ onNavigate, onLogout }) => {
|
||||
const { toggleTheme, theme } = useTheme();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
};
|
||||
// Selector ensures we re-render only when the calculated count changes
|
||||
const itemCount = useCartStore((state) => state.getItemCount());
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { PlaylistsView } from './PlaylistsView';
|
||||
|
||||
import { ToastProvider } from '../../feedback/ToastProvider';
|
||||
|
||||
const meta: Meta<typeof PlaylistsView> = {
|
||||
title: 'Components/Library/Playlists/PlaylistsView',
|
||||
component: PlaylistsView,
|
||||
|
|
@ -8,9 +10,11 @@ const meta: Meta<typeof PlaylistsView> = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background p-4 min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { NotificationMenu } from './NotificationMenu';
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { ToastProvider } from '../feedback/ToastProvider';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const meta = {
|
||||
|
|
@ -13,9 +15,11 @@ const meta = {
|
|||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<div className="flex justify-end p-20 bg-kodo-ink min-h-[400px]">
|
||||
<Story />
|
||||
</div>
|
||||
<ToastProvider>
|
||||
<div className="flex justify-end p-20 bg-kodo-ink min-h-[400px]">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card } from '../../ui/card';
|
||||
import { Button } from '../../ui/button';
|
||||
import { useTheme } from '../../../context/ThemeContext';
|
||||
import { useTheme } from '../../../components/theme/ThemeProvider';
|
||||
import { ThemeVariant } from '../../../types';
|
||||
import {
|
||||
Mail,
|
||||
|
|
@ -143,7 +143,16 @@ export const AccountSettings: React.FC = () => {
|
|||
].map((opt) => (
|
||||
<button
|
||||
key={opt.id}
|
||||
onClick={() => setTheme(opt.id)}
|
||||
onClick={() => {
|
||||
const map: Record<ThemeVariant, 'dark' | 'light'> = {
|
||||
[ThemeVariant.NEON]: 'dark',
|
||||
[ThemeVariant.LIGHT]: 'light',
|
||||
[ThemeVariant.GAMING]: 'dark',
|
||||
[ThemeVariant.NATURE]: 'light',
|
||||
[ThemeVariant.TERMINAL]: 'dark',
|
||||
};
|
||||
setTheme(map[opt.id]);
|
||||
}}
|
||||
className={`flex flex-col items-center justify-center gap-2 p-4 rounded-lg border transition-all ${theme === opt.id ? 'bg-kodo-cyan/10 border-kodo-cyan text-kodo-cyan' : 'bg-kodo-ink border-kodo-steel text-kodo-content-dim hover:text-white'}`}
|
||||
>
|
||||
{opt.icon}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Card } from '../../ui/card';
|
||||
import { Button } from '../../ui/button';
|
||||
import { useTheme } from '../../../context/ThemeContext';
|
||||
import { useTheme } from '../../../components/theme/ThemeProvider';
|
||||
import { ThemeVariant } from '../../../types';
|
||||
import {
|
||||
Moon,
|
||||
|
|
@ -79,7 +79,16 @@ export const AppearanceSettingsView: React.FC = () => {
|
|||
].map((opt) => (
|
||||
<div
|
||||
key={opt.id}
|
||||
onClick={() => setTheme(opt.id)}
|
||||
onClick={() => {
|
||||
const map: Record<ThemeVariant, 'dark' | 'light'> = {
|
||||
[ThemeVariant.NEON]: 'dark',
|
||||
[ThemeVariant.LIGHT]: 'light',
|
||||
[ThemeVariant.GAMING]: 'dark',
|
||||
[ThemeVariant.NATURE]: 'light',
|
||||
[ThemeVariant.TERMINAL]: 'dark', // Gaming treated as dark for now
|
||||
};
|
||||
setTheme(map[opt.id]);
|
||||
}}
|
||||
className={`
|
||||
cursor-pointer p-6 rounded-xl border-2 transition-all flex flex-col items-center gap-4 relative
|
||||
${theme === opt.id ? 'border-kodo-cyan bg-kodo-cyan/5' : 'border-kodo-steel bg-kodo-ink hover:border-kodo-steel'}
|
||||
|
|
|
|||
|
|
@ -121,8 +121,18 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="flex items-center justify-center pointer-events-none">{icon}</span>}
|
||||
{children}
|
||||
{asChild ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
{icon && (
|
||||
<span className="flex items-center justify-center pointer-events-none">
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ThemeProvider, useTheme } from './ThemeContext';
|
||||
import { ReactNode } from 'react';
|
||||
import { ThemeVariant } from '@/types';
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('ThemeContext', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should provide theme context', () => {
|
||||
const { result } = renderHook(() => useTheme(), { wrapper });
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
expect(result.current).toHaveProperty('theme');
|
||||
expect(result.current).toHaveProperty('setTheme');
|
||||
});
|
||||
|
||||
it('should have default theme', () => {
|
||||
const { result } = renderHook(() => useTheme(), { wrapper });
|
||||
|
||||
expect(result.current.theme).toBeDefined();
|
||||
});
|
||||
|
||||
it('should change theme', () => {
|
||||
const { result } = renderHook(() => useTheme(), { wrapper });
|
||||
|
||||
const initialTheme = result.current.theme;
|
||||
|
||||
act(() => {
|
||||
result.current.setTheme(ThemeVariant.GAMING);
|
||||
});
|
||||
|
||||
expect(result.current.theme).toBe(ThemeVariant.GAMING);
|
||||
expect(result.current.theme).not.toBe(initialTheme);
|
||||
});
|
||||
|
||||
it('should toggle theme', () => {
|
||||
const { result } = renderHook(() => useTheme(), { wrapper });
|
||||
|
||||
const initialTheme = result.current.theme;
|
||||
|
||||
act(() => {
|
||||
if (result.current.toggleTheme) {
|
||||
result.current.toggleTheme();
|
||||
}
|
||||
});
|
||||
|
||||
// Le thème devrait changer si toggleTheme existe
|
||||
if (result.current.toggleTheme) {
|
||||
expect(result.current.theme).not.toBe(initialTheme);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { ThemeVariant } from '../types';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: ThemeVariant;
|
||||
setTheme: (theme: ThemeVariant) => void;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Define color palettes (RGB channels)
|
||||
const palettes: Record<ThemeVariant, Record<string, string>> = {
|
||||
[ThemeVariant.NEON]: {
|
||||
'--kodo-void': '5 5 8',
|
||||
'--kodo-ink': '10 11 15',
|
||||
'--kodo-graphite': '18 19 26',
|
||||
'--kodo-slate': '26 28 38',
|
||||
'--kodo-steel': '37 40 54',
|
||||
'--kodo-cyan': '0 255 247',
|
||||
'--kodo-cyan-dim': '0 196 189',
|
||||
'--kodo-magenta': '255 0 255',
|
||||
'--kodo-lime': '184 255 0',
|
||||
'--kodo-gold': '255 215 0',
|
||||
'--kodo-red': '255 51 51',
|
||||
'--kodo-terminal': '0 255 0',
|
||||
'--kodo-content-highlight': '255 255 255',
|
||||
'--kodo-content-dim': '156 163 175',
|
||||
'--kodo-text-main': '243 243 224',
|
||||
},
|
||||
[ThemeVariant.GAMING]: {
|
||||
'--kodo-void': '10 10 10',
|
||||
'--kodo-ink': '15 15 15',
|
||||
'--kodo-graphite': '20 20 20',
|
||||
'--kodo-slate': '30 30 30',
|
||||
'--kodo-steel': '45 45 45',
|
||||
'--kodo-cyan': '255 215 0', // Gold acts as primary
|
||||
'--kodo-cyan-dim': '218 165 32',
|
||||
'--kodo-magenta': '255 51 51', // Red acts as accent
|
||||
'--kodo-lime': '255 255 255',
|
||||
'--kodo-gold': '255 140 0',
|
||||
'--kodo-red': '139 0 0',
|
||||
'--kodo-terminal': '50 205 50',
|
||||
'--kodo-content-highlight': '255 255 255',
|
||||
'--kodo-content-dim': '156 163 175',
|
||||
'--kodo-text-main': '243 243 224',
|
||||
},
|
||||
[ThemeVariant.NATURE]: {
|
||||
'--kodo-void': '5 15 5',
|
||||
'--kodo-ink': '10 20 10',
|
||||
'--kodo-graphite': '18 31 18',
|
||||
'--kodo-slate': '26 38 26',
|
||||
'--kodo-steel': '37 54 37',
|
||||
'--kodo-cyan': '74 222 128', // Green-400
|
||||
'--kodo-cyan-dim': '34 197 94',
|
||||
'--kodo-magenta': '250 204 21', // Yellow-400
|
||||
'--kodo-lime': '132 204 22',
|
||||
'--kodo-gold': '234 179 8',
|
||||
'--kodo-red': '248 113 113',
|
||||
'--kodo-terminal': '20 83 45',
|
||||
'--kodo-content-highlight': '255 255 255',
|
||||
'--kodo-content-dim': '156 163 175',
|
||||
'--kodo-text-main': '236 253 245',
|
||||
},
|
||||
[ThemeVariant.TERMINAL]: {
|
||||
'--kodo-void': '0 0 0',
|
||||
'--kodo-ink': '0 17 0',
|
||||
'--kodo-graphite': '0 26 0',
|
||||
'--kodo-slate': '0 34 0',
|
||||
'--kodo-steel': '0 51 0',
|
||||
'--kodo-cyan': '0 255 0',
|
||||
'--kodo-cyan-dim': '0 200 0',
|
||||
'--kodo-magenta': '0 204 0',
|
||||
'--kodo-lime': '0 255 0',
|
||||
'--kodo-gold': '0 255 0',
|
||||
'--kodo-red': '255 0 0',
|
||||
'--kodo-terminal': '0 255 0',
|
||||
'--kodo-content-highlight': '0 255 0',
|
||||
'--kodo-content-dim': '0 170 0',
|
||||
'--kodo-text-main': '0 255 0',
|
||||
},
|
||||
[ThemeVariant.LIGHT]: {
|
||||
'--kodo-void': '245 247 250',
|
||||
'--kodo-ink': '255 255 255',
|
||||
'--kodo-graphite': '255 255 255',
|
||||
'--kodo-slate': '241 245 249',
|
||||
'--kodo-steel': '226 232 240',
|
||||
'--kodo-cyan': '13 148 136', // Teal 600 (Darker for contrast)
|
||||
'--kodo-cyan-dim': '20 184 166',
|
||||
'--kodo-magenta': '192 38 211', // Fuchsia 600
|
||||
'--kodo-lime': '101 163 13', // Lime 600
|
||||
'--kodo-gold': '202 138 4',
|
||||
'--kodo-red': '220 38 38',
|
||||
'--kodo-terminal': '22 101 52',
|
||||
'--kodo-content-highlight': '17 24 39', // Gray 900
|
||||
'--kodo-content-dim': '75 85 99', // Gray 600
|
||||
'--kodo-text-main': '17 24 39',
|
||||
},
|
||||
};
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [theme, setTheme] = useState<ThemeVariant>(ThemeVariant.NEON);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const colors = palettes[theme];
|
||||
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
root.style.setProperty(key, value as string);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const variants = Object.values(ThemeVariant);
|
||||
const currentIndex = variants.indexOf(theme);
|
||||
const nextIndex = (currentIndex + 1) % variants.length;
|
||||
setTheme(variants[nextIndex]);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,22 +1,8 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { ForgotPasswordPage } from './ForgotPasswordPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { withRouter, withQueryClient, withToast } from '../../../stories/decorators';
|
||||
|
||||
// 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,
|
||||
|
|
@ -30,11 +16,9 @@ const meta: Meta<typeof ForgotPasswordPage> = {
|
|||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent, expect, fn } from '@storybook/test';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { LoginPage } from './LoginPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { withRouter, withQueryClient } from '../../../stories/decorators';
|
||||
|
||||
// Create a mocked QueryClient for stories
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* LoginPage - Page de connexion
|
||||
|
|
@ -30,11 +23,8 @@ const meta: Meta<typeof LoginPage> = {
|
|||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,8 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/test';
|
||||
import { RegisterPage } from './RegisterPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { withRouter, withQueryClient, withToast } from '../../../stories/decorators';
|
||||
|
||||
// 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,
|
||||
|
|
@ -31,11 +16,9 @@ const meta: Meta<typeof RegisterPage> = {
|
|||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,7 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { withRouter, withQueryClient, withToast } from '../../../stories/decorators';
|
||||
|
||||
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,
|
||||
|
|
@ -28,12 +15,13 @@ const meta: Meta<typeof DashboardPage> = {
|
|||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { withRouter } from '../../../stories/decorators';
|
||||
import NotFoundPage from './NotFoundPage';
|
||||
|
||||
/**
|
||||
|
|
@ -19,6 +20,7 @@ const meta: Meta<typeof NotFoundPage> = {
|
|||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [withRouter],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent, expect } from '@storybook/test';
|
||||
import { withRouter } from '../../../stories/decorators';
|
||||
import ServerErrorPage from './ServerErrorPage';
|
||||
|
||||
/**
|
||||
|
|
@ -20,6 +21,7 @@ const meta: Meta<typeof ServerErrorPage> = {
|
|||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [withRouter],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { UploadModal } from './UploadModal';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
|
||||
const meta = {
|
||||
title: 'Features/Library/Components/UploadModal',
|
||||
component: UploadModal,
|
||||
|
|
@ -10,6 +12,13 @@ const meta = {
|
|||
argTypes: {
|
||||
isOpen: { control: 'boolean' },
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<Story />
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof UploadModal>;
|
||||
|
||||
export default meta;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { LibraryPage } from './LibraryPage';
|
||||
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 },
|
||||
},
|
||||
});
|
||||
import { withRouter, withQueryClient, withToast, withAudio } from '../../../stories/decorators';
|
||||
|
||||
const meta: Meta<typeof LibraryPage> = {
|
||||
title: 'Pages/Library/LibraryPage',
|
||||
|
|
@ -16,14 +8,14 @@ const meta: Meta<typeof LibraryPage> = {
|
|||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
withAudio,
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
import { withRouter, withQueryClient, withToast, withAudio } from '../../../stories/decorators';
|
||||
|
||||
const meta: Meta<typeof NotificationsPage> = {
|
||||
title: 'Pages/Notifications/NotificationsPage',
|
||||
|
|
@ -15,12 +8,14 @@ const meta: Meta<typeof NotificationsPage> = {
|
|||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
withAudio,
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { CreatePlaylistDialog } from './CreatePlaylistDialog';
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
import { withToast } from '@/stories/decorators';
|
||||
|
||||
const meta: Meta<typeof CreatePlaylistDialog> = {
|
||||
title: 'Features/Playlists/CreatePlaylistDialog',
|
||||
|
|
@ -9,13 +9,7 @@ const meta: Meta<typeof CreatePlaylistDialog> = {
|
|||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ToastProvider>
|
||||
<Story />
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
decorators: [withToast],
|
||||
argTypes: {
|
||||
onOpenChange: { action: 'onOpenChange' },
|
||||
onCreated: { action: 'onCreated' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { PlaylistList } from './PlaylistList';
|
||||
import { ToastProvider } from '@/components/feedback/ToastProvider';
|
||||
|
||||
/**
|
||||
* PlaylistList - Liste de playlists
|
||||
|
|
@ -21,9 +22,11 @@ const meta: Meta<typeof PlaylistList> = {
|
|||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
<ToastProvider>
|
||||
<div className="bg-kodo-background min-h-screen p-4">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,20 +1,7 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { PlaylistListPage } from './PlaylistListPage';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { withRouter, withQueryClient, withToast, withAudio } from '../../../stories/decorators';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* PlaylistListPage - Page de liste des playlists
|
||||
*
|
||||
* Page principale affichant toutes les playlists
|
||||
* avec recherche et filtres.
|
||||
*/
|
||||
const meta: Meta<typeof PlaylistListPage> = {
|
||||
title: 'Pages/Playlists/PlaylistListPage',
|
||||
component: PlaylistListPage,
|
||||
|
|
@ -28,12 +15,14 @@ const meta: Meta<typeof PlaylistListPage> = {
|
|||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
withAudio,
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,14 +24,16 @@ export const Default: Story = {
|
|||
return <ViewToggle {...args} value={value} onChange={(newValue) => updateArgs({ value: newValue })} />;
|
||||
},
|
||||
args: {
|
||||
value: 'list',
|
||||
value: 'list' as const,
|
||||
showLabels: true,
|
||||
onChange: () => { },
|
||||
}
|
||||
};
|
||||
|
||||
export const IconOnly: Story = {
|
||||
args: {
|
||||
value: 'grid',
|
||||
value: 'grid' as const,
|
||||
showLabels: false,
|
||||
onChange: () => { },
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { TrackDetailPage } from './TrackDetailPage';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { AudioProvider } from '../../../context/AudioContext';
|
||||
import { ToastProvider } from '../../../components/feedback/ToastProvider';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
const createMockQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
|
|
@ -10,32 +13,24 @@ const createMockQueryClient = () => new QueryClient({
|
|||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* TrackDetailPage - Page détail d'un track
|
||||
*
|
||||
* Page complète affichant les détails d'un track avec
|
||||
* waveform, métadonnées et actions.
|
||||
*/
|
||||
const meta: Meta<typeof TrackDetailPage> = {
|
||||
title: 'Pages/Tracks/TrackDetailPage',
|
||||
component: TrackDetailPage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Page de détail d\'un track audio.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<MemoryRouter initialEntries={['/tracks/demo-track']}>
|
||||
<Routes>
|
||||
<Route path="/tracks/:id" element={<Story />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
<ToastProvider>
|
||||
<AudioProvider>
|
||||
<MemoryRouter initialEntries={['/tracks/demo-track']}>
|
||||
<Routes>
|
||||
<Route path="/tracks/:id" element={<div className="bg-kodo-background min-h-screen"><Story /></div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</AudioProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MarketplaceHome } from './MarketplaceHome';
|
||||
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 },
|
||||
},
|
||||
});
|
||||
import { withRouter, withQueryClient, withToast, withAudio } from '../../stories/decorators';
|
||||
|
||||
const meta: Meta<typeof MarketplaceHome> = {
|
||||
title: 'Pages/Marketplace/MarketplaceHome',
|
||||
|
|
@ -16,14 +8,14 @@ const meta: Meta<typeof MarketplaceHome> = {
|
|||
parameters: { layout: 'fullscreen' },
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
withRouter,
|
||||
withQueryClient,
|
||||
withToast,
|
||||
withAudio,
|
||||
(Story) => (
|
||||
<QueryClientProvider client={createMockQueryClient()}>
|
||||
<BrowserRouter>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
<div className="bg-kodo-background min-h-screen">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
72
apps/web/src/stories/decorators.tsx
Normal file
72
apps/web/src/stories/decorators.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ToastProvider } from '../components/feedback/ToastProvider';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useCartStore } from '../stores/cartStore';
|
||||
import { AudioProvider } from '../context/AudioContext';
|
||||
|
||||
// === HELPER UTILS ===
|
||||
|
||||
/**
|
||||
* Wraps the story in an AudioProvider.
|
||||
* Useful for player components and views requiring audio context.
|
||||
*/
|
||||
export const withAudio = (Story: React.ComponentType) => (
|
||||
<AudioProvider>
|
||||
<Story />
|
||||
</AudioProvider>
|
||||
);
|
||||
|
||||
// Create a new QueryClient for each story to ensure isolation
|
||||
const createTestQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
// === DECORATORS ===
|
||||
|
||||
/**
|
||||
* Wraps the story in a MemoryRouter.
|
||||
* Useful for components using Link, useNavigate, useLocation, etc.
|
||||
*/
|
||||
export const withRouter = (Story: React.ComponentType) => (
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
/**
|
||||
* Wraps the story in a ToastProvider.
|
||||
* Useful for components triggering notifications (addToast).
|
||||
*/
|
||||
export const withToast = (Story: React.ComponentType) => (
|
||||
<ToastProvider>
|
||||
<div className="relative min-h-[300px] w-full">
|
||||
<Story />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Wraps the story in a QueryClientProvider.
|
||||
* Useful for Smart Components fetching data.
|
||||
*/
|
||||
export const withQueryClient = (Story: React.ComponentType) => (
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper to mock Cart Store state.
|
||||
* Usage: decorators: [withCartState([{ product: ..., quantity: 1 }])]
|
||||
*/
|
||||
export const withCartState = (initialItems: any[]) => (Story: React.ComponentType) => {
|
||||
React.useEffect(() => {
|
||||
useCartStore.setState({ items: initialItems });
|
||||
return () => useCartStore.setState({ items: [] });
|
||||
}, [initialItems]);
|
||||
return <Story />;
|
||||
};
|
||||
|
|
@ -32,7 +32,7 @@ let isResolved = false;
|
|||
toastModulePromise.then((mod) => {
|
||||
toastModule = mod;
|
||||
isResolved = true;
|
||||
}).catch((err) => {
|
||||
}).catch(() => {
|
||||
isResolved = true;
|
||||
// Ignorer les erreurs de chargement
|
||||
});
|
||||
|
|
@ -48,43 +48,43 @@ function getToastModuleSync() {
|
|||
// Le module a échoué à charger, retourner un stub
|
||||
logger.error('Toast module failed to load');
|
||||
return {
|
||||
success: () => {},
|
||||
error: () => {},
|
||||
loading: () => {},
|
||||
custom: () => {},
|
||||
dismiss: () => {},
|
||||
remove: () => {},
|
||||
success: () => { },
|
||||
error: () => { },
|
||||
loading: () => { },
|
||||
custom: () => { },
|
||||
dismiss: () => { },
|
||||
remove: () => { },
|
||||
promise: () => Promise.resolve(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
if (toastModule) {
|
||||
return toastModule.default;
|
||||
}
|
||||
|
||||
|
||||
// Si le module n'est pas encore chargé, retourner un stub temporaire
|
||||
// qui sera remplacé une fois le module chargé
|
||||
return {
|
||||
success: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.success(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.success as any)(...args));
|
||||
},
|
||||
error: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.error(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.error as any)(...args));
|
||||
},
|
||||
loading: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.loading(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.loading as any)(...args));
|
||||
},
|
||||
custom: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.custom(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.custom as any)(...args));
|
||||
},
|
||||
dismiss: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.dismiss(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.dismiss as any)(...args));
|
||||
},
|
||||
remove: (...args: any[]) => {
|
||||
toastModulePromise.then((mod) => mod.default.remove(...args));
|
||||
toastModulePromise.then((mod) => (mod.default.remove as any)(...args));
|
||||
},
|
||||
promise: (...args: any[]) => {
|
||||
return toastModulePromise.then((mod) => mod.default.promise(...args));
|
||||
return toastModulePromise.then((mod) => (mod.default.promise as any)(...args));
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ function getToastModuleSync() {
|
|||
const toast = new Proxy({} as typeof import('react-hot-toast').default, {
|
||||
get(_target, prop) {
|
||||
const toastFn = getToastModuleSync() as any;
|
||||
|
||||
|
||||
if (prop in toastFn) {
|
||||
const method = toastFn[prop];
|
||||
if (typeof method === 'function') {
|
||||
|
|
@ -101,7 +101,7 @@ const toast = new Proxy({} as typeof import('react-hot-toast').default, {
|
|||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
},
|
||||
apply(_target, _thisArg, args) {
|
||||
|
|
@ -110,7 +110,7 @@ const toast = new Proxy({} as typeof import('react-hot-toast').default, {
|
|||
return toastFn(...args);
|
||||
}
|
||||
// Si ce n'est pas une fonction, essayer d'appeler via le module
|
||||
return toastModulePromise.then((mod) => mod.default(...args));
|
||||
return toastModulePromise.then((mod) => (mod.default as any)(...args));
|
||||
},
|
||||
}) as typeof import('react-hot-toast').default;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue