veza/apps/web/src/features/search/components/search-page/SearchPageResults.tsx
senke 4b57b46bac feat: frontend improvements — UI polish, player bar, auth flow, i18n
- 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>
2026-03-18 11:35:44 +01:00

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>
);
}