refactor(cart): migrate CheckoutView and MarketplaceView to useCartStore and fix stories

This commit is contained in:
senke 2026-02-03 09:50:51 +01:00
parent 5e8e8b5875
commit 0a054ac8d9
4 changed files with 105 additions and 26 deletions

View file

@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CheckoutView } from './CheckoutView';
import { useCartStore } from '../../stores/cartStore';
import { useEffect } from 'react';
const MOCK_ITEMS = [
{
cartId: 'c1',
product: {
id: 'p1',
title: 'Analog Dreams Vol. 2',
price: 24.99,
coverUrl: 'https://picsum.photos/200',
author: 'Vintage Synths',
},
quantity: 1,
}
];
const meta: Meta<typeof CheckoutView> = {
title: 'Components/Views/CheckoutView',
component: CheckoutView,
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>;
// Helper decorator to set store state
const withStoreState = (items: any[]) => (Story: any) => {
useEffect(() => {
useCartStore.setState({ items });
return () => useCartStore.setState({ items: [] });
}, [items]);
return <Story />;
};
export const Default: Story = {
name: 'Par défaut',
decorators: [withStoreState(MOCK_ITEMS as any)],
};
export const Processing: Story = {
name: 'Traitement',
decorators: [withStoreState(MOCK_ITEMS as any)],
};
export const Success: Story = {
name: 'Succès',
decorators: [withStoreState(MOCK_ITEMS as any)],
};

View file

@ -1,12 +1,13 @@
import { logger } from '@/utils/logger';
import { ArrowLeft, CreditCard, Loader2, Lock } from 'lucide-react';
import React, { useState } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { useCart } from '../../context/CartContext';
import { CreditCard, Lock, ArrowLeft, Loader2 } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { useToast } from '../../components/feedback/ToastProvider';
import { marketplaceService } from '../../services/marketplaceService';
import { logger } from '@/utils/logger';
import { useCartStore } from '../../stores/cartStore';
import { Button } from '../ui/button';
import { Card } from '../ui/card';
import { Input } from '../ui/input';
interface CheckoutViewProps {
onBack: () => void;
@ -17,7 +18,13 @@ export const CheckoutView: React.FC<CheckoutViewProps> = ({
onBack,
onComplete,
}) => {
const { cart, cartTotal, clearCart } = useCart();
const { cart, cartTotal, clearCart } = useCartStore(
useShallow((state) => ({
cart: state.items,
cartTotal: state.getTotal(),
clearCart: state.clearCart,
}))
);
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
@ -47,7 +54,7 @@ export const CheckoutView: React.FC<CheckoutViewProps> = ({
setLoading(true);
try {
// Map cart items to API format
const orderItems = cart.map((item) => ({ product_id: item.id }));
const orderItems = cart.map((item) => ({ product_id: item.product.id }));
await marketplaceService.createOrder(orderItems);
addToast('Purchase successful!', 'success');
@ -225,14 +232,14 @@ export const CheckoutView: React.FC<CheckoutViewProps> = ({
>
<div className="flex-1 pr-4">
<div className="text-kodo-text-main font-medium truncate">
{item.title}
{item.product.title}
</div>
<div className="text-xs text-kodo-content-dim">
{item.selectedLicense?.name || 'Standard'} License
</div>
</div>
<div className="text-white font-mono">
${(item.selectedLicense?.price || item.price).toFixed(2)}
${(item.selectedLicense?.price || item.product.price).toFixed(2)}
</div>
</div>
))}

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MarketplaceView } from './MarketplaceView';
const meta: Meta<typeof MarketplaceView> = {
title: 'Components/Views/MarketplaceView',
component: MarketplaceView,
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' };

View file

@ -5,7 +5,7 @@ import { SearchInput } from '../ui/input';
import { Loader2, SlidersHorizontal } from 'lucide-react';
import { Product } from '../../types';
import { useToast } from '../../components/feedback/ToastProvider';
import { useCart } from '../../context/CartContext';
import { useCartStore } from '../../stores/cartStore';
import { ProductCard } from '../marketplace/ProductCard';
import { ProductDetailView } from '../marketplace/ProductDetailView';
import { marketplaceService } from '../../services/marketplaceService';
@ -13,7 +13,7 @@ import { logger } from '@/utils/logger';
export const MarketplaceView: React.FC = () => {
const { addToast } = useToast();
const { addToCart } = useCart();
const addToCart = useCartStore((state) => state.addItem);
const [loading, setLoading] = useState(true);
const [products, setProducts] = useState<Product[]>([]);
@ -23,7 +23,6 @@ export const MarketplaceView: React.FC = () => {
const [activeCategory, setActiveCategory] = useState('All');
const [searchQuery, setSearchQuery] = useState('');
const [filtersOpen, setFiltersOpen] = useState(false);
const [playingPreview, setPlayingPreview] = useState<string | null>(null);
useEffect(() => {
loadProducts();
@ -56,14 +55,7 @@ export const MarketplaceView: React.FC = () => {
}
};
const togglePreview = (id: string) => {
if (playingPreview === id) {
setPlayingPreview(null);
} else {
setPlayingPreview(id);
addToast('Previewing audio...', 'info');
}
};
// Filter Logic
const filteredProducts = products.filter((p) => {
@ -171,13 +163,12 @@ export const MarketplaceView: React.FC = () => {
<ProductCard
key={product.id}
product={product}
onPurchase={(p) => {
addToast('Checkout feature coming soon', 'info');
// In a real app, this would redirect to checkout
}}
onClick={(p) => setSelectedProduct(p)}
onPreview={() => { }}
isPlayingPreview={false}
onAddToCart={(p) => {
addToast('Added to cart', 'success');
addToCart(p, p.license_type || 'personal');
addToCart(p);
}}
/>
))}