veza/apps/web/src/components/views/DiscoverView.tsx

189 lines
11 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Play, Heart, TrendingUp, Sparkles, Calendar, ArrowRight, Disc, Loader2 } from 'lucide-react';
import { Track } from '../../types';
import { useAudio } from '../../context/AudioContext';
import { trackService } from '../../services/trackService';
import { useToast } from '../../context/ToastContext';
import { logger } from '@/utils/logger';
const GENRES = [
{ name: 'Synthwave', color: 'from-pink-500 to-purple-600' },
{ name: 'Techno', color: 'from-gray-700 to-black' },
{ name: 'Ambient', color: 'from-blue-400 to-teal-500' },
{ name: 'Lo-Fi', color: 'from-orange-300 to-red-400' },
{ name: 'Drum & Bass', color: 'from-yellow-400 to-orange-600' },
{ name: 'House', color: 'from-indigo-500 to-blue-600' },
];
export const DiscoverView: React.FC = () => {
2026-01-07 18:39:21 +00:00
const { playTrack } = useAudio();
const { addToast } = useToast();
const [hoveredTrack, setHoveredTrack] = useState<string | null>(null);
const [trending, setTrending] = useState<Track[]>([]);
const [newReleases, setNewReleases] = useState<Track[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const [trendingRes, newRes] = await Promise.all([
trackService.list({ sort_by: 'trending', limit: 5 }),
trackService.list({ sort_by: 'created_at', limit: 4 })
]);
setTrending(trendingRes.tracks);
setNewReleases(newRes.tracks);
} catch (e) {
logger.error('Failed to load discovery data', {
error: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
});
// Mock fallback
setTrending([
{ id: 't1', title: 'Midnight City', artist: 'M83', coverUrl: 'https://picsum.photos/id/10/300/300', duration: '4:03', durationSec: 243, play_count: 500000, like_count: 20000 } as unknown as Track,
{ id: 't2', title: 'Nightcall', artist: 'Kavinsky', coverUrl: 'https://picsum.photos/id/20/300/300', duration: '4:18', durationSec: 258, play_count: 450000, like_count: 18000 } as unknown as Track,
]);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const handlePlay = (track: Track) => {
playTrack(track, trending);
};
if (loading) {
return (
<div className="flex h-[50vh] items-center justify-center">
<Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" />
</div>
2026-01-07 18:39:21 +00:00
);
}
return (
<div className="animate-fadeIn space-y-12 pb-20">
{/* Hero / For You */}
<section>
<div className="flex items-center gap-2 mb-6">
<Sparkles className="w-5 h-5 text-kodo-cyan" />
<h2 className="text-2xl font-display font-bold text-white">For You</h2>
</div>
2026-01-07 18:39:21 +00:00
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card variant="gaming" className="col-span-1 md:col-span-2 relative overflow-hidden group cursor-pointer h-64 flex items-end p-8 border-none" onClick={() => addToast("Playing Discovery Weekly")}>
<img src="https://picsum.photos/id/88/800/400" className="absolute inset-0 w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 opacity-60 group-hover:opacity-40" />
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent"></div>
<div className="relative z-10 w-full">
<div className="text-kodo-cyan text-xs font-bold uppercase tracking-widest mb-2">Weekly Mix</div>
<h3 className="text-3xl font-bold text-white mb-2">Discover Weekly</h3>
<p className="text-gray-300 text-sm mb-4 line-clamp-1">Fresh tracks curated based on your listening history.</p>
<Button variant="primary" icon={<Play className="w-4 h-4 fill-current" />}>Play Now</Button>
</div>
2026-01-07 18:39:21 +00:00
</Card>
<Card variant="default" className="relative overflow-hidden group cursor-pointer h-64 p-6 flex flex-col justify-end">
<img src="https://picsum.photos/id/99/400/400" className="absolute inset-0 w-full h-full object-cover opacity-50 group-hover:opacity-30 transition-all" />
<div className="absolute inset-0 bg-gradient-to-t from-kodo-ink to-transparent"></div>
<div className="relative z-10">
<h3 className="text-xl font-bold text-white mb-1">New Arrivals</h3>
<p className="text-xs text-gray-400">Best of the week</p>
</div>
2026-01-07 18:39:21 +00:00
<div className="absolute top-4 right-4 bg-kodo-lime text-black text-[10px] font-bold px-2 py-1 rounded">UPDATED</div>
</Card>
</div>
</section>
{/* Trending Now */}
<section>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-kodo-magenta" />
<h2 className="text-2xl font-display font-bold text-white">Trending Now</h2>
</div>
2026-01-07 18:39:21 +00:00
<Button variant="ghost" size="sm" className="text-gray-400 hover:text-white">View All <ArrowRight className="w-4 h-4 ml-1" /></Button>
</div>
2026-01-07 18:39:21 +00:00
<div className="space-y-2">
{trending.map((track, i) => (
<div
key={track.id}
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 group transition-colors cursor-pointer"
onMouseEnter={() => setHoveredTrack(track.id)}
onMouseLeave={() => setHoveredTrack(null)}
onClick={() => handlePlay(track)}
>
<div className="w-8 text-center font-bold text-gray-600">{i + 1}</div>
<div className="relative w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
<img src={track.coverUrl} className="w-full h-full object-cover" />
<div className={`absolute inset-0 bg-black/50 flex items-center justify-center transition-opacity ${hoveredTrack === track.id ? 'opacity-100' : 'opacity-0'}`}>
<Play className="w-5 h-5 text-white fill-current" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="text-white font-bold truncate">{track.title}</div>
<div className="text-gray-400 text-xs truncate">{track.artist}</div>
</div>
2026-01-07 18:39:21 +00:00
<div className="text-gray-500 text-xs font-mono hidden md:block">{track.play_count.toLocaleString()} plays</div>
<div className="text-gray-500 text-xs font-mono">{track.duration}</div>
<button className="text-gray-500 hover:text-kodo-magenta p-2 opacity-0 group-hover:opacity-100 transition-opacity" onClick={(e) => { e.stopPropagation(); addToast("Liked"); }}><Heart className="w-4 h-4" /></button>
</div>
2026-01-07 18:39:21 +00:00
))}
</div>
</section>
2026-01-07 18:39:21 +00:00
{/* New Releases */}
<section>
<div className="flex items-center gap-2 mb-6">
<Calendar className="w-5 h-5 text-kodo-gold" />
<h2 className="text-2xl font-display font-bold text-white">New Releases</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{newReleases.map(release => (
<div key={release.id} className="group cursor-pointer" onClick={() => handlePlay(release)}>
<div className="aspect-square rounded-xl overflow-hidden mb-3 relative shadow-lg">
<img src={release.coverUrl} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
<div className="absolute inset-0 bg-black/20 group-hover:bg-transparent transition-colors"></div>
<div className="absolute bottom-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="primary" size="icon" className="h-8 w-8"><Play className="w-4 h-4 fill-current" /></Button>
</div>
</div>
<h3 className="font-bold text-white truncate group-hover:text-kodo-gold transition-colors">{release.title}</h3>
<p className="text-xs text-gray-400">{release.artist}</p>
</div>
2026-01-07 18:39:21 +00:00
))}
</div>
</section>
{/* Popular Genres */}
<section>
<div className="flex items-center gap-2 mb-6">
<Disc className="w-5 h-5 text-gray-400" />
<h2 className="text-2xl font-display font-bold text-white">Browse by Genre</h2>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{GENRES.map(genre => (
<div
key={genre.name}
className={`aspect-square rounded-xl bg-gradient-to-br ${genre.color} p-4 relative overflow-hidden cursor-pointer hover:scale-105 transition-transform duration-300`}
onClick={() => addToast(`Browsing ${genre.name}`)}
>
<span className="font-bold text-white text-lg relative z-10">{genre.name}</span>
<div className="absolute -bottom-2 -right-2 opacity-30 transform rotate-12">
<Disc className="w-16 h-16 text-white" />
</div>
</div>
))}
</div>
</section>
</div>
);
};