2026-01-07 09:31:02 +00:00
|
|
|
import { Button } from '../ui/button';
|
2026-02-03 08:40:54 +00:00
|
|
|
import { Card } from '../ui/card';
|
2026-02-08 23:00:21 +00:00
|
|
|
import { EmptyState } from '../ui/empty-state';
|
2026-02-09 22:04:35 +00:00
|
|
|
import { Skeleton } from '../ui/skeleton';
|
2026-02-03 08:40:54 +00:00
|
|
|
import { useCartStore } from '../../stores/cartStore';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { Product } from '../../types';
|
2026-02-03 08:40:54 +00:00
|
|
|
import { Heart, Pause, Play, ShoppingCart, Trash2, Zap } from 'lucide-react';
|
|
|
|
|
import React, { useState } from 'react';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
// Mock Wishlist Data
|
2026-01-07 18:39:21 +00:00
|
|
|
const MOCK_WISHLIST: any[] = [
|
2026-01-13 18:47:57 +00:00
|
|
|
{
|
|
|
|
|
id: 'w1',
|
|
|
|
|
title: 'Analog Dreams Vol. 2',
|
|
|
|
|
type: 'sample_pack',
|
|
|
|
|
price: 24.99,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
rating: 4.8,
|
|
|
|
|
coverUrl: 'https://picsum.photos/id/40/300/300',
|
|
|
|
|
author: 'Vintage Synths',
|
|
|
|
|
description: 'Warm analog pads and leads.',
|
|
|
|
|
features: [],
|
|
|
|
|
licenses: [],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w2',
|
|
|
|
|
title: 'Tech House Essentials',
|
|
|
|
|
type: 'preset',
|
|
|
|
|
price: 19.99,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
rating: 4.5,
|
|
|
|
|
coverUrl: 'https://picsum.photos/id/45/300/300',
|
|
|
|
|
author: 'Club Ready',
|
|
|
|
|
description: 'Floor filling serum presets.',
|
|
|
|
|
features: [],
|
|
|
|
|
licenses: [],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'w3',
|
|
|
|
|
title: 'Cinematic FX',
|
|
|
|
|
type: 'sample_pack',
|
|
|
|
|
price: 34.5,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
rating: 5.0,
|
|
|
|
|
coverUrl: 'https://picsum.photos/id/50/300/300',
|
|
|
|
|
author: 'Sound Design Co',
|
|
|
|
|
isHot: true,
|
|
|
|
|
description: 'Impacts, risers, and drops.',
|
|
|
|
|
features: [],
|
|
|
|
|
licenses: [],
|
|
|
|
|
},
|
2026-01-07 09:31:02 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const WishlistView: React.FC = () => {
|
2026-02-03 08:40:54 +00:00
|
|
|
const addToCart = useCartStore((state) => state.addItem);
|
2026-01-13 18:47:57 +00:00
|
|
|
const { addToast } = useToast();
|
2026-02-09 22:04:35 +00:00
|
|
|
const [isLoading] = useState(false);
|
2026-01-13 18:47:57 +00:00
|
|
|
const [wishlist, setWishlist] = useState<Product[]>(MOCK_WISHLIST);
|
|
|
|
|
const [playingPreview, setPlayingPreview] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const handleRemove = (id: string) => {
|
|
|
|
|
setWishlist((prev) => prev.filter((p) => p.id !== id));
|
|
|
|
|
addToast('Removed from wishlist', 'info');
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleAddToCart = (product: Product) => {
|
|
|
|
|
addToCart(product);
|
|
|
|
|
handleRemove(product.id);
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleAddAll = () => {
|
|
|
|
|
wishlist.forEach((p) => addToCart(p));
|
|
|
|
|
setWishlist([]);
|
|
|
|
|
addToast('All items moved to cart', 'success');
|
|
|
|
|
};
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-02-09 22:04:35 +00:00
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-6xl mx-auto pb-20">
|
|
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4 mb-8">
|
|
|
|
|
<div>
|
|
|
|
|
<Skeleton className="h-9 w-48 mb-2" />
|
|
|
|
|
<Skeleton variant="text" className="w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
<Skeleton className="h-10 w-44" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{[1, 2, 3].map((i) => (
|
|
|
|
|
<Skeleton key={i} className="h-96 w-full rounded-lg" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
if (wishlist.length === 0) {
|
|
|
|
|
return (
|
2026-02-08 23:00:21 +00:00
|
|
|
<EmptyState
|
|
|
|
|
icon={<Heart className="w-full h-full" />}
|
|
|
|
|
title="Your wishlist is empty"
|
|
|
|
|
description="Save items you want to listen to later or purchase in the future."
|
|
|
|
|
size="lg"
|
|
|
|
|
className="min-h-96 animate-fadeIn"
|
|
|
|
|
/>
|
2026-01-13 18:47:57 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="animate-fadeIn max-w-6xl mx-auto pb-20">
|
2026-02-07 15:07:09 +00:00
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4 mb-8">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-display font-bold text-white mb-2">
|
|
|
|
|
WISHLIST
|
|
|
|
|
</h1>
|
2026-02-07 15:07:09 +00:00
|
|
|
<p className="text-muted-foreground font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
{wishlist.length} saved items
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
icon={<ShoppingCart className="w-4 h-4" />}
|
|
|
|
|
onClick={handleAddAll}
|
|
|
|
|
>
|
|
|
|
|
ADD ALL TO CART
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{wishlist.map((product) => (
|
|
|
|
|
<Card
|
|
|
|
|
key={product.id}
|
|
|
|
|
variant="default"
|
2026-02-07 15:07:09 +00:00
|
|
|
className="p-4 group hover:border-border/50 transition-all"
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
|
|
|
|
<div className="flex gap-4">
|
2026-02-07 15:07:09 +00:00
|
|
|
<div className="relative w-24 h-24 bg-muted rounded-lg overflow-hidden flex-shrink-0">
|
2026-01-13 18:47:57 +00:00
|
|
|
<img
|
|
|
|
|
src={product.coverUrl}
|
feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):
- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
(max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:15:58 +00:00
|
|
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-[var(--duration-slow)]"
|
2026-01-13 18:47:57 +00:00
|
|
|
/>
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setPlayingPreview(
|
|
|
|
|
playingPreview === product.id ? null : product.id,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{playingPreview === product.id ? (
|
|
|
|
|
<Pause className="w-8 h-8 text-white" />
|
|
|
|
|
) : (
|
|
|
|
|
<Play className="w-8 h-8 text-white fill-current" />
|
|
|
|
|
)}
|
2026-01-07 18:39:21 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
{product.isHot && (
|
2026-02-07 15:07:09 +00:00
|
|
|
<div className="absolute top-1 left-1 bg-warning text-warning-foreground text-xs font-bold px-1.5 py-0.5 rounded">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Zap className="w-2 h-2 inline" /> HOT
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex-1 min-w-0 flex flex-col justify-between">
|
2026-01-07 18:39:21 +00:00
|
|
|
<div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<h3 className="font-bold text-white truncate">
|
|
|
|
|
{product.title}
|
|
|
|
|
</h3>
|
2026-02-07 15:07:09 +00:00
|
|
|
<p className="text-xs text-muted-foreground truncate">
|
2026-01-13 18:47:57 +00:00
|
|
|
{product.author}
|
|
|
|
|
</p>
|
2026-02-07 15:07:09 +00:00
|
|
|
<p className="text-xs text-muted-foreground mt-1 capitalize">
|
2026-01-13 18:47:57 +00:00
|
|
|
{product.type}
|
|
|
|
|
</p>
|
2026-01-07 18:39:21 +00:00
|
|
|
</div>
|
2026-02-07 15:07:09 +00:00
|
|
|
<div className="text-lg font-mono font-bold text-muted-foreground">
|
2026-01-13 18:47:57 +00:00
|
|
|
${product.price}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="flex gap-2 mt-4 pt-4 border-t border-border/30">
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="flex-1"
|
|
|
|
|
onClick={() => handleAddToCart(product)}
|
|
|
|
|
>
|
|
|
|
|
Add to Cart
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
2026-02-07 15:07:09 +00:00
|
|
|
className="text-muted-foreground hover:text-destructive"
|
2026-01-13 18:47:57 +00:00
|
|
|
onClick={() => handleRemove(product.id)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4" />
|
|
|
|
|
</Button>
|
2026-01-07 18:39:21 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</Card>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|