Add frontend pages for music and books
Some checks failed
Rebuild Issue Index / build-index (push) Has been cancelled
Some checks failed
Rebuild Issue Index / build-index (push) Has been cancelled
- Create MusicDetails component with artist info, albums, stats, request button - Create BookDetails component with cover, overview, request button - Create music/[artistId] and book/[bookId] pages - Update Search component with tabbed interface (Movies & TV, Music, Books) - Music/Book search results with card grid and cover art - Link to detail pages from search results
This commit is contained in:
111
src/components/BookDetails/index.tsx
Normal file
111
src/components/BookDetails/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import Button from '@app/components/Common/Button';
|
||||||
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
|
import { useUser } from '@app/hooks/useUser';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface BookDetailsProps {
|
||||||
|
book?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BookDetails = ({ book }: BookDetailsProps) => {
|
||||||
|
const { user } = useUser();
|
||||||
|
const [requesting, setRequesting] = useState(false);
|
||||||
|
const [requested, setRequested] = useState(false);
|
||||||
|
|
||||||
|
if (!book) {
|
||||||
|
return (
|
||||||
|
<div className="mt-16 text-center text-gray-400">
|
||||||
|
<h2 className="text-2xl">Book not found</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coverUrl =
|
||||||
|
book.images?.find((i: any) => i.coverType === 'cover')?.remoteUrl ||
|
||||||
|
book.images?.[0]?.remoteUrl;
|
||||||
|
|
||||||
|
const handleRequest = async () => {
|
||||||
|
setRequesting(true);
|
||||||
|
try {
|
||||||
|
await axios.post('/api/v1/request', {
|
||||||
|
mediaType: 'book',
|
||||||
|
mediaId: 0,
|
||||||
|
foreignId: book.foreignBookId,
|
||||||
|
foreignTitle: book.title,
|
||||||
|
});
|
||||||
|
setRequested(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Request failed', e);
|
||||||
|
}
|
||||||
|
setRequesting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media-page">
|
||||||
|
<PageTitle title={book.title} />
|
||||||
|
|
||||||
|
<div className="media-header">
|
||||||
|
<div className="media-poster">
|
||||||
|
{coverUrl && (
|
||||||
|
<img
|
||||||
|
src={coverUrl}
|
||||||
|
alt={book.title}
|
||||||
|
className="rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="media-title">
|
||||||
|
<h1 className="text-4xl font-bold text-white">{book.title}</h1>
|
||||||
|
{book.author && (
|
||||||
|
<p className="mt-1 text-lg text-gray-400">
|
||||||
|
by {book.author.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{book.genres?.slice(0, 5).map((genre: string) => (
|
||||||
|
<span
|
||||||
|
key={genre}
|
||||||
|
className="rounded-full bg-gray-700 px-3 py-1 text-sm text-gray-300"
|
||||||
|
>
|
||||||
|
{genre}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{book.pageCount > 0 && (
|
||||||
|
<p className="mt-2 text-sm text-gray-400">
|
||||||
|
{book.pageCount} pages ·{' '}
|
||||||
|
{book.releaseDate?.substring(0, 4)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="media-actions mt-4">
|
||||||
|
{requested ? (
|
||||||
|
<Button disabled buttonType="success">
|
||||||
|
<span>✓ Requested</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
onClick={handleRequest}
|
||||||
|
disabled={requesting}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{requesting ? 'Requesting...' : '📚 Request Book'}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{book.overview && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-xl font-bold text-white">Overview</h2>
|
||||||
|
<p className="mt-2 text-gray-300">{book.overview}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookDetails;
|
||||||
183
src/components/MusicDetails/index.tsx
Normal file
183
src/components/MusicDetails/index.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import Button from '@app/components/Common/Button';
|
||||||
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
|
import { useUser } from '@app/hooks/useUser';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface MusicDetailsProps {
|
||||||
|
artist?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicDetails = ({ artist }: MusicDetailsProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { user } = useUser();
|
||||||
|
const [requesting, setRequesting] = useState(false);
|
||||||
|
const [requested, setRequested] = useState(artist?.inLibrary || false);
|
||||||
|
|
||||||
|
if (!artist) {
|
||||||
|
return (
|
||||||
|
<div className="mt-16 text-center text-gray-400">
|
||||||
|
<h2 className="text-2xl">Artist not found</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const posterUrl =
|
||||||
|
artist.images?.find((i: any) => i.coverType === 'poster')?.remoteUrl ||
|
||||||
|
artist.images?.[0]?.remoteUrl;
|
||||||
|
|
||||||
|
const handleRequest = async () => {
|
||||||
|
setRequesting(true);
|
||||||
|
try {
|
||||||
|
await axios.post('/api/v1/request', {
|
||||||
|
mediaType: 'music',
|
||||||
|
mediaId: 0,
|
||||||
|
foreignId: artist.foreignArtistId,
|
||||||
|
foreignTitle: artist.artistName,
|
||||||
|
});
|
||||||
|
setRequested(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Request failed', e);
|
||||||
|
}
|
||||||
|
setRequesting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="media-page">
|
||||||
|
<PageTitle title={artist.artistName} />
|
||||||
|
<div className="media-page-bg-image">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center"
|
||||||
|
style={{
|
||||||
|
backgroundImage: posterUrl ? `url(${posterUrl})` : undefined,
|
||||||
|
filter: 'blur(16px) brightness(0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-900" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="media-header">
|
||||||
|
<div className="media-poster">
|
||||||
|
{posterUrl && (
|
||||||
|
<img
|
||||||
|
src={posterUrl}
|
||||||
|
alt={artist.artistName}
|
||||||
|
className="rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="media-title">
|
||||||
|
<h1 className="text-4xl font-bold text-white">
|
||||||
|
{artist.artistName}
|
||||||
|
</h1>
|
||||||
|
{artist.disambiguation && (
|
||||||
|
<p className="mt-1 text-lg text-gray-400">
|
||||||
|
{artist.disambiguation}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{artist.genres?.slice(0, 5).map((genre: string) => (
|
||||||
|
<span
|
||||||
|
key={genre}
|
||||||
|
className="rounded-full bg-gray-700 px-3 py-1 text-sm text-gray-300"
|
||||||
|
>
|
||||||
|
{genre}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="media-actions mt-4">
|
||||||
|
{requested ? (
|
||||||
|
<Button disabled buttonType="success">
|
||||||
|
<span>
|
||||||
|
{artist.inLibrary ? '✓ In Library' : '✓ Requested'}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
onClick={handleRequest}
|
||||||
|
disabled={requesting}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{requesting ? 'Requesting...' : '🎵 Request Artist'}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Albums */}
|
||||||
|
{artist.albums && artist.albums.length > 0 && (
|
||||||
|
<div className="slider-header mt-8">
|
||||||
|
<h2 className="text-xl font-bold text-white">
|
||||||
|
Albums ({artist.albums.length})
|
||||||
|
</h2>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
|
||||||
|
{artist.albums.map((album: any) => {
|
||||||
|
const albumArt =
|
||||||
|
album.images?.find((i: any) => i.coverType === 'cover')
|
||||||
|
?.remoteUrl || album.images?.[0]?.remoteUrl;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={album.id}
|
||||||
|
className="group relative overflow-hidden rounded-lg bg-gray-800 shadow-lg"
|
||||||
|
>
|
||||||
|
{albumArt && (
|
||||||
|
<img
|
||||||
|
src={albumArt}
|
||||||
|
alt={album.title}
|
||||||
|
className="aspect-square w-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="p-2">
|
||||||
|
<h3 className="truncate text-sm font-semibold text-white">
|
||||||
|
{album.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
{album.releaseDate?.substring(0, 4)} · {album.albumType}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
{artist.statistics && (
|
||||||
|
<div className="mt-8 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||||
|
<div className="rounded-lg bg-gray-800 p-4">
|
||||||
|
<p className="text-sm text-gray-400">Albums</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{artist.statistics.albumCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-gray-800 p-4">
|
||||||
|
<p className="text-sm text-gray-400">Tracks</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{artist.statistics.totalTrackCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-gray-800 p-4">
|
||||||
|
<p className="text-sm text-gray-400">On Disk</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{artist.statistics.trackFileCount}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-gray-800 p-4">
|
||||||
|
<p className="text-sm text-gray-400">Size</p>
|
||||||
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{(artist.statistics.sizeOnDisk / 1073741824).toFixed(1)} GB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicDetails;
|
||||||
@@ -9,7 +9,10 @@ import type {
|
|||||||
PersonResult,
|
PersonResult,
|
||||||
TvResult,
|
TvResult,
|
||||||
} from '@server/models/Search';
|
} from '@server/models/Search';
|
||||||
|
import axios from 'axios';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages('components.Search', {
|
const messages = defineMessages('components.Search', {
|
||||||
@@ -17,9 +20,86 @@ const messages = defineMessages('components.Search', {
|
|||||||
searchresults: 'Search Results',
|
searchresults: 'Search Results',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type SearchTab = 'all' | 'music' | 'books';
|
||||||
|
|
||||||
|
const MusicResultCard = ({ artist }: { artist: any }) => {
|
||||||
|
const posterUrl =
|
||||||
|
artist.images?.find((i: any) => i.coverType === 'poster')?.remoteUrl ||
|
||||||
|
artist.images?.[0]?.remoteUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/music/${artist.foreignArtistId}`}>
|
||||||
|
<div className="group relative w-36 cursor-pointer overflow-hidden rounded-lg bg-gray-800 shadow-lg transition duration-200 hover:scale-105 sm:w-36 md:w-44">
|
||||||
|
<div className="aspect-[2/3] w-full">
|
||||||
|
{posterUrl ? (
|
||||||
|
<img
|
||||||
|
src={posterUrl}
|
||||||
|
alt={artist.name}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center bg-gray-700 text-4xl">
|
||||||
|
🎵
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/80 via-transparent p-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-white">{artist.name}</h3>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
{artist.artistType || 'Artist'}
|
||||||
|
{artist.inLibrary && ' · ✓ In Library'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BookResultCard = ({ book }: { book: any }) => {
|
||||||
|
const coverUrl =
|
||||||
|
book.images?.find((i: any) => i.coverType === 'cover')?.remoteUrl ||
|
||||||
|
book.images?.[0]?.remoteUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/book/${book.foreignBookId}`}>
|
||||||
|
<div className="group relative w-36 cursor-pointer overflow-hidden rounded-lg bg-gray-800 shadow-lg transition duration-200 hover:scale-105 sm:w-36 md:w-44">
|
||||||
|
<div className="aspect-[2/3] w-full">
|
||||||
|
{coverUrl ? (
|
||||||
|
<img
|
||||||
|
src={coverUrl}
|
||||||
|
alt={book.title}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center bg-gray-700 text-4xl">
|
||||||
|
📚
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/80 via-transparent p-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-white">{book.title}</h3>
|
||||||
|
<p className="text-xs text-gray-300">
|
||||||
|
{book.author?.name || ''}
|
||||||
|
{book.releaseDate && ` · ${book.releaseDate.substring(0, 4)}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [activeTab, setActiveTab] = useState<SearchTab>('all');
|
||||||
|
const [musicResults, setMusicResults] = useState<any[]>([]);
|
||||||
|
const [bookResults, setBookResults] = useState<any[]>([]);
|
||||||
|
const [musicLoading, setMusicLoading] = useState(false);
|
||||||
|
const [bookLoading, setBookLoading] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoadingInitialData,
|
isLoadingInitialData,
|
||||||
@@ -31,31 +111,125 @@ const Search = () => {
|
|||||||
error,
|
error,
|
||||||
} = useDiscover<MovieResult | TvResult | PersonResult>(
|
} = useDiscover<MovieResult | TvResult | PersonResult>(
|
||||||
`/api/v1/search`,
|
`/api/v1/search`,
|
||||||
{
|
{ query: router.query.query },
|
||||||
query: router.query.query,
|
|
||||||
},
|
|
||||||
{ hideAvailable: false, hideBlocklisted: false }
|
{ hideAvailable: false, hideBlocklisted: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Search music and books when query changes
|
||||||
|
useEffect(() => {
|
||||||
|
const query = router.query.query as string;
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
setMusicLoading(true);
|
||||||
|
axios
|
||||||
|
.get(`/api/v1/music/search?query=${encodeURIComponent(query)}`)
|
||||||
|
.then((res) => setMusicResults(res.data?.results || []))
|
||||||
|
.catch(() => setMusicResults([]))
|
||||||
|
.finally(() => setMusicLoading(false));
|
||||||
|
|
||||||
|
setBookLoading(true);
|
||||||
|
axios
|
||||||
|
.get(`/api/v1/book/search?query=${encodeURIComponent(query)}`)
|
||||||
|
.then((res) => setBookResults(res.data?.results || []))
|
||||||
|
.catch(() => setBookResults([]))
|
||||||
|
.finally(() => setBookLoading(false));
|
||||||
|
}, [router.query.query]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorPage statusCode={500} />;
|
return <ErrorPage statusCode={500} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabs: { id: SearchTab; label: string; count: number }[] = [
|
||||||
|
{ id: 'all', label: 'Movies & TV', count: titles?.length || 0 },
|
||||||
|
{ id: 'music', label: '🎵 Music', count: musicResults.length },
|
||||||
|
{ id: 'books', label: '📚 Books', count: bookResults.length },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title={intl.formatMessage(messages.search)} />
|
<PageTitle title={intl.formatMessage(messages.search)} />
|
||||||
<div className="mb-5 mt-1">
|
<div className="mb-5 mt-1">
|
||||||
<Header>{intl.formatMessage(messages.searchresults)}</Header>
|
<Header>{intl.formatMessage(messages.searchresults)}</Header>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mb-6 flex gap-2 border-b border-gray-700">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`px-4 py-2 text-sm font-medium transition ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'border-b-2 border-indigo-500 text-white'
|
||||||
|
: 'text-gray-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{tab.count > 0 && (
|
||||||
|
<span className="ml-1 text-xs text-gray-500">
|
||||||
|
({tab.count})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movies & TV Tab */}
|
||||||
|
{activeTab === 'all' && (
|
||||||
<ListView
|
<ListView
|
||||||
items={titles}
|
items={titles}
|
||||||
isEmpty={isEmpty}
|
isEmpty={isEmpty}
|
||||||
isLoading={
|
isLoading={
|
||||||
isLoadingInitialData || (isLoadingMore && (titles?.length ?? 0) > 0)
|
isLoadingInitialData ||
|
||||||
|
(isLoadingMore && (titles?.length ?? 0) > 0)
|
||||||
}
|
}
|
||||||
isReachingEnd={isReachingEnd}
|
isReachingEnd={isReachingEnd}
|
||||||
onScrollBottom={fetchMore}
|
onScrollBottom={fetchMore}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Music Tab */}
|
||||||
|
{activeTab === 'music' && (
|
||||||
|
<div>
|
||||||
|
{musicLoading ? (
|
||||||
|
<div className="text-center text-gray-400">Searching music...</div>
|
||||||
|
) : musicResults.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400">
|
||||||
|
No music results found.
|
||||||
|
{!router.query.query && ' Try searching for an artist or album.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7">
|
||||||
|
{musicResults.map((artist) => (
|
||||||
|
<MusicResultCard
|
||||||
|
key={artist.foreignArtistId}
|
||||||
|
artist={artist}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Books Tab */}
|
||||||
|
{activeTab === 'books' && (
|
||||||
|
<div>
|
||||||
|
{bookLoading ? (
|
||||||
|
<div className="text-center text-gray-400">Searching books...</div>
|
||||||
|
) : bookResults.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400">
|
||||||
|
No book results found.
|
||||||
|
{!router.query.query && ' Try searching for a title or author.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7">
|
||||||
|
{bookResults.map((book) => (
|
||||||
|
<BookResultCard key={book.foreignBookId} book={book} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
33
src/pages/book/[bookId]/index.tsx
Normal file
33
src/pages/book/[bookId]/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import BookDetails from '@app/components/BookDetails';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
|
interface BookPageProps {
|
||||||
|
book?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BookPage: NextPage<BookPageProps> = ({ book }) => {
|
||||||
|
return <BookDetails book={book} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<BookPageProps> = async (
|
||||||
|
ctx
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
|
process.env.PORT || 5055
|
||||||
|
}/api/v1/book/search?query=${ctx.query.bookId}`,
|
||||||
|
{
|
||||||
|
headers: ctx.req?.headers?.cookie
|
||||||
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { props: { book: response.data?.results?.[0] } };
|
||||||
|
} catch {
|
||||||
|
return { props: {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookPage;
|
||||||
33
src/pages/music/[artistId]/index.tsx
Normal file
33
src/pages/music/[artistId]/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import MusicDetails from '@app/components/MusicDetails';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
|
interface MusicPageProps {
|
||||||
|
artist?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicPage: NextPage<MusicPageProps> = ({ artist }) => {
|
||||||
|
return <MusicDetails artist={artist} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<MusicPageProps> = async (
|
||||||
|
ctx
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`http://${process.env.HOST || 'localhost'}:${
|
||||||
|
process.env.PORT || 5055
|
||||||
|
}/api/v1/music/artist/${ctx.query.artistId}`,
|
||||||
|
{
|
||||||
|
headers: ctx.req?.headers?.cookie
|
||||||
|
? { cookie: ctx.req.headers.cookie }
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { props: { artist: response.data } };
|
||||||
|
} catch {
|
||||||
|
return { props: {} };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicPage;
|
||||||
Reference in New Issue
Block a user