Files
lidify/frontend/app/search/page.tsx
2025-12-25 18:58:06 -06:00

253 lines
11 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import { SearchIcon } from "lucide-react";
import { useSearchData } from "@/features/search/hooks/useSearchData";
import { useSoulseekSearch } from "@/features/search/hooks/useSoulseekSearch";
import { SearchFilters } from "@/features/search/components/SearchFilters";
import { TopResult } from "@/features/search/components/TopResult";
import { EmptyState } from "@/features/search/components/EmptyState";
import { LibraryAlbumsGrid } from "@/features/search/components/LibraryAlbumsGrid";
import { LibraryPodcastsGrid } from "@/features/search/components/LibraryPodcastsGrid";
import { LibraryTracksList } from "@/features/search/components/LibraryTracksList";
import { SimilarArtistsGrid } from "@/features/search/components/SimilarArtistsGrid";
import { SoulseekSongsList } from "@/features/search/components/SoulseekSongsList";
import { TVSearchInput } from "@/features/search/components/TVSearchInput";
import type { FilterTab } from "@/features/search/types";
export default function SearchPage() {
const searchParams = useSearchParams();
const router = useRouter();
const [filterTab, setFilterTab] = useState<FilterTab>("all");
const [query, setQuery] = useState("");
// Custom hooks
const {
libraryResults,
discoverResults,
isLibrarySearching,
isDiscoverSearching,
hasSearched,
} = useSearchData({ query });
const {
soulseekResults,
isSoulseekSearching,
isSoulseekPolling,
soulseekEnabled,
downloadingFiles,
handleDownload,
} = useSoulseekSearch({ query });
// Read query from URL params on mount
useEffect(() => {
const q = searchParams.get("q");
if (q) {
setQuery(q);
}
}, [searchParams]);
// Derived state
const topArtist = discoverResults.find((r) => r.type === "music");
const isLoading =
isLibrarySearching ||
isDiscoverSearching ||
isSoulseekSearching ||
isSoulseekPolling;
const showLibrary = filterTab === "all" || filterTab === "library";
const showDiscover = filterTab === "all" || filterTab === "discover";
const showSoulseek = filterTab === "all" || filterTab === "soulseek";
// Handle TV search
const handleTVSearch = (searchQuery: string) => {
setQuery(searchQuery);
router.push(`/search?q=${encodeURIComponent(searchQuery)}`);
};
return (
<div className="min-h-screen px-6 py-6">
{/* TV Search Input - only visible in TV mode */}
<TVSearchInput initialQuery={query} onSearch={handleTVSearch} />
<SearchFilters
filterTab={filterTab}
onFilterChange={setFilterTab}
soulseekEnabled={soulseekEnabled}
hasSearched={hasSearched}
/>
<div className="pb-24 space-y-12">
<EmptyState hasSearched={hasSearched} isLoading={isLoading} />
{/* Loading spinner */}
{hasSearched &&
(isLibrarySearching ||
isDiscoverSearching ||
isSoulseekSearching) &&
(!libraryResults || !libraryResults.artists?.length) &&
discoverResults.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 relative z-10">
<div className="relative w-16 h-16 mb-4">
<svg
className="w-16 h-16 animate-spin"
viewBox="0 0 64 64"
>
<defs>
<linearGradient
id="spinnerGrad"
x1="0%"
y1="0%"
x2="100%"
y2="100%"
>
<stop
offset="0%"
style={{
stopColor: "#facc15",
stopOpacity: 1,
}}
/>
<stop
offset="25%"
style={{
stopColor: "#f59e0b",
stopOpacity: 1,
}}
/>
<stop
offset="50%"
style={{
stopColor: "#c026d3",
stopOpacity: 1,
}}
/>
<stop
offset="75%"
style={{
stopColor: "#a855f7",
stopOpacity: 1,
}}
/>
<stop
offset="100%"
style={{
stopColor: "#facc15",
stopOpacity: 1,
}}
/>
</linearGradient>
</defs>
<circle
cx="32"
cy="32"
r="28"
fill="none"
stroke="url(#spinnerGrad)"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray="140 40"
/>
</svg>
</div>
<p className="text-gray-400 text-sm">
{isSoulseekSearching || isSoulseekPolling
? `Searching... (${soulseekResults.length} found)`
: "Searching..."}
</p>
</div>
)}
{/* Top Result */}
{hasSearched &&
(showDiscover || showLibrary) &&
(libraryResults?.artists?.[0] || topArtist) && (
<TopResult
libraryArtist={libraryResults?.artists?.[0]}
discoveryArtist={topArtist}
/>
)}
{/* Soulseek Songs */}
{hasSearched && showSoulseek && soulseekResults.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-white mb-6">
Songs
</h2>
<SoulseekSongsList
soulseekResults={soulseekResults}
downloadingFiles={downloadingFiles}
onDownload={handleDownload}
/>
</section>
)}
{/* Library Songs */}
{hasSearched &&
showLibrary &&
libraryResults?.tracks?.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-white mb-6">
Songs in Your Library
</h2>
<LibraryTracksList tracks={libraryResults.tracks} />
</section>
)}
{/* Library Albums */}
{hasSearched &&
showLibrary &&
libraryResults?.albums?.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-white mb-6">
Your Albums
</h2>
<LibraryAlbumsGrid albums={libraryResults.albums} />
</section>
)}
{/* Library Podcasts */}
{hasSearched &&
showLibrary &&
libraryResults?.podcasts?.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-white mb-6">
Podcasts
</h2>
<LibraryPodcastsGrid
podcasts={libraryResults.podcasts}
/>
</section>
)}
{/* Similar Artists */}
{hasSearched &&
showDiscover &&
discoverResults.filter((r) => r.type === "music").length >
1 && (
<SimilarArtistsGrid discoverResults={discoverResults} />
)}
{/* No Results */}
{hasSearched &&
!isLoading &&
!topArtist &&
soulseekResults.length === 0 &&
(!libraryResults ||
(!libraryResults.artists?.length &&
!libraryResults.albums?.length &&
!libraryResults.tracks?.length)) && (
<div className="flex flex-col items-center justify-center py-24 text-center">
<SearchIcon className="w-16 h-16 text-gray-700 mb-4" />
<h3 className="text-xl font-bold text-white mb-2">
No results found
</h3>
<p className="text-gray-400">
Try searching for something else
</p>
</div>
)}
</div>
</div>
);
}