157 lines
7.6 KiB
TypeScript
157 lines
7.6 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Button } from '../ui/button';
|
|
import { SearchInput } from '../ui/input';
|
|
import { Play, Heart, Filter, Zap, TrendingUp, Star, Loader2, Clock } from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { trackService } from '../../services/trackService';
|
|
import { socialService } from '../../services/socialService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
// Derived mock type for explore grid
|
|
interface ExploreItem {
|
|
id: string;
|
|
type: 'image' | 'audio' | 'video';
|
|
thumbnail: string;
|
|
likes: number;
|
|
comments: number;
|
|
title: string;
|
|
author: string;
|
|
}
|
|
|
|
// 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 ExploreView: React.FC = () => {
|
|
const { addToast } = useToast();
|
|
const [activeTab, setActiveTab] = useState<'for_you' | 'trending' | 'new' | 'popular'>('for_you');
|
|
const [filter, setFilter] = useState('All');
|
|
const [items, setItems] = useState<ExploreItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Aggregate data from tracks and social feed to simulate explore grid
|
|
const [tracksRes, feedRes] = await Promise.all([
|
|
trackService.list({ sort_by: 'trending', limit: 6 }),
|
|
socialService.getFeed({ limit: 6 })
|
|
]);
|
|
|
|
const trackItems: ExploreItem[] = tracksRes.tracks.map(t => ({
|
|
id: t.id,
|
|
type: 'audio',
|
|
thumbnail: t.coverUrl || '',
|
|
likes: t.like_count,
|
|
comments: 0,
|
|
title: t.title,
|
|
author: t.artist
|
|
}));
|
|
|
|
const postItems: ExploreItem[] = feedRes.posts.map(p => ({
|
|
id: p.id,
|
|
type: (p.type === 'image' || p.type === 'video') ? p.type : 'image', // Fallback to image for layout
|
|
thumbnail: p.image || p.audioTrack?.coverUrl || p.author.avatar,
|
|
likes: p.likes,
|
|
comments: p.comments,
|
|
title: `${p.content.substring(0, 30)}...`,
|
|
author: p.author.name
|
|
}));
|
|
|
|
setItems([...trackItems, ...postItems].sort(() => 0.5 - Math.random()));
|
|
} catch (e) {
|
|
logger.error('Error loading explore data', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
activeTab,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [activeTab]);
|
|
|
|
return (
|
|
<div className="space-y-6 animate-fadeIn">
|
|
{/* Navigation & Search */}
|
|
<div className="flex flex-col md:flex-row justify-between items-center gap-4 bg-kodo-ink/50 p-2 rounded-xl border border-kodo-steel/50">
|
|
<div className="flex gap-2 overflow-x-auto w-full md:w-auto p-1">
|
|
{[
|
|
{ id: 'for_you', label: 'For You', icon: <Zap className="w-4 h-4" /> },
|
|
{ id: 'trending', label: 'Trending', icon: <TrendingUp className="w-4 h-4" /> },
|
|
{ id: 'new', label: 'New', icon: <Clock className="w-4 h-4" /> },
|
|
{ id: 'popular', label: 'Popular', icon: <Star className="w-4 h-4" /> },
|
|
].map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as any)}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-bold transition-all whitespace-nowrap ${activeTab === tab.id ? 'bg-kodo-cyan text-black shadow-neon-cyan' : 'text-gray-400 hover:text-white hover:bg-white/5'}`}
|
|
>
|
|
{tab.icon} {tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-2 w-full md:w-auto">
|
|
<div className="w-full md:w-64">
|
|
<SearchInput placeholder="Search explore..." />
|
|
</div>
|
|
<Button variant="ghost" size="icon" className="border border-kodo-steel"><Filter className="w-4 h-4" /></Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
{['All', 'Images', 'Audio', 'Video', 'Polls'].map(f => (
|
|
<button
|
|
key={f}
|
|
onClick={() => setFilter(f)}
|
|
className={`px-3 py-1 rounded-full text-xs font-bold border transition-colors ${filter === f ? 'bg-white text-black border-white' : 'border-gray-600 text-gray-400 hover:border-gray-400'}`}
|
|
>
|
|
{f}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Grid Content */}
|
|
{loading ? (
|
|
<div className="flex justify-center py-20"><Loader2 className="w-10 h-10 text-kodo-cyan animate-spin" /></div>
|
|
) : (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
{items.map((item, i) => (
|
|
<div
|
|
key={item.id}
|
|
className={`relative group cursor-pointer overflow-hidden rounded-xl bg-gray-900 aspect-square ${i === 0 ? 'col-span-2 row-span-2' : ''}`}
|
|
onClick={() => addToast(`Opening ${item.title}`)}
|
|
>
|
|
<img src={item.thumbnail} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110 opacity-80 group-hover:opacity-100" />
|
|
|
|
{/* Overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-4">
|
|
<h4 className="text-white font-bold truncate text-sm mb-1">{item.title}</h4>
|
|
<div className="flex justify-between items-center text-xs text-gray-300">
|
|
<span>@{item.author}</span>
|
|
<div className="flex gap-2">
|
|
<span className="flex items-center gap-1"><Heart className="w-3 h-3 fill-current" /> {item.likes}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Type Indicator */}
|
|
<div className="absolute top-2 right-2 bg-black/50 backdrop-blur p-1.5 rounded-full text-white">
|
|
{item.type === 'audio' ? <Play className="w-3 h-3 fill-current" /> : item.type === 'video' ? <Play className="w-3 h-3" /> : <div className="w-3 h-3 bg-white rounded-full"></div>}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|