portfolio/components/ui/SpotifyPlayer.tsx

213 lines
5.5 KiB
TypeScript
Raw Normal View History

2023-07-29 22:40:36 +00:00
'use client'
2024-09-27 20:02:51 +00:00
import useSWR, {useSWRConfig} from 'swr'
2022-12-06 12:54:34 +00:00
import fetcher from '@/lib/fetcher'
2023-01-14 19:31:05 +00:00
import Image from 'next/image'
2022-12-06 12:54:34 +00:00
import Link from 'next/link'
import clsx from 'clsx'
2024-09-27 20:02:51 +00:00
import {ElementType, ReactElement, useEffect} from 'react'
2022-12-06 12:54:34 +00:00
import {animate} from 'motion'
2023-01-14 19:31:05 +00:00
type Status = {
as?: ElementType
isPlaying: boolean
}
type Artist = {
as?: ElementType
artist: string
}
type Title = {
as?: ElementType
title: string
songUrl: string
className?: string
}
type Album = {
album: string
albumImageUrl: string
}
type Song = {
as?: ElementType
} & Artist & Title & Album & Status
2022-12-06 12:54:34 +00:00
function AnimatedBars() {
useEffect(() => {
animate(
'#bar1',
{
transform: [
'scaleY(1.0) translateY(0rem)',
'scaleY(1.5) translateY(-0.082rem)',
'scaleY(1.0) translateY(0rem)'
]
},
{
duration: 1.0,
repeat: Infinity,
easing: ['ease-in-out']
}
);
animate(
'#bar2',
{
transform: [
'scaleY(1.0) translateY(0rem)',
'scaleY(3) translateY(-0.083rem)',
'scaleY(1.0) translateY(0rem)'
]
},
{
delay: 0.2,
duration: 1.5,
repeat: Infinity,
easing: ['ease-in-out']
}
);
animate(
'#bar3',
{
transform: [
'scaleY(1.0) translateY(0rem)',
'scaleY(0.5) translateY(0.37rem)',
'scaleY(1.0) translateY(0rem)'
]
},
{
delay: 0.3,
duration: 1.5,
repeat: Infinity,
easing: ['ease-in-out']
}
);
}, [])
return (
<div className="w-auto flex items-end overflow-hidden flex-shrink-0">
<span
id="bar1"
className="w-1 mr-[3px] h-2 bg-gray-300 dark:bg-green-950"
/>
<span
id="bar2"
className="w-1 mr-[3px] h-1 bg-gray-300 dark:bg-green-950"
/>
<span
id="bar3"
className="w-1 h-3 bg-gray-300 dark:bg-green-950"
/>
</div>
)
}
2023-02-22 22:40:36 +00:00
type PlayerStateResponse = {
data: Song
error: string
isLoading: boolean
}
2023-01-18 17:02:44 +00:00
function usePlayerState(path: string) {
2024-09-27 20:02:51 +00:00
const {mutate} = useSWRConfig()
2024-08-17 22:21:51 +00:00
const {data, error, isLoading} = useSWR(`/api/spotify/${path}`, fetcher) as PlayerStateResponse
2022-12-06 12:54:34 +00:00
2024-09-27 20:02:51 +00:00
mutate(`/api/spotify/${path}`, data).then(r => r)
2022-12-06 12:54:34 +00:00
return {
song: data,
isLoading,
isError: error
}
}
2023-01-14 19:31:05 +00:00
function Song({as: Component = 'div', artist, title, songUrl, album, albumImageUrl, isPlaying}: Song) {
2022-12-06 12:54:34 +00:00
return (
2023-01-06 23:16:19 +00:00
<Component
className="flex items-center space-x-4">
2022-12-06 12:54:34 +00:00
{isPlaying &&
<AnimatedBars/>
}
2023-01-06 23:16:19 +00:00
<Song.Album
album={album}
albumImageUrl={albumImageUrl}
2022-12-06 12:54:34 +00:00
/>
<div>
2023-01-06 23:16:19 +00:00
<Song.Title
title={title}
songUrl={songUrl}
className={isPlaying
? 'dark:text-green-950'
: 'dark:text-zinc-100'}
/>
<Song.Artist artist={artist}/>
2022-12-06 12:54:34 +00:00
</div>
2023-01-06 23:16:19 +00:00
</Component>
)
}
2023-01-14 19:31:05 +00:00
Song.Album = function SongAlbum({album, albumImageUrl}: Album) {
2023-01-06 23:16:19 +00:00
return (
<Image
width="64"
height="64"
alt={album}
src={albumImageUrl}
className="aspect-square rounded-2xl object-cover"
/>
)
}
2023-01-14 19:31:05 +00:00
Song.Title = function SongTitle({as: Component = 'h2', title, songUrl, className}: Title) {
2023-01-06 23:16:19 +00:00
return (
2023-02-22 22:52:51 +00:00
<Component className={clsx(className, 'text-sm font-semibold text-zinc-800 line-clamp-1 lg:line-clamp-none')}>
2023-01-06 23:16:19 +00:00
<Link href={songUrl}>
{title}
</Link>
</Component>
2022-12-06 12:54:34 +00:00
)
}
2023-01-14 19:31:05 +00:00
Song.Artist = function SongArtist({as: Component = 'p', artist}: Artist) {
2022-12-06 12:54:34 +00:00
return (
2023-01-06 23:16:19 +00:00
<Component className="text-sm text-zinc-600 dark:text-zinc-400 line-clamp-1 lg:line-clamp-none">
{artist}
</Component>
)
}
2023-01-14 19:31:05 +00:00
Song.Skeleton = function SongSkeleton() {
2023-01-06 23:16:19 +00:00
return (
2023-01-14 19:31:05 +00:00
<div className="flex items-center space-x-4 animate-pulse">
2022-12-06 12:54:34 +00:00
<div
className="w-[64px] h-[64px] bg-zinc-100 rounded-2xl dark:bg-zinc-900"
/>
<div>
<p className="w-[128px] h-3 bg-zinc-100 rounded-2xl dark:bg-zinc-900"/>
<p className="mt-3 w-[128px] h-3 bg-zinc-100 rounded-2xl dark:bg-zinc-900"/>
</div>
2023-01-14 19:31:05 +00:00
</div>
2022-12-06 12:54:34 +00:00
)
}
2023-01-14 19:31:05 +00:00
export function SpotifyPlayer(): ReactElement | null {
2023-01-18 17:02:44 +00:00
const currentlyPlaying = usePlayerState('currently-playing')
const lastPlayed = usePlayerState('last-played')
2022-12-06 12:54:34 +00:00
2023-01-18 20:24:41 +00:00
if (currentlyPlaying.isError || lastPlayed.isError) return null
2022-12-06 12:54:34 +00:00
return (
2023-01-14 19:31:05 +00:00
<div className="grid">
2023-01-18 17:02:44 +00:00
{currentlyPlaying.isLoading
2023-01-06 23:16:19 +00:00
? <Song.Skeleton/>
2023-01-18 17:02:44 +00:00
: currentlyPlaying.song?.isPlaying
? <Song {...currentlyPlaying.song}/>
: lastPlayed.isLoading
? <Song.Skeleton/>
: <Song {...lastPlayed.song}/>
2022-12-06 12:54:34 +00:00
}
</div>
)
2023-09-11 18:42:50 +00:00
}