feat: 增加一些新组件

This commit is contained in:
qier222 2022-05-14 14:39:10 +08:00
parent 0520af8466
commit e9d82dd792
No known key found for this signature in database
GPG key ID: 9C85007ED905F14D
17 changed files with 124 additions and 256 deletions

View file

@ -1,13 +1,10 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { NowPlaying } from './NowPlaying'
import NowPlaying from './NowPlaying'
export default {
title: 'NowPlaying',
component: NowPlaying,
argTypes: {
backgroundColor: { control: 'color' },
},
parameters: {
viewport: {
defaultViewport: 'iphone8p',
@ -18,7 +15,3 @@ export default {
const Template: ComponentStory<typeof NowPlaying> = args => <NowPlaying />
export const Primary = Template.bind({})
Primary.args = {
primary: true,
label: 'NowPlaying',
}

View file

@ -2,7 +2,7 @@ import React from 'react'
import cx from 'classnames'
import SvgIcon from './SvgIcon'
export const NowPlaying = () => {
const NowPlaying = () => {
return (
<div className='relative flex aspect-square w-full flex-col justify-end overflow-hidden rounded-3xl'>
{/* Cover */}
@ -38,10 +38,12 @@ export const NowPlaying = () => {
{/* Controls */}
<div className='mt-4 flex w-full items-center justify-between'>
<SvgIcon
name='shuffle'
className='h-7 w-7 text-black/90 dark:text-white/40'
/>
<button>
<SvgIcon
name='shuffle'
className='h-7 w-7 text-black/90 dark:text-white/40'
/>
</button>
<div className='text-black/95 dark:text-white/80'>
<button className='rounded-full bg-black/10 p-[10px] dark:bg-white/10'>
@ -55,12 +57,16 @@ export const NowPlaying = () => {
</button>
</div>
<SvgIcon
name='repeat-1'
className='h-7 w-7 text-black/90 dark:text-white/40'
/>
<button>
<SvgIcon
name='repeat-1'
className='h-7 w-7 text-black/90 dark:text-white/40'
/>{' '}
</button>
</div>
</div>
</div>
)
}
export default NowPlaying

View file

@ -0,0 +1,16 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Sidebar from './Sidebar'
export default {
title: 'Sidebar',
component: Sidebar,
} as ComponentMeta<typeof Sidebar>
const Template: ComponentStory<typeof Sidebar> = args => (
<div className='h-[calc(100vh_-_32px)] w-min rounded-l-3xl bg-[#F8F8F8] dark:bg-black'>
<Sidebar />
</div>
)
export const Primary = Template.bind({})

View file

@ -1,107 +1,29 @@
import { NavLink } from 'react-router-dom'
import SvgIcon from './SvgIcon'
import useUserPlaylists from '@/web/hooks/useUserPlaylists'
import { scrollToTop } from '@/web/utils/common'
import { prefetchPlaylist } from '@/web/hooks/usePlaylist'
import { player } from '@/web/store'
import { Mode, TrackListSourceType } from '@/web/utils/player'
import React from 'react'
import cx from 'classnames'
import { useMemo } from 'react'
import { useSnapshot } from 'valtio'
const primaryTabs = [
{
name: '主页',
icon: 'home',
route: '/',
},
{
name: '播客',
icon: 'podcast',
route: '/podcast',
},
{
name: '音乐库',
icon: 'music-library',
route: '/library',
},
] as const
const PrimaryTabs = () => {
return (
<div>
<div className={cx(window.env?.isMac && 'app-region-drag', 'h-14')}></div>
{primaryTabs.map(tab => (
<NavLink
onClick={() => scrollToTop()}
key={tab.route}
to={tab.route}
className={({ isActive }) =>
cx(
'btn-hover-animation mx-3 flex cursor-default items-center rounded-lg px-3 py-2 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:after:bg-white/20',
!isActive && 'text-gray-700 dark:text-white',
isActive && 'text-brand-500 '
)
}
>
<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>
)
}
const Playlists = () => {
const { data: playlists } = useUserPlaylists()
const playerSnapshot = useSnapshot(player)
const currentPlaylistID = useMemo(
() => playerSnapshot.trackListSource?.id,
[playerSnapshot.trackListSource]
)
const playlistMode = useMemo(
() => playerSnapshot.trackListSource?.type,
[playerSnapshot.trackListSource]
)
const mode = useMemo(() => playerSnapshot.mode, [playerSnapshot.mode])
return (
<div className='mb-16 overflow-auto pb-2'>
{playlists?.playlist?.map(playlist => (
<NavLink
onMouseOver={() => prefetchPlaylist({ id: playlist.id })}
key={playlist.id}
onClick={() => scrollToTop()}
to={`/playlist/${playlist.id}`}
className={({ isActive }: { isActive: boolean }) =>
cx(
'btn-hover-animation line-clamp-1 my-px mx-3 flex cursor-default items-center justify-between rounded-lg px-3 py-[0.38rem] text-sm text-black opacity-70 transition-colors duration-200 after:scale-[0.97] after:bg-black/[.06] dark:text-white dark:after:bg-white/20',
isActive && 'after:scale-100 after:opacity-100'
)
}
>
<span className='line-clamp-1'>{playlist.name}</span>
{playlistMode === TrackListSourceType.Playlist &&
mode === Mode.TrackList &&
currentPlaylistID === playlist.id && (
<SvgIcon className='h-5 w-5' name='volume-half' />
)}
</NavLink>
))}
</div>
)
}
import SvgIcon from './SvgIcon'
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'
>
<PrimaryTabs />
<Playlists />
<div className='relative flex h-full w-[104px] flex-col justify-center'>
<div className='grid grid-cols-1 justify-items-center gap-12 text-black/10 dark:text-white/20'>
<SvgIcon
name='my'
className='h-10 w-10 text-brand-600 dark:text-brand-700'
/>
<SvgIcon name='explore' className='h-10 w-10' />
<SvgIcon name='discovery' className='h-10 w-10' />
<SvgIcon name='lyrics' className='h-10 w-10' />
</div>
<div
className='absolute bottom-8 right-0 left-0 flex rotate-180 items-center font-medium text-brand-600 dark:text-brand-700'
style={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
letterSpacing: '0.5px',
}}
>
<span>USER PAGE</span>
</div>
</div>
)
}

View file

@ -0,0 +1,16 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Topbar from './Topbar'
export default {
title: 'Topbar',
component: Topbar,
} as ComponentMeta<typeof Topbar>
const Template: ComponentStory<typeof Topbar> = args => (
<div className='w-[calc(100vw_-_32px)] rounded-3xl bg-[#F8F8F8] px-11 dark:bg-black'>
<Topbar />
</div>
)
export const Primary = Template.bind({})

View file

@ -1,114 +1,44 @@
import SvgIcon from '@/web/components/SvgIcon'
import useScroll from '@/web/hooks/useScroll'
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import Avatar from './Avatar'
import cx from 'classnames'
const NavigationButtons = () => {
const navigate = useNavigate()
enum ACTION {
Back = 'back',
Forward = 'forward',
}
const handleNavigate = (action: ACTION) => {
if (action === ACTION.Back) navigate(-1)
if (action === ACTION.Forward) navigate(1)
}
return (
<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-2 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={action} />
</div>
))}
</div>
)
}
const SearchBox = () => {
const { type } = useParams()
const [keywords, setKeywords] = useState('')
const navigate = useNavigate()
const toSearch = (e: React.KeyboardEvent) => {
if (!keywords) return
if (e.key === 'Enter') {
navigate(`/search/${keywords}${type ? `/${type}` : ''}`)
}
}
return (
<div className='app-region-no-drag group flex w-[16rem] cursor-text items-center rounded-full bg-gray-500 bg-opacity-5 pl-2.5 pr-2 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'
/>
<input
value={keywords}
onChange={e => setKeywords(e.target.value)}
onKeyDown={toSearch}
type='text'
className='flex-grow bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400'
placeholder='搜索'
/>
<div
onClick={() => setKeywords('')}
className={cx(
'cursor-default rounded-full p-1 text-gray-600 transition hover:bg-gray-400/20 dark:text-white/50 dark:hover:bg-white/20',
!keywords && 'hidden'
)}
>
<SvgIcon className='h-4 w-4' name='x' />
</div>
</div>
)
}
const Settings = () => {
const navigate = useNavigate()
return (
<div
onClick={() => navigate('/settings')}
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-[1.125rem] w-[1.125rem]' name='settings' />
</div>
)
}
import SvgIcon from './SvgIcon'
const Topbar = () => {
/**
* Show topbar background when scroll down
*/
const [mainContainer, setMainContainer] = useState<HTMLElement | null>(null)
const scroll = useScroll(mainContainer, { throttle: 100 })
useEffect(() => {
setMainContainer(document.getElementById('mainContainer'))
}, [setMainContainer])
return (
<div
className={cx(
'sticky z-30 flex h-16 min-h-[4rem] w-full cursor-default items-center justify-between px-8 transition duration-300',
window.env?.isMac && 'app-region-drag',
window.env?.isEnableTitlebar ? 'top-8' : 'top-0',
!scroll.arrivedState.top &&
'bg-white bg-opacity-[.86] backdrop-blur-xl backdrop-saturate-[1.8] dark:bg-[#222] dark:bg-opacity-[.86]'
)}
>
<div className='flex gap-2'>
<NavigationButtons />
<SearchBox />
<div className='flex w-full items-center justify-between pt-11 pb-10'>
{/* Left Part */}
<div className='flex items-center'>
{/* Navigation Buttons */}
<button className='rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
<SvgIcon name='back' className='h-7 w-7 text-[#717171]' />
</button>
<button className='ml-[10px] rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
<SvgIcon name='forward' className='h-7 w-7 text-[#717171]' />
</button>
{/* Dividing line */}
<div className='mx-6 h-4 w-px bg-black/20 dark:bg-white/20'></div>
{/* Search Box */}
<div className='flex min-w-[284px] items-center rounded-full bg-[#E9E9E9] p-[10px] text-[#717171] dark:bg-[#0E0E0E]'>
<SvgIcon name='search' className='mr-[10px] h-7 w-7' />
<input
placeholder='Search Song Name'
className='bg-transparent placeholder:text-[#717171]'
/>
</div>
</div>
<div className='flex items-center gap-3'>
<Settings />
<Avatar />
{/* Right Part */}
<div className='flex'>
<button className='rounded-full bg-[#E9E9E9] p-[10px] dark:bg-[#0E0E0E]'>
<SvgIcon name='placeholder' className='h-7 w-7 text-[#717171]' />
</button>
{/* Avatar */}
<div>
<img
className='ml-3 h-12 w-12 rounded-full'
src='http://p1.music.126.net/AetIV1GOZiLKk1yy8PMPfw==/109951165378042240.jpg'
/>
</div>
</div>
</div>
)