fix: improve query loading and error states
This commit is contained in:
@@ -630,7 +630,7 @@ Responsibilities:
|
|||||||
- [ ] Responsive navigation
|
- [ ] Responsive navigation
|
||||||
- [ ] Toast/notification system
|
- [ ] Toast/notification system
|
||||||
- [ ] Error boundary
|
- [ ] Error boundary
|
||||||
- [ ] Query loading/error patterns
|
- [x] Query loading/error patterns
|
||||||
|
|
||||||
## Frontend Music Views
|
## Frontend Music Views
|
||||||
|
|
||||||
|
|||||||
34
apps/web/src/components/query-state.tsx
Normal file
34
apps/web/src/components/query-state.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function LoadingPanel({ title = 'Загрузка...' }: { title?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-[320px] place-items-center rounded-[16px] border border-[#24314f] bg-[#121b2e]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-semibold tracking-tight text-white">{title}</div>
|
||||||
|
<div className="mt-3 text-base text-slate-400">Подтягиваю данные и подготавливаю экран.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorPanel({
|
||||||
|
title = 'Не удалось загрузить данные',
|
||||||
|
description = 'Проверь соединение с сервером и попробуй ещё раз.',
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
onRetry?: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-[320px] place-items-center rounded-[16px] border border-dashed border-[#31405f] bg-[#121b2e]">
|
||||||
|
<div className="max-w-xl text-center">
|
||||||
|
<div className="text-3xl font-semibold tracking-tight text-white">{title}</div>
|
||||||
|
<div className="mt-3 text-base text-slate-400">{description}</div>
|
||||||
|
{onRetry ? (
|
||||||
|
<button className="mt-6 rounded-[10px] bg-[#15c98b] px-6 py-3 text-base font-medium text-[#081225]" onClick={onRetry} type="button">
|
||||||
|
Повторить
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
||||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
@@ -20,8 +21,16 @@ export function AlbumDetailPage() {
|
|||||||
|
|
||||||
const album = albumQuery.data
|
const album = albumQuery.data
|
||||||
|
|
||||||
|
if (albumQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю альбом" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void albumQuery.refetch()} title="Не получилось загрузить альбом" />
|
||||||
|
}
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return <div className="text-slate-400">Загрузка альбома...</div>
|
return <ErrorPanel title="Альбом не найден" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDuration = album.tracks.reduce((sum, track) => sum + track.durationSeconds, 0)
|
const totalDuration = album.tracks.reduce((sum, track) => sum + track.durationSeconds, 0)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { coverArtUrl, fetchAlbums } from '@/lib/api'
|
import { coverArtUrl, fetchAlbums } from '@/lib/api'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
@@ -12,6 +13,14 @@ export function AlbumsPage() {
|
|||||||
|
|
||||||
const albums = albumsQuery.data?.items ?? []
|
const albums = albumsQuery.data?.items ?? []
|
||||||
|
|
||||||
|
if (albumsQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю альбомы" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumsQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void albumsQuery.refetch()} title="Не получилось загрузить альбомы" />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||||
@@ -18,8 +19,16 @@ export function ArtistDetailPage() {
|
|||||||
|
|
||||||
const artist = artistQuery.data
|
const artist = artistQuery.data
|
||||||
|
|
||||||
|
if (artistQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю исполнителя" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void artistQuery.refetch()} title="Не получилось загрузить исполнителя" />
|
||||||
|
}
|
||||||
|
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
return <div className="text-slate-400">Загрузка исполнителя...</div>
|
return <ErrorPanel title="Исполнитель не найден" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteArtistIds = new Set((favoritesQuery.data?.artists ?? []).map((item) => item.id))
|
const favoriteArtistIds = new Set((favoritesQuery.data?.artists ?? []).map((item) => item.id))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { Search, SlidersHorizontal } from 'lucide-react'
|
import { Search, SlidersHorizontal } from 'lucide-react'
|
||||||
import { coverArtUrl, fetchArtists } from '@/lib/api'
|
import { coverArtUrl, fetchArtists } from '@/lib/api'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
@@ -12,6 +13,14 @@ export function ArtistsPage() {
|
|||||||
|
|
||||||
const artists = artistsQuery.data?.items ?? []
|
const artists = artistsQuery.data?.items ?? []
|
||||||
|
|
||||||
|
if (artistsQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю исполнителей" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistsQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void artistsQuery.refetch()} title="Не получилось загрузить исполнителей" />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||||
import { coverArtUrl, fetchFavorites } from '@/lib/api'
|
import { coverArtUrl, fetchFavorites } from '@/lib/api'
|
||||||
@@ -17,6 +18,14 @@ export function FavoritesPage() {
|
|||||||
const albums = favorites?.albums ?? []
|
const albums = favorites?.albums ?? []
|
||||||
const artists = favorites?.artists ?? []
|
const artists = favorites?.artists ?? []
|
||||||
|
|
||||||
|
if (favoritesQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю избранное" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favoritesQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void favoritesQuery.refetch()} title="Не получилось загрузить избранное" />
|
||||||
|
}
|
||||||
|
|
||||||
if (tracks.length === 0 && albums.length === 0 && artists.length === 0) {
|
if (tracks.length === 0 && albums.length === 0 && artists.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-[520px] place-items-center rounded-[16px] border border-dashed border-[#24314f] bg-[#121b2e]">
|
<div className="grid min-h-[520px] place-items-center rounded-[16px] border border-dashed border-[#24314f] bg-[#121b2e]">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { type Track, coverArtUrl, fetchHome, fetchRecentlyPlayed, fetchTracks } from '@/lib/api'
|
import { type Track, coverArtUrl, fetchHome, fetchRecentlyPlayed, fetchTracks } from '@/lib/api'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { usePlayerStore } from '@/stores/player-store'
|
import { usePlayerStore } from '@/stores/player-store'
|
||||||
@@ -24,6 +25,23 @@ export function HomePage() {
|
|||||||
const recentTracks = recentTracksQuery.data?.items ?? homeQuery.data?.recentTracks ?? []
|
const recentTracks = recentTracksQuery.data?.items ?? homeQuery.data?.recentTracks ?? []
|
||||||
const popularAlbums = [...recentAlbums].reverse()
|
const popularAlbums = [...recentAlbums].reverse()
|
||||||
|
|
||||||
|
if (homeQuery.isLoading || tracksQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю домашнюю страницу" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (homeQuery.isError || tracksQuery.isError) {
|
||||||
|
return (
|
||||||
|
<ErrorPanel
|
||||||
|
onRetry={() => {
|
||||||
|
void homeQuery.refetch()
|
||||||
|
void tracksQuery.refetch()
|
||||||
|
void recentTracksQuery.refetch()
|
||||||
|
}}
|
||||||
|
title="Не получилось собрать главную"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
<section className="relative overflow-hidden rounded-[14px] bg-[#111b2e]">
|
<section className="relative overflow-hidden rounded-[14px] bg-[#111b2e]">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||||
import { coverArtUrl, fetchFavorites, fetchTracks } from '@/lib/api'
|
import { coverArtUrl, fetchFavorites, fetchTracks } from '@/lib/api'
|
||||||
@@ -21,6 +22,14 @@ export function TracksPage() {
|
|||||||
const tracks = tracksQuery.data?.items ?? []
|
const tracks = tracksQuery.data?.items ?? []
|
||||||
const favoriteTrackIds = new Set((favoritesQuery.data?.tracks ?? []).map((track) => track.id))
|
const favoriteTrackIds = new Set((favoritesQuery.data?.tracks ?? []).map((track) => track.id))
|
||||||
|
|
||||||
|
if (tracksQuery.isLoading) {
|
||||||
|
return <LoadingPanel title="Загружаю треки" />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracksQuery.isError) {
|
||||||
|
return <ErrorPanel onRetry={() => void tracksQuery.refetch()} title="Не получилось загрузить треки" />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<HeaderSearch onClick={() => navigate('/search')} />
|
<HeaderSearch onClick={() => navigate('/search')} />
|
||||||
|
|||||||
Reference in New Issue
Block a user