mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-18 06:07:48 +00:00
feat: updates
This commit is contained in:
parent
a1b0bcf4d3
commit
884f3df41a
198 changed files with 4572 additions and 5336 deletions
24
packages/web/pages/Artist/Artist.tsx
Normal file
24
packages/web/pages/Artist/Artist.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import Header from './Header'
|
||||
import Popular from './Popular'
|
||||
import ArtistAlbum from './ArtistAlbums'
|
||||
import FansAlsoLike from './FansAlsoLike'
|
||||
import ArtistMVs from './ArtistMVs'
|
||||
|
||||
const Artist = () => {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
{/* Dividing line */}
|
||||
<div className='mt-10 mb-7.5 h-px w-full bg-white/20'></div>
|
||||
<Popular />
|
||||
<ArtistAlbum />
|
||||
<ArtistMVs />
|
||||
<FansAlsoLike />
|
||||
|
||||
{/* Page padding */}
|
||||
<div className='h-16'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Artist
|
||||
53
packages/web/pages/Artist/ArtistAlbums.tsx
Normal file
53
packages/web/pages/Artist/ArtistAlbums.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||
import CoverRow from '@/web/components/CoverRow'
|
||||
import React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const ArtistAlbum = () => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
|
||||
const { data: albumsRaw, isLoading: isLoadingAlbums } = useArtistAlbums({
|
||||
id: Number(params.id) || 0,
|
||||
limit: 1000,
|
||||
})
|
||||
|
||||
const pages = useMemo(() => {
|
||||
const pages: Album[][] = []
|
||||
albumsRaw?.hotAlbums.forEach((album, index) => {
|
||||
const pageNo = Math.floor(index / 12)
|
||||
if (!pages[pageNo]) {
|
||||
pages[pageNo] = [album]
|
||||
} else {
|
||||
pages[pageNo].push(album)
|
||||
}
|
||||
})
|
||||
return pages
|
||||
}, [albumsRaw?.hotAlbums])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-4 mt-11 text-12 font-medium uppercase text-neutral-300'>
|
||||
{t`common.album_other`}
|
||||
</div>
|
||||
|
||||
<div className='no-scrollbar flex gap-6 overflow-y-hidden overflow-x-scroll'>
|
||||
{pages.map((page, index) => (
|
||||
<CoverRow
|
||||
key={index}
|
||||
albums={page}
|
||||
itemTitle='name'
|
||||
itemSubtitle='year'
|
||||
className='h-full w-full flex-shrink-0'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const memoized = React.memo(ArtistAlbum)
|
||||
memoized.displayName = 'ArtistAlbum'
|
||||
export default memoized
|
||||
34
packages/web/pages/Artist/ArtistMVs.tsx
Normal file
34
packages/web/pages/Artist/ArtistMVs.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import useArtistMV from '@/web/api/hooks/useArtistMV'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ArtistMVs = () => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { data: videos } = useArtistMV({ id: Number(params.id) || 0 })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
{t`common.video_other`}
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 gap-6'>
|
||||
{videos?.mvs?.slice(0, 6)?.map(video => (
|
||||
<div key={video.id} onClick={() => navigate(`/mv/${video.id}`)}>
|
||||
<img
|
||||
src={video.imgurl16v9}
|
||||
className='aspect-video w-full rounded-24 object-contain'
|
||||
/>
|
||||
<div className='mt-2 text-12 font-medium text-neutral-600'>
|
||||
{video.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArtistMVs
|
||||
26
packages/web/pages/Artist/FansAlsoLike.tsx
Normal file
26
packages/web/pages/Artist/FansAlsoLike.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import ArtistRow from '@/web/components/ArtistRow'
|
||||
import useSimilarArtists from '@/web/api/hooks/useSimilarArtists'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const FansAlsoLike = () => {
|
||||
const params = useParams()
|
||||
const { data: artists, isLoading } = useSimilarArtists({
|
||||
id: Number(params.id) || 0,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isLoading || artists?.artists) && (
|
||||
<div>
|
||||
<div className='mb-6 mt-10 text-12 font-medium uppercase text-neutral-300'>
|
||||
Fans Also Like
|
||||
</div>
|
||||
|
||||
<ArtistRow artists={artists?.artists?.slice(0, 5)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FansAlsoLike
|
||||
74
packages/web/pages/Artist/Header/Actions.tsx
Normal file
74
packages/web/pages/Artist/Header/Actions.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import useUserArtists, {
|
||||
useMutationLikeAArtist,
|
||||
} from '@/web/api/hooks/useUserArtists'
|
||||
import Icon from '@/web/components/Icon'
|
||||
import { openContextMenu } from '@/web/states/contextMenus'
|
||||
import player from '@/web/states/player'
|
||||
import { cx } from '@emotion/css'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const Actions = ({ isLoading }: { isLoading: boolean }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: likedArtists } = useUserArtists()
|
||||
const params = useParams()
|
||||
const id = Number(params.id) || 0
|
||||
const isLiked = !!likedArtists?.data?.find(artist => artist.id === id)
|
||||
const likeArtist = useMutationLikeAArtist()
|
||||
|
||||
return (
|
||||
<div className='mt-11 flex items-end justify-between lg:z-10 lg:mt-6'>
|
||||
<div className='flex items-end'>
|
||||
{/* Menu */}
|
||||
<button
|
||||
onClick={event => {
|
||||
openContextMenu({
|
||||
event,
|
||||
type: 'artist',
|
||||
dataSourceID: id,
|
||||
})
|
||||
}}
|
||||
className={cx(
|
||||
'mr-2.5 flex h-14 w-14 items-center justify-center rounded-full transition duration-400 dark:bg-white/10 ',
|
||||
isLoading
|
||||
? 'text-transparent'
|
||||
: 'text-white/40 hover:text-white/70 hover:dark:bg-white/30 '
|
||||
)}
|
||||
>
|
||||
<Icon name='more' className='pointer-events-none h-7 w-7' />
|
||||
</button>
|
||||
|
||||
{/* Like */}
|
||||
<button
|
||||
onClick={() => likeArtist.mutateAsync(id)}
|
||||
className={cx(
|
||||
'mr-2.5 flex h-14 w-14 items-center justify-center rounded-full transition duration-400 dark:bg-white/10 ',
|
||||
isLoading
|
||||
? 'text-transparent'
|
||||
: 'text-white/40 hover:text-white/70 hover:dark:bg-white/30 '
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={isLiked ? 'heart' : 'heart-outline'}
|
||||
className='h-7 w-7'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Listen */}
|
||||
<button
|
||||
onClick={() => player.playArtistPopularTracks(id)}
|
||||
className={cx(
|
||||
'h-14 rounded-full px-10 text-18 font-medium',
|
||||
isLoading ? 'bg-white/10 text-transparent' : 'bg-brand-700 text-white'
|
||||
)}
|
||||
>
|
||||
{t`artist.listen`}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Actions
|
||||
90
packages/web/pages/Artist/Header/ArtistInfo.tsx
Normal file
90
packages/web/pages/Artist/Header/ArtistInfo.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import useIsMobile from '@/web/hooks/useIsMobile'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ArtistInfo = ({
|
||||
artist,
|
||||
isLoading,
|
||||
}: {
|
||||
artist?: Artist
|
||||
isLoading: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Name */}
|
||||
{isLoading ? (
|
||||
<div className=' text-28 font-semibold text-transparent lg:text-32'>
|
||||
<span className='rounded-full bg-white/10'>PLACEHOLDER</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-28 font-semibold text-white/70 lg:text-32'>
|
||||
{artist?.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type */}
|
||||
{isLoading ? (
|
||||
<div className='mt-2.5 text-24 font-medium text-transparent lg:mt-6'>
|
||||
<span className='rounded-full bg-white/10'>Artist</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-2.5 text-24 font-medium text-white/40 lg:mt-6'>
|
||||
Artist
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Counts */}
|
||||
{isLoading ? (
|
||||
<div className='mt-1 text-12 font-medium text-transparent'>
|
||||
<span className='rounded-full bg-white/10'>PLACEHOLDER12345</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-1 text-12 font-medium text-white/40'>
|
||||
{t('common.track-with-count', { count: artist?.musicSize })} ·{' '}
|
||||
{t('common.album-with-count', { count: artist?.albumSize })} ·{' '}
|
||||
{t('common.video-with-count', { count: artist?.mvSize })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{!isMobile &&
|
||||
(isLoading || isLoadingArtistFromApple ? (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-5 mt-6 text-14 font-bold text-transparent',
|
||||
css`
|
||||
height: 86px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<span className='rounded-full bg-white/10'>
|
||||
PLACEHOLDER1234567890
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-5 mt-6 text-14 font-bold text-white/40',
|
||||
css`
|
||||
height: 86px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{artistFromApple?.attributes?.artistBio || artist?.briefDesc}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArtistInfo
|
||||
59
packages/web/pages/Artist/Header/Cover.tsx
Normal file
59
packages/web/pages/Artist/Header/Cover.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { isIOS, isSafari, resizeImage } from '@/web/utils/common'
|
||||
import Image from '@/web/components/Image'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import useAppleMusicArtist from '@/web/hooks/useAppleMusicArtist'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Hls from 'hls.js'
|
||||
import { motion } from 'framer-motion'
|
||||
import uiStates from '@/web/states/uiStates'
|
||||
import VideoCover from '@/web/components/VideoCover'
|
||||
|
||||
const Cover = ({ artist }: { artist?: Artist }) => {
|
||||
const { data: artistFromApple, isLoading: isLoadingArtistFromApple } =
|
||||
useAppleMusicArtist({
|
||||
id: artist?.id,
|
||||
name: artist?.name,
|
||||
})
|
||||
|
||||
const video =
|
||||
artistFromApple?.attributes?.editorialVideo?.motionArtistSquare1x1?.video
|
||||
const cover = isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url || artist?.img1v1Url
|
||||
|
||||
useEffect(() => {
|
||||
if (cover) uiStates.blurBackgroundImage = cover
|
||||
}, [cover])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
'relative overflow-hidden lg:rounded-24',
|
||||
css`
|
||||
grid-area: cover;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
className={cx(
|
||||
'aspect-square h-full w-full lg:z-10',
|
||||
video ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
src={resizeImage(
|
||||
isLoadingArtistFromApple
|
||||
? ''
|
||||
: artistFromApple?.attributes?.artwork?.url ||
|
||||
artist?.img1v1Url ||
|
||||
'',
|
||||
'lg'
|
||||
)}
|
||||
/>
|
||||
|
||||
{video && <VideoCover source={video} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cover
|
||||
60
packages/web/pages/Artist/Header/Header.tsx
Normal file
60
packages/web/pages/Artist/Header/Header.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { cx, css } from '@emotion/css'
|
||||
import { breakpoint as bp } from '@/web/utils/const'
|
||||
import ArtistInfo from './ArtistInfo'
|
||||
import Actions from './Actions'
|
||||
import LatestRelease from './LatestRelease'
|
||||
import Cover from './Cover'
|
||||
import useArtist from '@/web/api/hooks/useArtist'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
const Header = () => {
|
||||
const params = useParams()
|
||||
const { data: artistRaw, isLoading } = useArtist({
|
||||
id: Number(params.id) || 0,
|
||||
})
|
||||
const artist = artistRaw?.artist
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'lg:grid lg:gap-10',
|
||||
css`
|
||||
grid-template-columns: auto 558px;
|
||||
grid-template-areas:
|
||||
'info cover'
|
||||
'info cover';
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Cover artist={artist} />
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'lg:flex lg:flex-col lg:justify-between',
|
||||
css`
|
||||
grid-area: info;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
'mx-2.5 rounded-48 bg-white/10 p-8 backdrop-blur-3xl lg:mx-0 lg:bg-transparent lg:p-0 lg:backdrop-blur-none',
|
||||
css`
|
||||
margin-top: -60px;
|
||||
${bp.lg} {
|
||||
margin-top: 0px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
<ArtistInfo isLoading={isLoading} artist={artist} />
|
||||
<Actions isLoading={isLoading} />
|
||||
</div>
|
||||
|
||||
<LatestRelease />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
125
packages/web/pages/Artist/Header/LatestRelease.tsx
Normal file
125
packages/web/pages/Artist/Header/LatestRelease.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import dayjs from 'dayjs'
|
||||
import { cx, css } from '@emotion/css'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import Image from '@/web/components/Image'
|
||||
import useArtistAlbums from '@/web/api/hooks/useArtistAlbums'
|
||||
import { useMemo } from 'react'
|
||||
import useArtistMV from '@/web/api/hooks/useArtistMV'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Album = ({ album }: { album?: Album }) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
if (!album) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => navigate(`/album/${album.id}`)}
|
||||
className='group flex rounded-24 bg-white/10 p-2.5 transition-colors duration-400 hover:bg-white/20'
|
||||
>
|
||||
<Image
|
||||
src={resizeImage(album.picUrl, 'sm')}
|
||||
className={cx(
|
||||
'aspect-square shrink-0',
|
||||
css`
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
<div className='flex-shrink-1 ml-2'>
|
||||
<div className='line-clamp-1 text-16 font-medium text-night-100 transition-colors duration-400 group-hover:text-night-50'>
|
||||
{album.name}
|
||||
</div>
|
||||
<div className='mt-1 text-14 font-bold text-night-500 transition-colors duration-400 group-hover:text-night-200'>
|
||||
{album.type}
|
||||
{album.size > 1 ? `· ${album.size} Tracks` : ''}
|
||||
</div>
|
||||
<div className='mt-1.5 text-12 font-medium text-night-500 transition-colors duration-400 group-hover:text-night-200'>
|
||||
{dayjs(album?.publishTime || 0).format('MMM DD, YYYY')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Video = ({ video }: { video?: any }) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<>
|
||||
{video && (
|
||||
<div
|
||||
className='group mt-4 flex rounded-24 bg-white/10 p-2.5 transition-colors duration-400 hover:bg-white/20'
|
||||
onClick={() => navigate(`/mv/${video.id}`)}
|
||||
>
|
||||
<img
|
||||
src={video.imgurl16v9}
|
||||
className={cx(
|
||||
'object-contain',
|
||||
css`
|
||||
height: 60px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
<div className='flex-shrink-1 ml-2'>
|
||||
<div className='line-clamp-1 text-16 font-medium text-night-100 transition-colors duration-400 group-hover:text-night-50'>
|
||||
{video.name}
|
||||
</div>
|
||||
<div className='mt-1 text-14 font-bold text-night-500 transition-colors duration-400 group-hover:text-night-200'>
|
||||
MV
|
||||
</div>
|
||||
<div className='mt-1.5 text-12 font-medium text-night-500 transition-colors duration-400 group-hover:text-night-200'>
|
||||
{dayjs(video.publishTime).format('MMM DD, YYYY')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const LatestRelease = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const params = useParams()
|
||||
|
||||
const { data: albumsRaw, isLoading: isLoadingAlbums } = useArtistAlbums({
|
||||
id: Number(params.id) || 0,
|
||||
limit: 1000,
|
||||
})
|
||||
|
||||
const album = useMemo(() => albumsRaw?.hotAlbums?.[0], [albumsRaw?.hotAlbums])
|
||||
|
||||
const { data: videos, isLoading: isLoadingVideos } = useArtistMV({
|
||||
id: Number(params.id) || 0,
|
||||
})
|
||||
const video = videos?.mvs?.[0]
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isLoadingVideos && !isLoadingAlbums && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className='mx-2.5 lg:mx-0'
|
||||
>
|
||||
<div className='mb-3 mt-7 text-14 font-bold text-neutral-300'>
|
||||
{t`artist.latest-releases`}
|
||||
</div>
|
||||
|
||||
<Album album={album} />
|
||||
<Video video={video} />
|
||||
</motion.div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LatestRelease
|
||||
3
packages/web/pages/Artist/Header/index.tsx
Normal file
3
packages/web/pages/Artist/Header/index.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Header from './Header'
|
||||
|
||||
export default Header
|
||||
87
packages/web/pages/Artist/Popular.tsx
Normal file
87
packages/web/pages/Artist/Popular.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { resizeImage } from '@/web/utils/common'
|
||||
import player from '@/web/states/player'
|
||||
import { State as PlayerState } from '@/web/utils/player'
|
||||
import useTracks from '@/web/api/hooks/useTracks'
|
||||
import { css, cx } from '@emotion/css'
|
||||
import Image from '@/web/components/Image'
|
||||
import useArtist from '@/web/api/hooks/useArtist'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Track = ({
|
||||
track,
|
||||
isPlaying,
|
||||
onPlay,
|
||||
}: {
|
||||
track?: Track
|
||||
isPlaying?: boolean
|
||||
onPlay: (id: number) => void
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className='flex items-center justify-between'
|
||||
onClick={e => {
|
||||
if (e.detail === 2 && track?.id) onPlay(track.id)
|
||||
}}
|
||||
>
|
||||
{/* Cover */}
|
||||
<Image
|
||||
className='mr-4 aspect-square h-14 w-14 flex-shrink-0 rounded-12'
|
||||
src={resizeImage(track?.al?.picUrl || '', 'sm')}
|
||||
animation={false}
|
||||
placeholder={false}
|
||||
/>
|
||||
|
||||
{/* Track info */}
|
||||
<div className='mr-3 flex-grow'>
|
||||
<div
|
||||
className={cx(
|
||||
'line-clamp-1 text-16 font-medium ',
|
||||
isPlaying
|
||||
? 'text-brand-700'
|
||||
: 'text-neutral-700 dark:text-neutral-200'
|
||||
)}
|
||||
>
|
||||
{track?.name}
|
||||
</div>
|
||||
<div className='line-clamp-1 mt-1 text-14 font-bold text-neutral-200 dark:text-neutral-700'>
|
||||
{track?.ar.map(a => a.name).join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Popular = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const params = useParams()
|
||||
const { data: artist, isLoading: isLoadingArtist } = useArtist({
|
||||
id: Number(params.id) || 0,
|
||||
})
|
||||
|
||||
const tracks = artist?.hotSongs || []
|
||||
const onPlay = (id: number) => {
|
||||
if (!tracks) return
|
||||
player.playAList(
|
||||
tracks.map(t => t.id),
|
||||
id
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-4 text-12 font-medium uppercase text-neutral-300'>
|
||||
{t`artist.popular`}
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-3 grid-rows-3 gap-4 overflow-hidden'>
|
||||
{tracks?.slice(0, 9)?.map(t => (
|
||||
<Track key={t.id} track={t} onPlay={onPlay} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Popular
|
||||
3
packages/web/pages/Artist/index.tsx
Normal file
3
packages/web/pages/Artist/index.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Artist from './Artist'
|
||||
|
||||
export default Artist
|
||||
Loading…
Add table
Add a link
Reference in a new issue