- Header, Sidebar, Toast, Dropdown, EmptyState component refinements - Auth flow: LoginPage, RegisterPage, AuthInput, AuthLayout improvements - Player bar: glass effect, progress, track info, controls enhancements - Dashboard, Discover, Search pages updates - PlaylistCard, TrackCard component improvements - Auth store and API interceptors hardening - i18n: updated en/es/fr locale files - CSS additions in index.css - Package.json and vite config updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
219 lines
9.9 KiB
TypeScript
219 lines
9.9 KiB
TypeScript
import { useNavigate } from 'react-router-dom';
|
|
import { Card } from '@/components/ui/card';
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
import { Avatar } from '@/components/ui/avatar';
|
|
import { Music, User } from 'lucide-react';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import type { SearchResults } from '@/types/search';
|
|
import { highlightMatch } from '../../utils/highlightMatch';
|
|
|
|
interface SearchPageResultsProps {
|
|
results: SearchResults;
|
|
query?: string;
|
|
activeTab?: string;
|
|
onTabChange?: (value: string) => void;
|
|
}
|
|
|
|
export function SearchPageResults({ results, query = '', activeTab = 'all', onTabChange }: SearchPageResultsProps) {
|
|
const navigate = useNavigate();
|
|
|
|
const tracks = tracks ?? [];
|
|
const artists = artists ?? [];
|
|
const playlists = playlists ?? [];
|
|
|
|
const totalResults = tracks.length + artists.length + playlists.length;
|
|
|
|
return (
|
|
<div aria-live="polite" aria-atomic="true">
|
|
<div role="status" className="sr-only">
|
|
{totalResults} result{totalResults !== 1 ? 's' : ''} found
|
|
</div>
|
|
<Tabs value={activeTab} onValueChange={onTabChange ?? (() => {})} className="w-full">
|
|
<TabsList className="bg-transparent border-b border-white/10 w-full justify-start h-auto p-0 gap-8 mb-8">
|
|
<TabsTrigger
|
|
value="all"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-heading bg-transparent"
|
|
>
|
|
All Results
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="tracks"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-heading bg-transparent"
|
|
>
|
|
Tracks ({tracks.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="artists"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-heading bg-transparent"
|
|
>
|
|
Artists ({artists.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="playlists"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-primary py-3 px-0 text-lg font-heading bg-transparent"
|
|
>
|
|
Playlists ({playlists.length})
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all" className="space-y-12">
|
|
{tracks.length > 0 && (
|
|
<section>
|
|
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
<Music className="w-5 h-5 text-primary" /> Top Tracks
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{tracks.slice(0, 6).map((track) => (
|
|
<Card
|
|
key={track.id}
|
|
variant="glass"
|
|
tabIndex={0}
|
|
className="p-3 flex items-center gap-4 hover:bg-white/5 transition-colors cursor-pointer group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={() => navigate(`/tracks/${track.id}`)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/tracks/${track.id}`); } }}
|
|
>
|
|
<div className="w-16 h-16 rounded-lg bg-black/40 overflow-hidden flex-shrink-0">
|
|
{track.cover_art_path ? (
|
|
<img
|
|
src={track.cover_art_path}
|
|
alt=""
|
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform"
|
|
/>
|
|
) : (
|
|
<Music className="w-full h-full p-4 text-white/20" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-bold truncate group-hover:text-primary transition-colors">
|
|
{highlightMatch(track.title, query)}
|
|
</h4>
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{highlightMatch(track.artist, query)}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
{artists.length > 0 && (
|
|
<section>
|
|
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
<User className="w-5 h-5 text-primary" /> Artists
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
|
{artists.slice(0, 5).map((artist) => (
|
|
<Card
|
|
key={artist.id}
|
|
variant="glass"
|
|
tabIndex={0}
|
|
className="p-4 flex flex-col items-center text-center hover:bg-white/5 transition-colors cursor-pointer group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={() => navigate(`/u/${artist.username}`)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/u/${artist.username}`); } }}
|
|
>
|
|
<Avatar
|
|
src={artist.avatar_url}
|
|
fallback={artist.username[0]}
|
|
className="w-24 h-24 mb-4 shadow-lg group-hover:scale-105 transition-transform"
|
|
/>
|
|
<h4 className="font-bold truncate w-full group-hover:text-primary transition-colors">
|
|
{highlightMatch(artist.username, query)}
|
|
</h4>
|
|
{/* Follower count removed from public display (ORIGIN_UI_UX_SYSTEM §13.4) */}
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="tracks">
|
|
<div className="grid grid-cols-1 gap-2">
|
|
{tracks.map((track) => (
|
|
<button
|
|
type="button"
|
|
key={track.id}
|
|
tabIndex={0}
|
|
className="appearance-none bg-transparent border-0 p-0 text-left w-full flex items-center gap-4 p-4 rounded-xl border border-white/5 hover:bg-white/5 transition-colors cursor-pointer group bg-black/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
onClick={() => navigate(`/tracks/${track.id}`)}
|
|
>
|
|
<div className="w-12 h-12 rounded bg-black/40 overflow-hidden flex-shrink-0">
|
|
{track.cover_art_path && (
|
|
<img
|
|
src={track.cover_art_path}
|
|
alt=""
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-bold group-hover:text-primary">{highlightMatch(track.title, query)}</h4>
|
|
<p className="text-muted-foreground text-sm">{highlightMatch(track.artist, query)}</p>
|
|
</div>
|
|
<div className="text-xs font-mono text-muted-foreground flex-shrink-0">
|
|
{track.created_at ? `${formatDistanceToNow(new Date(track.created_at))} ago` : null}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="artists">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
{artists.map((artist) => (
|
|
<Card
|
|
key={artist.id}
|
|
variant="glass"
|
|
tabIndex={0}
|
|
className="p-6 flex flex-col items-center text-center hover:bg-white/5 cursor-pointer group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={() => navigate(`/u/${artist.username}`)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/u/${artist.username}`); } }}
|
|
>
|
|
<Avatar
|
|
src={artist.avatar_url}
|
|
fallback={artist.username[0]}
|
|
className="w-32 h-32 mb-4 shadow-lg group-hover:scale-105 transition-transform"
|
|
/>
|
|
<h4 className="font-bold text-lg group-hover:text-primary">
|
|
{highlightMatch(artist.username, query)}
|
|
</h4>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="playlists">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{playlists.map((playlist) => (
|
|
<Card
|
|
key={playlist.id}
|
|
variant="glass"
|
|
tabIndex={0}
|
|
className="p-0 overflow-hidden cursor-pointer group transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
onClick={() => navigate(`/playlists/${playlist.id}`)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(`/playlists/${playlist.id}`); } }}
|
|
>
|
|
<div className="h-32 bg-gradient-to-br from-primary/30 to-background relative">
|
|
{playlist.cover_url && (
|
|
<img
|
|
src={playlist.cover_url}
|
|
alt=""
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)}
|
|
<div className="absolute inset-0 bg-black/20 group-hover:bg-transparent transition-colors" />
|
|
</div>
|
|
<div className="p-4">
|
|
<h4 className="font-bold group-hover:text-primary">{highlightMatch(playlist.title, query)}</h4>
|
|
<p className="text-xs text-muted-foreground line-clamp-2 mt-1">
|
|
{playlist.description ?? 'No description'}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|