feat: updates

This commit is contained in:
qier222 2022-03-17 19:30:43 +08:00
parent d96bd2a547
commit e3486ab550
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
23 changed files with 331 additions and 261 deletions

View file

@ -1,23 +1,41 @@
import Realm from 'realm'
import type { FetchTracksResponse } from '../renderer/src/api/track'
import type { FetchAlbumResponse } from '../renderer/src/api/album'
enum ModelNames {
TRACK = 'Track',
ALBUM = 'Album',
ARTIST = 'Artist',
PLAYLIST = 'Playlist',
}
const universalProperties = {
id: 'int',
json: 'string',
updateAt: 'int',
}
const TrackSchema = {
name: ModelNames.TRACK,
properties: {
id: 'int',
json: 'string',
updateAt: 'int',
},
properties: universalProperties,
primaryKey: 'id',
}
const AlbumSchema = {
name: ModelNames.ALBUM,
properties: universalProperties,
primaryKey: 'id',
}
const PlaylistSchema = {
name: ModelNames.PLAYLIST,
properties: universalProperties,
primaryKey: 'id',
}
const realm = new Realm({
path: './dist/db.realm',
schema: [TrackSchema],
path: './.tmp/db.realm',
schema: [TrackSchema, AlbumSchema, PlaylistSchema],
})
export const database = {
@ -29,7 +47,7 @@ export const database = {
model,
{
id: key,
updateAt: ~~(Date.now() / 1000),
updateAt: Date.now(),
json: JSON.stringify(value),
},
'modified'
@ -40,29 +58,43 @@ export const database = {
},
}
export function setTracks(data: FetchTracksResponse) {
const tracks = data.songs
export async function setTracks(data: FetchTracksResponse) {
if (!data.songs) return
const write = async () =>
const tracks = data.songs
realm.write(() => {
tracks.forEach(track => {
database.set(ModelNames.TRACK, track.id, track)
})
})
write()
}
export async function setCache(api: string, data: any) {
switch (api) {
case 'song_detail':
case 'song_detail': {
setTracks(data)
return
}
case 'album': {
if (!data.album) return
realm.write(() => {
database.set(ModelNames.ALBUM, Number(data.album.id), data)
})
return
}
case 'playlist_detail': {
if (!data.playlist) return
realm.write(() => {
database.set(ModelNames.PLAYLIST, Number(data.playlist.id), data)
})
return
}
}
}
export function getCache(api: string, query: any) {
switch (api) {
case 'song_detail': {
const ids: string[] = query.ids.split(',')
const ids: string[] = query?.ids.split(',')
const idsQuery = ids.map(id => `id = ${id}`).join(' OR ')
const tracksRaw = realm
.objects(ModelNames.TRACK)
@ -78,5 +110,23 @@ export function getCache(api: string, query: any) {
privileges: {},
}
}
case 'album': {
if (!query?.id) return
const album = realm.objectForPrimaryKey(
ModelNames.ALBUM,
Number(query?.id)
)?.json
if (album) return JSON.parse(album)
return
}
case 'playlist_detail': {
if (!query?.id) return
const playlist = realm.objectForPrimaryKey(
ModelNames.PLAYLIST,
Number(query?.id)
)?.json
if (playlist) return JSON.parse(playlist)
return
}
}
}

View file

@ -9,13 +9,13 @@ import Main from './components/Main'
const App = () => {
return (
<QueryClientProvider client={reactQueryClient}>
<div id="layout" className="grid select-none grid-cols-[16rem_auto]">
<div id='layout' className='grid select-none grid-cols-[16rem_auto]'>
<Sidebar />
<Main />
<Player />
</div>
<Toaster position="bottom-center" containerStyle={{ bottom: '5rem' }} />
<Toaster position='bottom-center' containerStyle={{ bottom: '5rem' }} />
{/* Devtool */}
<ReactQueryDevtools

View file

@ -11,7 +11,7 @@ const ArtistInline = ({
<div className={classNames('flex truncate', className)}>
{artists.map((artist, index) => (
<span key={artist.id}>
<span className="hover:underline">{artist.name}</span>
<span className='hover:underline'>{artist.name}</span>
{index < artists.length - 1 ? ', ' : ''}&nbsp;
</span>
))}

View file

@ -12,7 +12,7 @@ const Cover = ({
const [isError, setIsError] = useState(false)
return (
<div onClick={onClick} className="group relative z-0">
<div onClick={onClick} className='group relative z-0'>
{/* Neon shadow */}
<div
className={classNames(
@ -27,8 +27,8 @@ const Cover = ({
{/* Cover */}
{isError ? (
<div className="box-content flex aspect-square h-full w-full items-center justify-center rounded-xl border border-black border-opacity-5 bg-gray-800 text-gray-300">
<SvgIcon name="music-note" className="h-1/2 w-1/2" />
<div className='box-content flex aspect-square h-full w-full items-center justify-center rounded-xl border border-black border-opacity-5 bg-gray-800 text-gray-300'>
<SvgIcon name='music-note' className='h-1/2 w-1/2' />
</div>
) : (
<img
@ -43,9 +43,9 @@ const Cover = ({
)}
{/* Play button */}
<div className="absolute top-0 hidden h-full w-full place-content-center group-hover:grid">
<button className="btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]">
<SvgIcon className="ml-1 h-4 w-4" name="play" />
<div className='absolute top-0 hidden h-full w-full place-content-center group-hover:grid'>
<button className='btn-pressed-animation grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'>
<SvgIcon className='ml-1 h-4 w-4' name='play' />
</button>
</div>
</div>

View file

@ -20,12 +20,12 @@ const Title = ({
seeMoreLink: string
}) => {
return (
<div className="flex items-baseline justify-between">
<div className="my-4 text-[28px] font-bold text-black dark:text-white">
<div className='flex items-baseline justify-between'>
<div className='my-4 text-[28px] font-bold text-black dark:text-white'>
{title}
</div>
{seeMoreLink && (
<div className="text-13px font-semibold text-gray-600 hover:underline">
<div className='text-13px font-semibold text-gray-600 hover:underline'>
See More
</div>
)}
@ -83,6 +83,8 @@ const CoverRow = ({
seeMoreLink,
isSkeleton,
className,
rows = 2,
navigateCallback, // Callback function when click on the cover/title
}: {
title?: string
albums?: Album[]
@ -92,13 +94,15 @@ const CoverRow = ({
seeMoreLink?: string
isSkeleton?: boolean
className?: string
rows?: number
navigateCallback?: () => void
}) => {
const renderItems = useMemo(() => {
if (isSkeleton) {
return new Array(10).fill({}) as Array<Album | Playlist | Artist>
return new Array(rows * 5).fill({}) as Array<Album | Playlist | Artist>
}
return albums ?? playlists ?? artists ?? []
}, [albums, artists, isSkeleton, playlists])
}, [albums, artists, isSkeleton, playlists, rows])
const navigate = useNavigate()
const goTo = (id: number) => {
@ -106,6 +110,7 @@ const CoverRow = ({
if (albums) navigate(`/album/${id}`)
if (playlists) navigate(`/playlist/${id}`)
if (artists) navigate(`/artist/${id}`)
if (navigateCallback) navigateCallback()
}
const prefetch = (id: number) => {
@ -129,12 +134,12 @@ const CoverRow = ({
<div
key={item.id ?? index}
onMouseOver={() => prefetch(item.id)}
className="grid gap-x-[24px] gap-y-7"
className='grid gap-x-[24px] gap-y-7'
>
<div>
{/* Cover */}
{isSkeleton ? (
<Skeleton className="box-content aspect-square w-full rounded-xl border border-black border-opacity-0" />
<Skeleton className='box-content aspect-square w-full rounded-xl border border-black border-opacity-0' />
) : (
<Cover
onClick={() => goTo(item.id)}
@ -143,30 +148,30 @@ const CoverRow = ({
)}
{/* Info */}
<div className="mt-2">
<div className="font-semibold">
<div className='mt-2'>
<div className='font-semibold'>
{/* Name */}
{isSkeleton ? (
<div className="flex w-full -translate-y-px flex-col">
<Skeleton className="w-full leading-tight">
<div className='flex w-full -translate-y-px flex-col'>
<Skeleton className='w-full leading-tight'>
PLACEHOLDER
</Skeleton>
<Skeleton className="w-1/3 translate-y-px leading-tight">
<Skeleton className='w-1/3 translate-y-px leading-tight'>
PLACEHOLDER
</Skeleton>
</div>
) : (
<span className="line-clamp-2 leading-tight ">
<span className='line-clamp-2 leading-tight '>
{/* Playlist private icon */}
{(item as Playlist).privacy && (
<SvgIcon
name="lock"
className="mr-1 mb-1 inline-block h-3 w-3 text-gray-300"
name='lock'
className='mr-1 mb-1 inline-block h-3 w-3 text-gray-300'
/>
)}
<span
onClick={() => goTo(item.id)}
className="decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200"
className='decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-200'
>
{item.name}
</span>
@ -176,11 +181,11 @@ const CoverRow = ({
{/* Subtitle */}
{isSkeleton ? (
<Skeleton className="w-3/5 translate-y-px text-[12px]">
<Skeleton className='w-3/5 translate-y-px text-[12px]'>
PLACEHOLDER
</Skeleton>
) : (
<div className="flex text-[12px] text-gray-500 dark:text-gray-400">
<div className='flex text-[12px] text-gray-500 dark:text-gray-400'>
<span>{getSubtitleText(item, subtitle)}</span>
</div>
)}

View file

@ -3,19 +3,19 @@ import style from './DailyTracksCard.module.scss'
const DailyTracksCard = () => {
return (
<div className="relative h-[198px] cursor-pointer overflow-hidden rounded-2xl">
<div className='relative h-[198px] cursor-pointer overflow-hidden rounded-2xl'>
{/* Cover */}
<img
className={classNames(
'absolute top-0 left-0 w-full will-change-transform',
style.animation
)}
src="https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg?param=1024y1024"
src='https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg?param=1024y1024'
/>
{/* 每日推荐 */}
<div className="absolute flex h-full w-1/2 items-center bg-gradient-to-r from-[#0000004d] to-transparent pl-8">
<div className="grid grid-cols-2 grid-rows-2 gap-2 text-[64px] font-semibold leading-[64px] text-white opacity-[96]">
<div className='absolute flex h-full w-1/2 items-center bg-gradient-to-r from-[#0000004d] to-transparent pl-8'>
<div className='grid grid-cols-2 grid-rows-2 gap-2 text-[64px] font-semibold leading-[64px] text-white opacity-[96]'>
{Array.from('每日推荐').map(word => (
<div key={word}>{word}</div>
))}
@ -23,8 +23,8 @@ const DailyTracksCard = () => {
</div>
{/* Play button */}
<button className="btn-pressed-animation absolute right-6 bottom-6 grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]">
<SvgIcon name="play" className="ml-1 h-4 w-4" />
<button className='btn-pressed-animation absolute right-6 bottom-6 grid h-11 w-11 cursor-default place-content-center rounded-full border border-white border-opacity-[.08] bg-white bg-opacity-[.14] text-white backdrop-blur backdrop-filter transition-all hover:bg-opacity-[.44]'>
<SvgIcon name='play' className='ml-1 h-4 w-4' />
</button>
</div>
)

View file

@ -25,36 +25,36 @@ const FMCard = () => {
return (
<div
className="relative flex h-[198px] overflow-hidden rounded-2xl p-4"
className='relative flex h-[198px] overflow-hidden rounded-2xl p-4'
style={{ background }}
>
<img className="rounded-lg shadow-2xl" src={coverUrl} />
<img className='rounded-lg shadow-2xl' src={coverUrl} />
<div className="ml-5 flex w-full flex-col justify-between text-white">
<div className='ml-5 flex w-full flex-col justify-between text-white'>
{/* Track info */}
<div>
<div className="text-xl font-semibold">How Can I Make It OK?</div>
<div className="opacity-75">Wolf Alice</div>
<div className='text-xl font-semibold'>How Can I Make It OK?</div>
<div className='opacity-75'>Wolf Alice</div>
</div>
<div className="flex items-center justify-between">
<div className='flex items-center justify-between'>
{/* Actions */}
<div>
{Object.values(ACTION).map(action => (
<button
key={action}
className="btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-2 transition duration-200 after:bg-white/10"
className='btn-pressed-animation btn-hover-animation mr-1 cursor-default rounded-lg p-2 transition duration-200 after:bg-white/10'
>
<SvgIcon name={action} className="h-5 w-5" />
<SvgIcon name={action} className='h-5 w-5' />
</button>
))}
</div>
{/* FM logo */}
<div className="right-4 bottom-5 flex text-white opacity-20">
<SvgIcon name="fm" className="mr-2 h-5 w-5" />
<span className="font-semibold">FM</span>
<div className='right-4 bottom-5 flex text-white opacity-20'>
<SvgIcon name='fm' className='mr-2 h-5 w-5' />
<span className='font-semibold'>FM</span>
</div>
</div>
</div>

View file

@ -4,11 +4,11 @@ import Topbar from '@/components/Topbar'
const Main = () => {
return (
<div
id="mainContainer"
className="relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]"
id='mainContainer'
className='relative flex h-screen max-h-screen flex-grow flex-col overflow-y-auto bg-white dark:bg-[#1d1d1d]'
>
<Topbar />
<main id="main" className="mb-24 flex-grow px-8">
<main id='main' className='mb-24 flex-grow px-8'>
<Router />
</main>
</div>

View file

@ -29,39 +29,39 @@ const PlayingTrack = () => {
return (
<Fragment>
{track && (
<div className="flex items-center gap-3">
<div className='flex items-center gap-3'>
{track?.al?.picUrl && (
<img
onClick={toAlbum}
className="aspect-square h-full rounded-md shadow-md"
className='aspect-square h-full rounded-md shadow-md'
src={resizeImage(track?.al?.picUrl ?? '', 'sm')}
/>
)}
{!track?.al?.picUrl && (
<div
onClick={toAlbum}
className="flex aspect-square h-full items-center justify-center rounded-md bg-black/[.04] shadow-sm"
className='flex aspect-square h-full items-center justify-center rounded-md bg-black/[.04] shadow-sm'
>
<SvgIcon className="h-6 w-6 text-gray-300" name="music-note" />
<SvgIcon className='h-6 w-6 text-gray-300' name='music-note' />
</div>
)}
<div className="flex flex-col justify-center leading-tight">
<div className='flex flex-col justify-center leading-tight'>
<div
onClick={toTrackListSource}
className="line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300"
className='line-clamp-1 font-semibold text-black decoration-gray-600 decoration-2 hover:underline dark:text-white dark:decoration-gray-300'
>
{track?.name}
</div>
<div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
<div className='mt-0.5 text-xs text-gray-500 dark:text-gray-400'>
<ArtistInline artists={track?.ar ?? []} />
</div>
</div>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon
className="h-4 w-4 text-black dark:text-white"
name="heart-outline"
className='h-4 w-4 text-black dark:text-white'
name='heart-outline'
/>
</IconButton>
</div>
@ -76,22 +76,22 @@ const MediaControls = () => {
const state = useMemo(() => playerSnapshot.state, [playerSnapshot.state])
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
<div className="flex items-center justify-center gap-2 text-black dark:text-white">
<div className='flex items-center justify-center gap-2 text-black dark:text-white'>
<IconButton onClick={() => track && player.prevTrack()} disabled={!track}>
<SvgIcon className="h-4 w-4" name="previous" />
<SvgIcon className='h-4 w-4' name='previous' />
</IconButton>
<IconButton
onClick={() => track && player.playOrPause()}
disabled={!track}
className="rounded-2xl"
className='rounded-2xl'
>
<SvgIcon
className="h-[1.5rem] w-[1.5rem] "
className='h-[1.5rem] w-[1.5rem] '
name={state === PlayerState.PLAYING ? 'pause' : 'play'}
/>
</IconButton>
<IconButton onClick={() => track && player.nextTrack()} disabled={!track}>
<SvgIcon className="h-4 w-4" name="next" />
<SvgIcon className='h-4 w-4' name='next' />
</IconButton>
</div>
)
@ -99,21 +99,21 @@ const MediaControls = () => {
const Others = () => {
return (
<div className="flex items-center justify-end gap-2 pr-2 text-black dark:text-white">
<div className='flex items-center justify-end gap-2 pr-2 text-black dark:text-white'>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="playlist" />
<SvgIcon className='h-4 w-4' name='playlist' />
</IconButton>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="repeat" />
<SvgIcon className='h-4 w-4' name='repeat' />
</IconButton>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="shuffle" />
<SvgIcon className='h-4 w-4' name='shuffle' />
</IconButton>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="volume" />
<SvgIcon className='h-4 w-4' name='volume' />
</IconButton>
<IconButton onClick={() => toast('Work in progress')}>
<SvgIcon className="h-4 w-4" name="chevron-up" />
<SvgIcon className='h-4 w-4' name='chevron-up' />
</IconButton>
</div>
)
@ -128,7 +128,7 @@ const Progress = () => {
const track = useMemo(() => playerSnapshot.track, [playerSnapshot.track])
return (
<div className="absolute w-screen">
<div className='absolute w-screen'>
{track && (
<Slider
min={0}
@ -141,7 +141,7 @@ const Progress = () => {
/>
)}
{!track && (
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
)}
</div>
)
@ -149,7 +149,7 @@ const Progress = () => {
const Player = () => {
return (
<div className="fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]">
<div className='fixed bottom-0 left-0 right-0 grid h-16 grid-cols-3 grid-rows-1 bg-white bg-opacity-[.86] py-2.5 px-5 backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'>
<Progress />
<PlayingTrack />

View file

@ -3,6 +3,7 @@ import SvgIcon from '@/components/SvgIcon'
import { prefetchPlaylist } from '@/hooks/usePlaylist'
import useUser from '@/hooks/useUser'
import useUserPlaylists from '@/hooks/useUserPlaylists'
import { scrollToTop } from '@/utils/common'
interface Tab {
name: string
@ -34,9 +35,10 @@ const primaryTabs: PrimaryTab[] = [
const PrimaryTabs = () => {
return (
<div>
<div className="app-region-drag h-14"></div>
<div className='app-region-drag h-14'></div>
{primaryTabs.map(tab => (
<NavLink
onClick={() => scrollToTop()}
key={tab.route}
to={tab.route}
className={({ isActive }: { isActive: boolean }) =>
@ -47,12 +49,12 @@ const PrimaryTabs = () => {
)
}
>
<SvgIcon className="mr-3 h-6 w-6" name={tab.icon} />
<span className="font-semibold">{tab.name}</span>
<SvgIcon className='mr-3 h-6 w-6' name={tab.icon} />
<span className='font-semibold'>{tab.name}</span>
</NavLink>
))}
<div className="mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10"></div>
<div className='mx-5 my-2 h-px bg-black opacity-5 dark:bg-white dark:opacity-10'></div>
</div>
)
}
@ -65,10 +67,11 @@ const Playlists = () => {
})
return (
<div className="mb-16 overflow-auto pb-2">
<div className='mb-16 overflow-auto pb-2'>
{playlists?.playlist?.map(playlist => (
<NavLink
key={playlist.id}
onClick={() => scrollToTop()}
to={`/playlist/${playlist.id}`}
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
className={({ isActive }: { isActive: boolean }) =>
@ -78,7 +81,7 @@ const Playlists = () => {
)
}
>
<span className="line-clamp-1">{playlist.name}</span>
<span className='line-clamp-1'>{playlist.name}</span>
</NavLink>
))}
</div>
@ -88,8 +91,8 @@ const Playlists = () => {
const Sidebar = () => {
return (
<div
id="sidebar"
className="grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:border-gray-500/10 dark:bg-gray-900 dark:bg-opacity-80"
id='sidebar'
className='grid h-screen max-w-sm grid-rows-[12rem_auto] border-r border-gray-300/10 bg-gray-50 bg-opacity-[.85] dark:border-gray-500/10 dark:bg-gray-900 dark:bg-opacity-80'
>
<PrimaryTabs />
<Playlists />

View file

@ -122,18 +122,18 @@ const Slider = ({
return (
<div
className="group flex h-2 -translate-y-[3px] items-center"
className='group flex h-2 -translate-y-[3px] items-center'
ref={sliderRef}
onClick={handleClick}
>
{/* Track */}
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
{/* Passed track */}
<div
className={classNames(
'absolute h-[2px] group-hover:bg-brand-500',
isDragging ? 'bg-brand-500' : 'bg-gray-500 dark:bg-gray-400'
isDragging ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-400'
)}
style={usedTrackStyle}
></div>
@ -148,7 +148,7 @@ const Slider = ({
onClick={e => e.stopPropagation()}
onPointerDown={handlePointerDown}
>
<div className="absolute h-2 w-2 rounded-full bg-brand-500"></div>
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
</div>
</div>
)

View file

@ -27,7 +27,7 @@ const Slider = () => {
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
<div className="absolute h-[2px] w-full bg-gray-500 bg-opacity-10"></div>
<div className='absolute h-[2px] w-full bg-gray-500 bg-opacity-10'></div>
<div
className={classNames(
'absolute h-[2px]',
@ -43,16 +43,16 @@ const Slider = () => {
)}
style={thumbStyle}
>
<div className="absolute h-2 w-2 rounded-full bg-brand-500"></div>
<div className='absolute h-2 w-2 rounded-full bg-brand-500'></div>
</div>
<input
type="range"
min="0"
max="100"
type='range'
min='0'
max='100'
value={value}
onChange={e => setValue(Number(e.target.value))}
className="absolute h-[2px] w-full appearance-none opacity-0"
className='absolute h-[2px] w-full appearance-none opacity-0'
/>
</div>
)

View file

@ -1,8 +1,8 @@
const SvgIcon = ({ name, className }: { name: string; className?: string }) => {
const symbolId = `#icon-${name}`
return (
<svg aria-hidden="true" className={className}>
<use href={symbolId} fill="currentColor" />
<svg aria-hidden='true' className={className}>
<use href={symbolId} fill='currentColor' />
</svg>
)
}

View file

@ -14,14 +14,14 @@ const NavigationButtons = () => {
if (action === ACTION.FORWARD) navigate(1)
}
return (
<div className="flex gap-1">
<div className='flex gap-1'>
{[ACTION.BACK, ACTION.FORWARD].map(action => (
<div
onClick={() => handleNavigate(action)}
key={action}
className="app-region-no-drag btn-hover-animation rounded-lg p-3 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200"
className='app-region-no-drag btn-hover-animation rounded-lg p-3 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200'
>
<SvgIcon className="h-4 w-4" name={action} />
<SvgIcon className='h-4 w-4' name={action} />
</div>
))}
</div>
@ -30,15 +30,15 @@ const NavigationButtons = () => {
const SearchBox = () => {
return (
<div className="app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 px-3 transition duration-300 hover:bg-opacity-10 dark:bg-gray-300 dark:bg-opacity-5">
<div className='app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 px-3 transition duration-300 hover:bg-opacity-10 dark:bg-gray-300 dark:bg-opacity-5'>
<SvgIcon
className="mr-2 h-4 w-4 text-gray-500 transition duration-300 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-200"
name="search"
className='mr-2 h-4 w-4 text-gray-500 transition duration-300 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-200'
name='search'
/>
<input
type="text"
className="w-full bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400"
placeholder="Search"
type='text'
className='w-full bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400'
placeholder='Search'
/>
</div>
)
@ -46,8 +46,8 @@ const SearchBox = () => {
const Settings = () => {
return (
<div className="app-region-no-drag btn-hover-animation rounded-lg p-2.5 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200">
<SvgIcon className="h-5 w-5" name="settings" />
<div className='app-region-no-drag btn-hover-animation rounded-lg p-2.5 text-gray-500 transition duration-300 after:rounded-full after:bg-black/[.06] hover:text-gray-900 dark:text-gray-300 dark:after:bg-white/10 dark:hover:text-gray-200'>
<SvgIcon className='h-5 w-5' name='settings' />
</div>
)
}
@ -59,7 +59,7 @@ const Avatar = () => {
<img
src={user?.profile?.avatarUrl}
onClick={() => navigate('/login')}
className="app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700"
className='app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700'
/>
)
}
@ -83,12 +83,12 @@ const Topbar = () => {
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
)}
>
<div className="flex gap-2">
<div className='flex gap-2'>
<NavigationButtons />
<SearchBox />
</div>
<div className="flex items-center gap-3">
<div className='flex items-center gap-3'>
<Settings />
<Avatar />
</div>

View file

@ -34,7 +34,7 @@ const PlayOrPauseButtonInTrack = memo(
)}
>
<SvgIcon
className="h-3.5 w-3.5 text-brand-500"
className='h-3.5 w-3.5 text-brand-500'
name={isPlaying && isHighlight ? 'pause' : 'play'}
/>
</div>
@ -74,10 +74,10 @@ const Track = memo(
)}
>
{/* Track name and number */}
<div className="col-span-6 grid grid-cols-[2rem_auto] pr-8">
<div className='col-span-6 grid grid-cols-[2rem_auto] pr-8'>
{/* Track number */}
{isSkeleton ? (
<Skeleton className="h-6.5 w-6.5 -translate-x-1"></Skeleton>
<Skeleton className='h-6.5 w-6.5 -translate-x-1'></Skeleton>
) : (
!isHighlight && (
<div
@ -101,9 +101,9 @@ const Track = memo(
)}
{/* Track name */}
<div className="flex">
<div className='flex'>
{isSkeleton ? (
<Skeleton className="text-lg">
<Skeleton className='text-lg'>
PLACEHOLDER123456789012345
</Skeleton>
) : (
@ -120,7 +120,7 @@ const Track = memo(
</div>
{/* Artists */}
<div className="col-span-4 flex items-center">
<div className='col-span-4 flex items-center'>
{isSkeleton ? (
<Skeleton>PLACEHOLDER1234</Skeleton>
) : (
@ -136,7 +136,7 @@ const Track = memo(
</div>
{/* Actions & Track duration */}
<div className="col-span-2 flex items-center justify-end">
<div className='col-span-2 flex items-center justify-end'>
{/* Like button */}
{!isSkeleton && (
<button
@ -150,7 +150,7 @@ const Track = memo(
>
<SvgIcon
name={isLiked ? 'heart' : 'heart-outline'}
className="h-4 w-4 "
className='h-4 w-4 '
/>
</button>
)}
@ -209,15 +209,15 @@ const TracksAlbum = ({
)
return (
<div className="grid w-full">
<div className='grid w-full'>
{/* Tracks table header */}
<div className="mx-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500">
<div className="col-span-6 grid grid-cols-[2rem_auto]">
<div className='mx-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'>
<div className='col-span-6 grid grid-cols-[2rem_auto]'>
<div>#</div>
<div>TITLE</div>
</div>
<div className="col-span-4">ARTIST</div>
<div className="col-span-2 justify-self-end">TIME</div>
<div className='col-span-4'>ARTIST</div>
<div className='col-span-2 justify-self-end'>TIME</div>
</div>
{/* Tracks */}

View file

@ -16,38 +16,38 @@ const TrackListGrid = ({
'grid-cols-1 py-1.5 px-2'
)}
>
<div className="grid grid-cols-[3rem_auto] items-center">
<div className='grid grid-cols-[3rem_auto] items-center'>
{/* Cover */}
<div>
{!isSkeleton && (
<img
src={resizeImage(track.al.picUrl, 'xs')}
className="box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]"
className='box-content h-9 w-9 rounded-md border border-black border-opacity-[.03]'
/>
)}
{isSkeleton && (
<Skeleton className="mr-4 h-9 w-9 rounded-md border border-gray-100" />
<Skeleton className='mr-4 h-9 w-9 rounded-md border border-gray-100' />
)}
</div>
{/* Track name & Artists */}
<div className="flex flex-col justify-center">
<div className='flex flex-col justify-center'>
{!isSkeleton && (
<div
v-if="!isSkeleton"
className="line-clamp-1 break-all text-base font-semibold"
v-if='!isSkeleton'
className='line-clamp-1 break-all text-base font-semibold'
>
{track.name}
</div>
)}
{isSkeleton && (
<Skeleton className="text-base">PLACEHOLDER12345</Skeleton>
<Skeleton className='text-base'>PLACEHOLDER12345</Skeleton>
)}
<div className="text-xs">
<div className='text-xs'>
{!isSkeleton && <ArtistInline artists={track.ar} />}
{isSkeleton && (
<Skeleton className="w-2/3 translate-y-px">PLACE</Skeleton>
<Skeleton className='w-2/3 translate-y-px'>PLACE</Skeleton>
)}
</div>
</div>

View file

@ -33,32 +33,32 @@ const Track = memo(
)}
>
{/* Track info */}
<div className="col-span-6 grid grid-cols-[4.2rem_auto] pr-8">
<div className='col-span-6 grid grid-cols-[4.2rem_auto] pr-8'>
{/* Cover */}
<div>
{isSkeleton ? (
<Skeleton className="mr-4 h-12 w-12 rounded-md border border-gray-100 dark:border-gray-800" />
<Skeleton className='mr-4 h-12 w-12 rounded-md border border-gray-100 dark:border-gray-800' />
) : (
<img
src={resizeImage(track.al.picUrl, 'xs')}
className="box-content h-12 w-12 rounded-md border border-black border-opacity-[.03]"
className='box-content h-12 w-12 rounded-md border border-black border-opacity-[.03]'
/>
)}
</div>
{/* Track name & Artists */}
<div className="flex flex-col justify-center">
<div className='flex flex-col justify-center'>
{isSkeleton ? (
<Skeleton className="text-lg">PLACEHOLDER12345</Skeleton>
<Skeleton className='text-lg'>PLACEHOLDER12345</Skeleton>
) : (
<div className="line-clamp-1 break-all text-lg font-semibold dark:text-white">
<div className='line-clamp-1 break-all text-lg font-semibold dark:text-white'>
{track.name}
</div>
)}
<div className="text-sm text-gray-600 dark:text-gray-400">
<div className='text-sm text-gray-600 dark:text-gray-400'>
{isSkeleton ? (
<Skeleton className="w-2/3 translate-y-px">PLACE</Skeleton>
<Skeleton className='w-2/3 translate-y-px'>PLACE</Skeleton>
) : (
<ArtistInline artists={track.ar} />
)}
@ -67,7 +67,7 @@ const Track = memo(
</div>
{/* Album name */}
<div className="col-span-4 flex items-center text-gray-600 dark:text-gray-400">
<div className='col-span-4 flex items-center text-gray-600 dark:text-gray-400'>
{isSkeleton ? (
<Skeleton>PLACEHOLDER1234567890</Skeleton>
) : (
@ -75,17 +75,17 @@ const Track = memo(
<NavLink
to={`/album/${track.al.id}`}
onMouseOver={() => prefetchAlbum({ id: track.al.id })}
className="hover:underline"
className='hover:underline'
>
{track.al.name}
</NavLink>
<span className="flex-grow"></span>
<span className='flex-grow'></span>
</Fragment>
)}
</div>
{/* Actions & Track duration */}
<div className="col-span-2 flex items-center justify-end">
<div className='col-span-2 flex items-center justify-end'>
{/* Like button */}
{!isSkeleton && (
<button
@ -98,7 +98,7 @@ const Track = memo(
>
<SvgIcon
name={isLiked ? 'heart' : 'heart-outline'}
className="h-4 w-4 "
className='h-4 w-4 '
/>
</button>
)}
@ -107,7 +107,7 @@ const Track = memo(
{isSkeleton ? (
<Skeleton>0:00</Skeleton>
) : (
<div className="min-w-[2.5rem] text-right text-gray-600 dark:text-gray-400">
<div className='min-w-[2.5rem] text-right text-gray-600 dark:text-gray-400'>
{formatDuration(track.dt, 'en', 'hh:mm:ss')}
</div>
)}
@ -146,16 +146,16 @@ const TracksList = memo(
return (
<Fragment>
{/* Tracks table header */}
<div className="ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500">
<div className="col-span-6 grid grid-cols-[4.2rem_auto]">
<div className='ml-2 mr-4 mt-10 mb-2 grid grid-cols-12 border-b border-gray-100 py-2.5 text-sm text-gray-400 dark:border-gray-800 dark:text-gray-500'>
<div className='col-span-6 grid grid-cols-[4.2rem_auto]'>
<div></div>
<div>TITLE</div>
</div>
<div className="col-span-4">ALBUM</div>
<div className="col-span-2 justify-self-end">TIME</div>
<div className='col-span-4'>ALBUM</div>
<div className='col-span-2 justify-self-end'>TIME</div>
</div>
<div className="grid w-full gap-1">
<div className='grid w-full gap-1'>
{/* Tracks */}
{!isSkeleton &&
tracks.map(track => (

View file

@ -10,7 +10,12 @@ import useAlbum from '@/hooks/useAlbum'
import useArtistAlbums from '@/hooks/useArtistAlbums'
import { player } from '@/store'
import { State as PlayerState } from '@/utils/player'
import { formatDate, formatDuration, resizeImage } from '@/utils/common'
import {
formatDate,
formatDuration,
resizeImage,
scrollToTop,
} from '@/utils/common'
const PlayButton = ({
album,
@ -49,7 +54,7 @@ const PlayButton = ({
<Button onClick={wrappedHandlePlay} isSkelton={isLoading}>
<SvgIcon
name={isPlaying && isThisAlbumPlaying ? 'pause' : 'play'}
className="mr-2 h-4 w-4"
className='mr-2 h-4 w-4'
/>
{isPlaying && isThisAlbumPlaying ? '暂停' : '播放'}
</Button>
@ -77,29 +82,29 @@ const Header = ({
return (
<Fragment>
{/* Header background */}
<div className="absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden">
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
{coverUrl && !isCoverError && (
<Fragment>
<img
src={coverUrl}
className="absolute -top-full w-full blur-[100px]"
className='absolute -top-full w-full blur-[100px]'
/>
<img
src={coverUrl}
className="absolute -top-full w-full blur-[100px]"
className='absolute -top-full w-full blur-[100px]'
/>
</Fragment>
)}
<div className="absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]"></div>
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]'></div>
</div>
<div className="grid grid-cols-[17rem_auto] items-center gap-9">
<div className='grid grid-cols-[17rem_auto] items-center gap-9'>
{/* Cover */}
<div className="relative z-0 aspect-square self-start">
<div className='relative z-0 aspect-square self-start'>
{/* Neon shadow */}
{!isLoading && coverUrl && !isCoverError && (
<div
className="absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter"
className='absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter'
style={{
backgroundImage: `url("${coverUrl}")`,
}}
@ -108,41 +113,41 @@ const Header = ({
{!isLoading && isCoverError ? (
// Fallback cover
<div className="flex h-full w-full items-center justify-center rounded-2xl border border-black border-opacity-5 bg-gray-100 text-gray-300">
<SvgIcon name="music-note" className="h-1/2 w-1/2" />
<div className='flex h-full w-full items-center justify-center rounded-2xl border border-black border-opacity-5 bg-gray-100 text-gray-300'>
<SvgIcon name='music-note' className='h-1/2 w-1/2' />
</div>
) : (
coverUrl && (
<img
src={coverUrl}
className="rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5"
className='rounded-2xl border border-b-0 border-black border-opacity-5 dark:border-white dark:border-opacity-5'
onError={() => setCoverError(true)}
/>
)
)}
{isLoading && <Skeleton className="h-full w-full rounded-2xl" />}
{isLoading && <Skeleton className='h-full w-full rounded-2xl' />}
</div>
{/* Info */}
<div className="z-10 flex h-full flex-col justify-between">
<div className='z-10 flex h-full flex-col justify-between'>
{/* Name */}
{isLoading ? (
<Skeleton className="w-3/4 text-6xl">PLACEHOLDER</Skeleton>
<Skeleton className='w-3/4 text-6xl'>PLACEHOLDER</Skeleton>
) : (
<div className="text-6xl font-bold dark:text-white">
<div className='text-6xl font-bold dark:text-white'>
{album?.name}
</div>
)}
{/* Artist */}
{isLoading ? (
<Skeleton className="mt-5 w-64 text-lg">PLACEHOLDER</Skeleton>
<Skeleton className='mt-5 w-64 text-lg'>PLACEHOLDER</Skeleton>
) : (
<div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300">
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
Album by{' '}
<NavLink
to={`/artist/${album?.artist.name}`}
className="cursor-default font-semibold hover:underline"
className='cursor-default font-semibold hover:underline'
>
{album?.artist.name}
</NavLink>
@ -151,11 +156,11 @@ const Header = ({
{/* Release date & track count & album duration */}
{isLoading ? (
<Skeleton className="w-72 translate-y-px text-sm">
<Skeleton className='w-72 translate-y-px text-sm'>
PLACEHOLDER
</Skeleton>
) : (
<div className="text-sm font-thin text-gray-500 dark:text-gray-400">
<div className='text-sm font-thin text-gray-500 dark:text-gray-400'>
{dayjs(album?.publishTime || 0).year()} · {album?.size} Songs,{' '}
{albumDuration}
</div>
@ -163,17 +168,17 @@ const Header = ({
{/* Description */}
{isLoading ? (
<Skeleton className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
<Skeleton className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
PLACEHOLDER
</Skeleton>
) : (
<div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400">
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
{album?.description}
</div>
)}
{/* Buttons */}
<div className="mt-5 flex gap-4">
<div className='mt-5 flex gap-4'>
<PlayButton {...{ album, handlePlay, isLoading }} />
<Button
@ -181,7 +186,7 @@ const Header = ({
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="heart" className="h-4 w-4" />
<SvgIcon name='heart' className='h-4 w-4' />
</Button>
<Button
@ -190,7 +195,7 @@ const Header = ({
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="more" className="h-4 w-4" />
<SvgIcon name='more' className='h-4 w-4' />
</Button>
</div>
</div>
@ -208,7 +213,6 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
const filteredAlbums = useMemo((): Album[] => {
if (!albums) return []
const albumID = album?.id
const allReleases = albums?.hotAlbums || []
const filteredAlbums = allReleases.filter(
album =>
@ -218,19 +222,18 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
const qualifiedAlbums = [...filteredAlbums, ...singles]
const formatName = (name: string) =>
name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '')
const uniqueAlbums: Album[] = []
qualifiedAlbums.forEach(a => {
// 去除当前页面的专辑
if (Number(a.id) === Number(albumID) || album?.name === a.name) return
if (formatName(a.name) === formatName(album?.name ?? '')) return
// 去除重复的专辑(包含 deluxe edition 的专辑会视为重复)
if (
uniqueAlbums.findIndex(aa => {
return (
a.name === aa.name ||
a.name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '') ===
aa.name.toLowerCase().replace(/(\s|deluxe|edition|\(|\))/g, '')
)
return formatName(a.name) === formatName(aa.name)
}) !== -1
) {
return
@ -248,25 +251,27 @@ const MoreAlbum = ({ album }: { album: Album | undefined }) => {
})
return uniqueAlbums.slice(0, 5)
}, [album?.id, album?.name, albums])
}, [album?.name, albums])
return (
<div>
<div className="my-5 h-px w-full bg-gray-100 dark:bg-gray-800"></div>
<div className="pl-px text-[1.375rem] font-semibold text-gray-800 dark:text-gray-100">
<div className='my-5 h-px w-full bg-gray-100 dark:bg-gray-800'></div>
<div className='pl-px text-[1.375rem] font-semibold text-gray-800 dark:text-gray-100'>
More by{' '}
<NavLink
to={`/artist/${album?.artist?.id}`}
className="cursor-default hover:underline"
className='cursor-default hover:underline'
>
{album?.artist.name}
</NavLink>
</div>
<div className="mt-3">
<div className='mt-3'>
<CoverRow
albums={filteredAlbums}
subtitle={Subtitle.TYPE_RELEASE_YEAR}
isSkeleton={isLoading}
rows={1}
navigateCallback={scrollToTop}
/>
</div>
</div>
@ -289,7 +294,7 @@ const Album = () => {
}
return (
<div className="mt-10">
<div className='mt-10'>
<Header
album={album?.album}
isLoading={isLoading}
@ -301,10 +306,10 @@ const Album = () => {
isSkeleton={isLoading}
/>
{album?.album && (
<div className="mt-5 text-xs text-gray-400">
<div className='mt-5 text-xs text-gray-400'>
<div> Released {formatDate(album.album.publishTime || 0, 'en')} </div>
{album.album.company && (
<div className="mt-[2px]">© {album.album.company} </div>
<div className='mt-[2px]'>© {album.album.company} </div>
)}
</div>
)}

View file

@ -14,15 +14,15 @@ export default function Home() {
return (
<div>
<CoverRow
title="Good Morning"
title='Good Morning'
playlists={recommendedPlaylists?.result.slice(0, 10) ?? []}
isSkeleton={isLoadingRecommendedPlaylists}
/>
<div className="mt-10 mb-4 text-[28px] font-bold text-black dark:text-white">
<div className='mt-10 mb-4 text-[28px] font-bold text-black dark:text-white'>
For You
</div>
<div className="grid grid-cols-2 gap-6">
<div className='grid grid-cols-2 gap-6'>
<DailyTracksCard />
<FMCard />
</div>

View file

@ -21,13 +21,13 @@ const EmailInput = ({
setEmail: (email: string) => void
}) => {
return (
<div className="w-full">
<div className="mb-1 text-sm font-medium text-gray-700">Email</div>
<div className='w-full'>
<div className='mb-1 text-sm font-medium text-gray-700'>Email</div>
<input
value={email}
onChange={e => setEmail(e.target.value)}
className="w-full rounded-md border border-gray-300 px-2 py-2"
type="email"
className='w-full rounded-md border border-gray-300 px-2 py-2'
type='email'
/>
</div>
)
@ -45,9 +45,9 @@ const PhoneInput = ({
setPhone: (phone: string) => void
}) => {
return (
<div className="w-full">
<div className="mb-1 text-sm font-medium text-gray-700">Phone</div>
<div className="flex w-full">
<div className='w-full'>
<div className='mb-1 text-sm font-medium text-gray-700'>Phone</div>
<div className='flex w-full'>
<input
className={classNames(
'rounded-md rounded-r-none border border-r-0 border-gray-300 px-3 py-2',
@ -55,14 +55,14 @@ const PhoneInput = ({
countryCode.length == 4 && 'w-16',
countryCode.length >= 5 && 'w-20'
)}
type="text"
placeholder="+86"
type='text'
placeholder='+86'
value={countryCode}
onChange={e => setCountryCode(e.target.value)}
/>
<input
className="flex-grow rounded-md rounded-l-none border border-gray-300 px-3 py-2"
type="text"
className='flex-grow rounded-md rounded-l-none border border-gray-300 px-3 py-2'
type='text'
value={phone}
onChange={e => setPhone(e.target.value)}
/>
@ -80,22 +80,22 @@ const PasswordInput = ({
}) => {
const [showPassword, setShowPassword] = useState(false)
return (
<div className="mt-3 flex w-full flex-col">
<div className="mb-1 text-sm font-medium text-gray-700">Password</div>
<div className="flex w-full">
<div className='mt-3 flex w-full flex-col'>
<div className='mb-1 text-sm font-medium text-gray-700'>Password</div>
<div className='flex w-full'>
<input
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full rounded-md rounded-r-none border border-r-0 border-gray-300 px-2 py-2"
className='w-full rounded-md rounded-r-none border border-r-0 border-gray-300 px-2 py-2'
type={showPassword ? 'text' : 'password'}
/>
<div className="flex items-center justify-center rounded-md rounded-l-none border border-l-0 border-gray-300 pr-1">
<div className='flex items-center justify-center rounded-md rounded-l-none border border-l-0 border-gray-300 pr-1'>
<button
onClick={() => setShowPassword(!showPassword)}
className="cursor-default rounded p-1.5 text-gray-400 transition duration-300 hover:bg-gray-100 hover:text-gray-600"
className='cursor-default rounded p-1.5 text-gray-400 transition duration-300 hover:bg-gray-100 hover:text-gray-600'
>
<SvgIcon
className="h-5 w-5"
className='h-5 w-5'
name={showPassword ? 'eye-off' : 'eye'}
/>
</button>
@ -153,21 +153,21 @@ const OtherLoginMethods = ({
]
return (
<Fragment>
<div className="mt-8 mb-4 flex w-full items-center">
<span className="h-px flex-grow bg-gray-300"></span>
<span className="mx-2 text-sm text-gray-400">or</span>
<span className="h-px flex-grow bg-gray-300"></span>
<div className='mt-8 mb-4 flex w-full items-center'>
<span className='h-px flex-grow bg-gray-300'></span>
<span className='mx-2 text-sm text-gray-400'>or</span>
<span className='h-px flex-grow bg-gray-300'></span>
</div>
<div className="flex gap-3">
<div className='flex gap-3'>
{otherLoginMethods.map(
({ id, name }) =>
method !== id && (
<button
key={id}
onClick={() => setMethod(id)}
className="flex w-full cursor-default items-center justify-center rounded-lg bg-gray-100 py-2 font-medium text-gray-600 transition duration-300 hover:bg-gray-200 hover:text-gray-800"
className='flex w-full cursor-default items-center justify-center rounded-lg bg-gray-100 py-2 font-medium text-gray-600 transition duration-300 hover:bg-gray-200 hover:text-gray-800'
>
<SvgIcon className="mr-2 h-5 w-5" name={id} />
<SvgIcon className='mr-2 h-5 w-5' name={id} />
<span>{name}</span>
</button>
)
@ -274,11 +274,11 @@ const LoginWithQRCode = () => {
}, [qrCodeUrl])
const qrCodeMessage = 'test'
return (
<div className="flex flex-col items-center justify-center">
<div className="rounded-3xl border p-6">
<img src={qrCodeImage} alt="QR Code" className="no-drag" />
<div className='flex flex-col items-center justify-center'>
<div className='rounded-3xl border p-6'>
<img src={qrCodeImage} alt='QR Code' className='no-drag' />
</div>
<div className="mt-4 text-sm text-gray-500">{qrCodeMessage}</div>
<div className='mt-4 text-sm text-gray-500'>{qrCodeMessage}</div>
</div>
)
}
@ -287,8 +287,8 @@ export default function Login() {
const [method, setMethod] = useState<Method>(Method.PHONE)
return (
<div className="grid h-full place-content-center">
<div className="w-80">
<div className='grid h-full place-content-center'>
<div className='w-80'>
{method === Method.EMAIL && <LoginWithEmail />}
{method === Method.PHONE && <LoginWithPhone />}
{method === Method.QRCODE && <LoginWithQRCode />}

View file

@ -27,18 +27,18 @@ const Header = memo(
return (
<Fragment>
{/* Header background */}
<div className="absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden">
<img src={coverUrl} className="absolute top-0 w-full blur-[100px]" />
<img src={coverUrl} className="absolute top-0 w-full blur-[100px]" />
<div className="absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]"></div>
<div className='absolute top-0 left-0 z-0 h-[24rem] w-full overflow-hidden'>
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
<img src={coverUrl} className='absolute top-0 w-full blur-[100px]' />
<div className='absolute top-0 h-full w-full bg-gradient-to-b from-white/[.84] to-white dark:from-black/[.5] dark:to-[#1d1d1d]'></div>
</div>
<div className="grid grid-cols-[16rem_auto] items-center gap-9">
<div className='grid grid-cols-[16rem_auto] items-center gap-9'>
{/* Cover */}
<div className="relative z-0 aspect-square self-start">
<div className='relative z-0 aspect-square self-start'>
{!isLoading && (
<div
className="absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter"
className='absolute top-3.5 z-[-1] h-full w-full scale-x-[.92] scale-y-[.96] rounded-2xl bg-cover opacity-40 blur-lg filter'
style={{
backgroundImage: `url("${coverUrl}")`,
}}
@ -48,70 +48,70 @@ const Header = memo(
{!isLoading && (
<img
src={coverUrl}
className="rounded-2xl border border-black border-opacity-5"
className='rounded-2xl border border-black border-opacity-5'
/>
)}
{isLoading && (
<Skeleton v-else className="h-full w-full rounded-2xl" />
<Skeleton v-else className='h-full w-full rounded-2xl' />
)}
</div>
{/* <!-- Playlist info --> */}
<div className="z-10 flex h-full flex-col justify-between">
<div className='z-10 flex h-full flex-col justify-between'>
{/* <!-- Playlist name --> */}
{!isLoading && (
<div className="text-4xl font-bold dark:text-white">
<div className='text-4xl font-bold dark:text-white'>
{playlist?.name}
</div>
)}
{isLoading && (
<Skeleton v-else className="w-3/4 text-4xl">
<Skeleton v-else className='w-3/4 text-4xl'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist creator --> */}
{!isLoading && (
<div className="mt-5 text-lg font-medium text-gray-800 dark:text-gray-300">
<div className='mt-5 text-lg font-medium text-gray-800 dark:text-gray-300'>
Playlist by <span>{playlist?.creator?.nickname}</span>
</div>
)}
{isLoading && (
<Skeleton v-else className="mt-5 w-64 text-lg">
<Skeleton v-else className='mt-5 w-64 text-lg'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist last update time & track count --> */}
{!isLoading && (
<div className="text-sm font-thin text-gray-500 dark:text-gray-400">
<div className='text-sm font-thin text-gray-500 dark:text-gray-400'>
Updated at
{formatDate(playlist?.updateTime || 0, 'en')} ·
{playlist?.trackCount} Songs
</div>
)}
{isLoading && (
<Skeleton v-else className="w-72 translate-y-px text-sm">
<Skeleton v-else className='w-72 translate-y-px text-sm'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Playlist description --> */}
{!isLoading && (
<div className="line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400">
<div className='line-clamp-2 mt-5 min-h-[2.5rem] text-sm text-gray-500 dark:text-gray-400'>
{playlist?.description}
</div>
)}
{isLoading && (
<Skeleton v-else className="mt-5 min-h-[2.5rem] w-1/2 text-sm">
<Skeleton v-else className='mt-5 min-h-[2.5rem] w-1/2 text-sm'>
PLACEHOLDER
</Skeleton>
)}
{/* <!-- Buttons --> */}
<div className="mt-5 flex gap-4">
<div className='mt-5 flex gap-4'>
<Button onClick={() => handlePlay()} isSkelton={isLoading}>
<SvgIcon name="play" className="mr-2 h-4 w-4" />
<SvgIcon name='play' className='mr-2 h-4 w-4' />
PLAY
</Button>
@ -120,7 +120,7 @@ const Header = memo(
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="heart" className="h-4 w-4" />
<SvgIcon name='heart' className='h-4 w-4' />
</Button>
<Button
@ -129,7 +129,7 @@ const Header = memo(
isSkelton={isLoading}
onClick={() => toast('Work in progress')}
>
<SvgIcon name="more" className="h-4 w-4" />
<SvgIcon name='more' className='h-4 w-4' />
</Button>
</div>
</div>
@ -225,7 +225,7 @@ const Playlist = () => {
)
return (
<div className="mt-10">
<div className='mt-10'>
<Header
playlist={playlist?.playlist}
isLoading={isLoading}

View file

@ -102,3 +102,9 @@ export function formatDuration(
export function sleep(time: number) {
return new Promise(resolve => setTimeout(resolve, time))
}
export function scrollToTop(smooth = false) {
const main = document.getElementById('mainContainer')
if (!main) return
main.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' })
}

View file

@ -9,6 +9,7 @@ module.exports = {
bracketSpacing: true,
htmlWhitespaceSensitivity: 'strict',
singleQuote: true,
jsxSingleQuote: true,
// Tailwind CSS
plugins: [require('prettier-plugin-tailwindcss')],