From 3abc864abdeedab44a0b32f147b1ce445d3c81d9 Mon Sep 17 00:00:00 2001 From: benya Date: Fri, 3 Apr 2026 01:40:30 +0300 Subject: [PATCH] feat: add search results page and richer navigation --- apps/web/src/App.tsx | 2 + apps/web/src/components/command-palette.tsx | 14 +- apps/web/src/pages/albums-page.tsx | 5 +- apps/web/src/pages/artists-page.tsx | 10 +- apps/web/src/pages/search-page.tsx | 165 ++++++++++++++++++++ apps/web/src/pages/tracks-page.tsx | 8 +- 6 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/pages/search-page.tsx diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index bca0c51..0d42ab6 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -10,6 +10,7 @@ import { HomePage } from '@/pages/home-page' import { LoginPage } from '@/pages/login-page' import { PlaylistDetailPage } from '@/pages/playlist-detail-page' import { PlaylistsPage } from '@/pages/playlists-page' +import { SearchPage } from '@/pages/search-page' import { TracksPage } from '@/pages/tracks-page' import { useSessionStore } from '@/stores/session-store' @@ -33,6 +34,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> diff --git a/apps/web/src/components/command-palette.tsx b/apps/web/src/components/command-palette.tsx index 3507147..f2fd7b4 100644 --- a/apps/web/src/components/command-palette.tsx +++ b/apps/web/src/components/command-palette.tsx @@ -150,11 +150,21 @@ export function CommandPalette({ label={track.title} meta={`${track.artistName} • ${track.albumTitle}`} onClick={() => { - navigate('/tracks') + navigate(`/search?q=${encodeURIComponent(deferredQuery)}`) onClose() }} /> ))} + {(searchQuery.data?.artists.length || searchQuery.data?.albums.length || searchQuery.data?.tracks.length) ? ( + { + navigate(`/search?q=${encodeURIComponent(deferredQuery)}`) + onClose() + }} + /> + ) : null} ) : ( @@ -177,7 +187,7 @@ export function CommandPalette({ return } if (command.action === 'navigate') { - navigate('/tracks') + navigate('/search') onClose() return } diff --git a/apps/web/src/pages/albums-page.tsx b/apps/web/src/pages/albums-page.tsx index 3e3f7a8..1cad891 100644 --- a/apps/web/src/pages/albums-page.tsx +++ b/apps/web/src/pages/albums-page.tsx @@ -1,9 +1,10 @@ import { useQuery } from '@tanstack/react-query' import { Search } from 'lucide-react' import { coverArtUrl, fetchAlbums } from '@/lib/api' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' export function AlbumsPage() { + const navigate = useNavigate() const albumsQuery = useQuery({ queryKey: ['albums'], queryFn: fetchAlbums, @@ -17,7 +18,7 @@ export function AlbumsPage() { - diff --git a/apps/web/src/pages/artists-page.tsx b/apps/web/src/pages/artists-page.tsx index 7ba4330..6566e57 100644 --- a/apps/web/src/pages/artists-page.tsx +++ b/apps/web/src/pages/artists-page.tsx @@ -1,9 +1,10 @@ import { useQuery } from '@tanstack/react-query' -import { SlidersHorizontal } from 'lucide-react' +import { Search, SlidersHorizontal } from 'lucide-react' import { coverArtUrl, fetchArtists } from '@/lib/api' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' export function ArtistsPage() { + const navigate = useNavigate() const artistsQuery = useQuery({ queryKey: ['artists'], queryFn: fetchArtists, @@ -20,7 +21,10 @@ export function ArtistsPage() { -
Поиск...
+
diff --git a/apps/web/src/pages/search-page.tsx b/apps/web/src/pages/search-page.tsx new file mode 100644 index 0000000..ea9b250 --- /dev/null +++ b/apps/web/src/pages/search-page.tsx @@ -0,0 +1,165 @@ +import { useQuery } from '@tanstack/react-query' +import { Search } from 'lucide-react' +import { FormEvent, useState } from 'react' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' +import { FavoriteToggle } from '@/components/favorite-toggle' +import { coverArtUrl, fetchFavorites, searchLibrary } from '@/lib/api' +import { usePlayerStore } from '@/stores/player-store' + +export function SearchPage() { + const navigate = useNavigate() + const [params] = useSearchParams() + const initialQuery = params.get('q') ?? '' + const [query, setQuery] = useState(initialQuery) + const setQueue = usePlayerStore((state) => state.setQueue) + const playTrack = usePlayerStore((state) => state.playTrack) + + const searchQuery = useQuery({ + queryKey: ['search-page', initialQuery], + queryFn: () => searchLibrary(initialQuery), + enabled: initialQuery.trim().length > 0, + }) + const favoritesQuery = useQuery({ + queryKey: ['favorites'], + queryFn: fetchFavorites, + }) + + const results = searchQuery.data + const favoriteTrackIds = new Set((favoritesQuery.data?.tracks ?? []).map((item) => item.id)) + const favoriteAlbumIds = new Set((favoritesQuery.data?.albums ?? []).map((item) => item.id)) + const favoriteArtistIds = new Set((favoritesQuery.data?.artists ?? []).map((item) => item.id)) + + function onSubmit(event: FormEvent) { + event.preventDefault() + navigate(`/search?q=${encodeURIComponent(query.trim())}`) + } + + return ( +
+
+
+ + setQuery(event.target.value)} + placeholder="Поиск альбома, исполнителя или трека" + value={query} + /> +
+ +
+ + {!initialQuery ? ( +
+ Введи название трека, альбома или исполнителя. +
+ ) : null} + + {results?.artists.length ? ( +
+
Исполнители
+
+ {results.artists.map((artist) => ( + +
+
+ {artist.coverArtId ? {artist.name} : null} +
+
+
{artist.name}
+
{artist.albumCount} альбомов
+
+
+ + + ))} +
+
+ ) : null} + + {results?.albums.length ? ( +
+
Альбомы
+
+ {results.albums.map((album) => ( +
+ + {album.coverArtId ? {album.title} : null} + +
+
+ + {album.title} + +
{album.artistName}
+
+ +
+
+ ))} +
+
+ ) : null} + + {results?.tracks.length ? ( +
+
Треки
+
+
+
#
+
Название
+
Альбом
+
+
+
+ {results.tracks.map((track, index) => ( + + ))} +
+
+ ) : null} + + {initialQuery && !searchQuery.isLoading && !results?.artists.length && !results?.albums.length && !results?.tracks.length ? ( +
+ Ничего не найдено по запросу "{initialQuery}". +
+ ) : null} +
+ ) +} + +function formatDuration(durationSeconds: number) { + const minutes = Math.floor(durationSeconds / 60) + const seconds = durationSeconds % 60 + return `${minutes}:${seconds.toString().padStart(2, '0')}` +} diff --git a/apps/web/src/pages/tracks-page.tsx b/apps/web/src/pages/tracks-page.tsx index 37ddd15..65b45cc 100644 --- a/apps/web/src/pages/tracks-page.tsx +++ b/apps/web/src/pages/tracks-page.tsx @@ -2,9 +2,11 @@ import { useQuery } from '@tanstack/react-query' import { Search } from 'lucide-react' import { FavoriteToggle } from '@/components/favorite-toggle' import { coverArtUrl, fetchFavorites, fetchTracks } from '@/lib/api' +import { useNavigate } from 'react-router-dom' import { usePlayerStore } from '@/stores/player-store' export function TracksPage() { + const navigate = useNavigate() const setQueue = usePlayerStore((state) => state.setQueue) const playTrack = usePlayerStore((state) => state.playTrack) const tracksQuery = useQuery({ @@ -21,7 +23,7 @@ export function TracksPage() { return (
- + navigate('/search')} />
#
@@ -74,10 +76,10 @@ export function TracksPage() { ) } -function HeaderSearch() { +function HeaderSearch({ onClick }: { onClick: () => void }) { return (
-