import { Search, Loader2, Music, Clock, Copy, Filter, ExternalLink } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { motion, AnimatePresence } from "framer-motion"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; interface Album { id: string; title: string; artists: { name: string }[]; duration: number; url: string; imageCover?: { url: string; width: number; height: number }[]; mediaTags?: string[]; releaseDate?: string; } export function TidalSearch() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [sortBy, setSortBy] = useState<"relevance" | "duration" | "year">("relevance"); const [filterQuality, setFilterQuality] = useState<"all" | "hires" | "lossless">("all"); const [countryCode, setCountryCode] = useState("US"); const [copiedId, setCopiedId] = useState(null); const timeoutRef = useRef(null); // Initial load useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); async function handleSearch() { if (!query.trim()) return; setLoading(true); setResults([]); try { const res = await fetch(`/api/tidal/search?query=${encodeURIComponent(query)}&countryCode=${countryCode}`); const data = await res.json(); if (data && data.albums) { setResults(data.albums); } else if (data && Array.isArray(data)) { setResults(data); } else { console.warn("Unexpected API response structure:", data); } } catch (error) { console.error("Search failed:", error); toast.error("Search failed. Please try again."); } finally { setLoading(false); } } const filteredResults = results.filter(album => { if (filterQuality === "all") return true; if (!album.mediaTags) return false; if (filterQuality === "hires") { return album.mediaTags.includes("HIRES_LOSSLESS"); } if (filterQuality === "lossless") { // Exclusive filter: Must be Lossless AND NOT Hi-Res/MQA const isHires = album.mediaTags.includes("HIRES_LOSSLESS"); return album.mediaTags.includes("LOSSLESS") && !isHires; } return true; }); const sortedResults = [...filteredResults].sort((a, b) => { if (sortBy === "duration") { return (b.duration || 0) - (a.duration || 0); } if (sortBy === "year") { const dateA = new Date(a.releaseDate || 0).getFullYear(); const dateB = new Date(b.releaseDate || 0).getFullYear(); return dateB - dateA; // Newest first } return 0; }); function formatDuration(seconds: number) { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m}:${s.toString().padStart(2, "0")}`; } const copyToClipboard = async (text: string, id: string) => { try { await navigator.clipboard.writeText(text); // Clear existing timeout to prevent premature hiding if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setCopiedId(id); toast.success("Copied to clipboard!"); timeoutRef.current = setTimeout(() => { setCopiedId(null); timeoutRef.current = null; }, 2000); } catch (err) { console.error("Failed to copy:", err); toast.error("Failed to copy link"); } }; function getYear(dateString?: string) { if (!dateString) return ""; return new Date(dateString).getFullYear().toString(); } return (
setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} className="flex-1 border-none shadow-none bg-transparent focus-visible:ring-0 h-9 font-medium" />
setCountryCode(e.target.value.toUpperCase().slice(0, 2))} className="w-14 border-none shadow-none bg-transparent focus-visible:ring-0 h-9 text-center font-mono text-xs uppercase text-muted-foreground focus:text-foreground transition-colors" maxLength={2} title="Region (ISO 3166-1 alpha-2)" />
{sortedResults.length === 0 && !loading && (

{query ? "No results found" : "Start your search above"}

)}
{sortedResults.map((album) => { const artworkUrl = album.imageCover && album.imageCover.length > 0 ? album.imageCover.find(img => img.width === 640)?.url || album.imageCover[0].url : null; return ( copyToClipboard(album.url, album.id)} >
{artworkUrl ? ( {album.title} ) : (
)} {/* Overlay */}
{copiedId === album.id ? (
COPIED
) : ( <> )}
{/* Quality Badges - Refined Layout */} {album.mediaTags && (
{album.mediaTags.includes("HIRES_LOSSLESS") && ( HI-RES )} {album.mediaTags.includes("MQA") && ( MQA )} {!album.mediaTags.includes("HIRES_LOSSLESS") && !album.mediaTags.includes("MQA") && album.mediaTags.includes("LOSSLESS") && ( LOSSLESS )} {album.mediaTags.includes("DOLBY_ATMOS") && ( ATMOS )} {album.mediaTags.includes("SONY_360RA") && ( 360RA )}
)}

{album.title}

{album.artists && album.artists.length > 0 ? ( album.artists.map((artist, index) => (
{ e.stopPropagation(); navigator.clipboard.writeText(artist.name); toast.success(`Copied "${artist.name}"`); }} > {artist.name} {index < album.artists.length - 1 && ( , )}
)) ) : ( Unknown Artist )}
{album.releaseDate && ( {getYear(album.releaseDate)} )} {formatDuration(album.duration) && ( <> {formatDuration(album.duration)} )}
); })}
); } function ScrollableContent({ children, className }: { children: React.ReactNode, className?: string }) { const [isHovered, setIsHovered] = useState(false); const containerRef = useRef(null); const contentRef = useRef(null); const [scrollDistance, setScrollDistance] = useState(0); const [duration, setDuration] = useState(0); useEffect(() => { if (containerRef.current && contentRef.current) { const containerWidth = containerRef.current.offsetWidth; const contentWidth = contentRef.current.offsetWidth; if (contentWidth > containerWidth) { const distance = contentWidth - containerWidth; setScrollDistance(distance); setDuration(distance * 0.01); // Adjust speed } else { setScrollDistance(0); } } }, [children]); return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > 0 ? -scrollDistance : 0 }} transition={{ duration: duration, ease: "linear", repeat: isHovered ? Infinity : 0, repeatType: "mirror", repeatDelay: 1 }} > {children}
); }