fix: improve query loading and error states
This commit is contained in:
@@ -630,7 +630,7 @@ Responsibilities:
|
||||
- [ ] Responsive navigation
|
||||
- [ ] Toast/notification system
|
||||
- [ ] Error boundary
|
||||
- [ ] Query loading/error patterns
|
||||
- [x] Query loading/error patterns
|
||||
|
||||
## 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 { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||
import { useParams } from 'react-router-dom'
|
||||
@@ -20,8 +21,16 @@ export function AlbumDetailPage() {
|
||||
|
||||
const album = albumQuery.data
|
||||
|
||||
if (albumQuery.isLoading) {
|
||||
return <LoadingPanel title="Загружаю альбом" />
|
||||
}
|
||||
|
||||
if (albumQuery.isError) {
|
||||
return <ErrorPanel onRetry={() => void albumQuery.refetch()} title="Не получилось загрузить альбом" />
|
||||
}
|
||||
|
||||
if (!album) {
|
||||
return <div className="text-slate-400">Загрузка альбома...</div>
|
||||
return <ErrorPanel title="Альбом не найден" />
|
||||
}
|
||||
|
||||
const totalDuration = album.tracks.reduce((sum, track) => sum + track.durationSeconds, 0)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { Search } from 'lucide-react'
|
||||
import { coverArtUrl, fetchAlbums } from '@/lib/api'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
@@ -12,6 +13,14 @@ export function AlbumsPage() {
|
||||
|
||||
const albums = albumsQuery.data?.items ?? []
|
||||
|
||||
if (albumsQuery.isLoading) {
|
||||
return <LoadingPanel title="Загружаю альбомы" />
|
||||
}
|
||||
|
||||
if (albumsQuery.isError) {
|
||||
return <ErrorPanel onRetry={() => void albumsQuery.refetch()} title="Не получилось загрузить альбомы" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { MoreVertical, Play, Shuffle } from 'lucide-react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||
@@ -18,8 +19,16 @@ export function ArtistDetailPage() {
|
||||
|
||||
const artist = artistQuery.data
|
||||
|
||||
if (artistQuery.isLoading) {
|
||||
return <LoadingPanel title="Загружаю исполнителя" />
|
||||
}
|
||||
|
||||
if (artistQuery.isError) {
|
||||
return <ErrorPanel onRetry={() => void artistQuery.refetch()} title="Не получилось загрузить исполнителя" />
|
||||
}
|
||||
|
||||
if (!artist) {
|
||||
return <div className="text-slate-400">Загрузка исполнителя...</div>
|
||||
return <ErrorPanel title="Исполнитель не найден" />
|
||||
}
|
||||
|
||||
const favoriteArtistIds = new Set((favoritesQuery.data?.artists ?? []).map((item) => item.id))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { Search, SlidersHorizontal } from 'lucide-react'
|
||||
import { coverArtUrl, fetchArtists } from '@/lib/api'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
@@ -12,6 +13,14 @@ export function ArtistsPage() {
|
||||
|
||||
const artists = artistsQuery.data?.items ?? []
|
||||
|
||||
if (artistsQuery.isLoading) {
|
||||
return <LoadingPanel title="Загружаю исполнителей" />
|
||||
}
|
||||
|
||||
if (artistsQuery.isError) {
|
||||
return <ErrorPanel onRetry={() => void artistsQuery.refetch()} title="Не получилось загрузить исполнителей" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||
import { coverArtUrl, fetchFavorites } from '@/lib/api'
|
||||
@@ -17,6 +18,14 @@ export function FavoritesPage() {
|
||||
const albums = favorites?.albums ?? []
|
||||
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) {
|
||||
return (
|
||||
<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 { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { type Track, coverArtUrl, fetchHome, fetchRecentlyPlayed, fetchTracks } from '@/lib/api'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { usePlayerStore } from '@/stores/player-store'
|
||||
@@ -24,6 +25,23 @@ export function HomePage() {
|
||||
const recentTracks = recentTracksQuery.data?.items ?? homeQuery.data?.recentTracks ?? []
|
||||
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 (
|
||||
<div className="space-y-10">
|
||||
<section className="relative overflow-hidden rounded-[14px] bg-[#111b2e]">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { ErrorPanel, LoadingPanel } from '@/components/query-state'
|
||||
import { Search } from 'lucide-react'
|
||||
import { FavoriteToggle } from '@/components/favorite-toggle'
|
||||
import { coverArtUrl, fetchFavorites, fetchTracks } from '@/lib/api'
|
||||
@@ -21,6 +22,14 @@ export function TracksPage() {
|
||||
const tracks = tracksQuery.data?.items ?? []
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<HeaderSearch onClick={() => navigate('/search')} />
|
||||
|
||||
Reference in New Issue
Block a user