feat/clean: styling and feature enhancements

The commit introduces several changes to improve the user interface and experience of the Audio Player. Major updates were made in the styling of audioPlayer.css, audioPlayer.tsx and src/app/playlist/[playlistId]/page.tsx. The UX was enhanced by adding a function to automatically synchronize scrolling with the song being played in the playlist. Also, a new feature has been added to make the track name a clickable link redirecting users to the song on Spotify. The Spotify logo was added in SVG format for better quality.
This commit is contained in:
Florian Sylvain
2023-11-16 16:45:27 +01:00
parent 28cd26b05c
commit 8fe9394b6b
4 changed files with 96 additions and 49 deletions
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" height="168px" width="559px" version="1.1" viewBox="0 0 559 168">
<path fill="#1ED760" d="m83.996 0.277c-46.249 0-83.743 37.493-83.743 83.742 0 46.251 37.494 83.741 83.743 83.741 46.254 0 83.744-37.49 83.744-83.741 0-46.246-37.49-83.738-83.745-83.738l0.001-0.004zm38.404 120.78c-1.5 2.46-4.72 3.24-7.18 1.73-19.662-12.01-44.414-14.73-73.564-8.07-2.809 0.64-5.609-1.12-6.249-3.93-0.643-2.81 1.11-5.61 3.926-6.25 31.9-7.288 59.263-4.15 81.337 9.34 2.46 1.51 3.24 4.72 1.73 7.18zm10.25-22.802c-1.89 3.072-5.91 4.042-8.98 2.152-22.51-13.836-56.823-17.843-83.448-9.761-3.453 1.043-7.1-0.903-8.148-4.35-1.04-3.453 0.907-7.093 4.354-8.143 30.413-9.228 68.222-4.758 94.072 11.127 3.07 1.89 4.04 5.91 2.15 8.976v-0.001zm0.88-23.744c-26.99-16.031-71.52-17.505-97.289-9.684-4.138 1.255-8.514-1.081-9.768-5.219-1.254-4.14 1.08-8.513 5.221-9.771 29.581-8.98 78.756-7.245 109.83 11.202 3.73 2.209 4.95 7.016 2.74 10.733-2.2 3.722-7.02 4.949-10.73 2.739zm94.56 3.072c-14.46-3.448-17.03-5.868-17.03-10.953 0-4.804 4.52-8.037 11.25-8.037 6.52 0 12.98 2.455 19.76 7.509 0.2 0.153 0.46 0.214 0.71 0.174 0.26-0.038 0.48-0.177 0.63-0.386l7.06-9.952c0.29-0.41 0.21-0.975-0.18-1.288-8.07-6.473-17.15-9.62-27.77-9.62-15.61 0-26.52 9.369-26.52 22.774 0 14.375 9.41 19.465 25.67 23.394 13.83 3.187 16.17 5.857 16.17 10.629 0 5.29-4.72 8.58-12.32 8.58-8.44 0-15.33-2.85-23.03-9.51-0.19-0.17-0.45-0.24-0.69-0.23-0.26 0.02-0.49 0.14-0.65 0.33l-7.92 9.42c-0.33 0.4-0.29 0.98 0.09 1.32 8.96 8 19.98 12.22 31.88 12.22 16.82 0 27.69-9.19 27.69-23.42 0.03-12.007-7.16-18.657-24.77-22.941l-0.03-0.013zm62.86-14.26c-7.29 0-13.27 2.872-18.21 8.757v-6.624c0-0.523-0.42-0.949-0.94-0.949h-12.95c-0.52 0-0.94 0.426-0.94 0.949v73.601c0 0.52 0.42 0.95 0.94 0.95h12.95c0.52 0 0.94-0.43 0.94-0.95v-23.23c4.94 5.53 10.92 8.24 18.21 8.24 13.55 0 27.27-10.43 27.27-30.369 0.02-19.943-13.7-30.376-27.26-30.376l-0.01 0.001zm12.21 30.375c0 10.149-6.25 17.239-15.21 17.239-8.85 0-15.53-7.41-15.53-17.239 0-9.83 6.68-17.238 15.53-17.238 8.81-0.001 15.21 7.247 15.21 17.237v0.001zm50.21-30.375c-17.45 0-31.12 13.436-31.12 30.592 0 16.972 13.58 30.262 30.91 30.262 17.51 0 31.22-13.39 31.22-30.479 0-17.031-13.62-30.373-31.01-30.373v-0.002zm0 47.714c-9.28 0-16.28-7.46-16.28-17.344 0-9.929 6.76-17.134 16.07-17.134 9.34 0 16.38 7.457 16.38 17.351 0 9.927-6.8 17.127-16.17 17.127zm68.27-46.53h-14.25v-14.566c0-0.522-0.42-0.948-0.94-0.948h-12.95c-0.52 0-0.95 0.426-0.95 0.948v14.566h-6.22c-0.52 0-0.94 0.426-0.94 0.949v11.127c0 0.522 0.42 0.949 0.94 0.949h6.22v28.795c0 11.63 5.79 17.53 17.22 17.53 4.64 0 8.49-0.96 12.12-3.02 0.3-0.16 0.48-0.48 0.48-0.82v-10.6c0-0.32-0.17-0.63-0.45-0.8-0.28-0.18-0.63-0.19-0.92-0.04-2.49 1.25-4.9 1.83-7.6 1.83-4.15 0-6.01-1.89-6.01-6.11v-26.76h14.25c0.52 0 0.94-0.426 0.94-0.949v-11.126c0.02-0.523-0.4-0.949-0.93-0.949l-0.01-0.006zm49.64 0.057v-1.789c0-5.263 2.02-7.61 6.54-7.61 2.7 0 4.87 0.536 7.3 1.346 0.3 0.094 0.61 0.047 0.85-0.132 0.25-0.179 0.39-0.466 0.39-0.77v-10.91c0-0.417-0.26-0.786-0.67-0.909-2.56-0.763-5.84-1.546-10.76-1.546-11.95 0-18.28 6.734-18.28 19.467v2.74h-6.22c-0.52 0-0.95 0.426-0.95 0.948v11.184c0 0.522 0.43 0.949 0.95 0.949h6.22v44.405c0 0.53 0.43 0.95 0.95 0.95h12.94c0.53 0 0.95-0.42 0.95-0.95v-44.402h12.09l18.52 44.402c-2.1 4.66-4.17 5.59-6.99 5.59-2.28 0-4.69-0.68-7.14-2.03-0.23-0.12-0.51-0.14-0.75-0.07-0.25 0.09-0.46 0.27-0.56 0.51l-4.39 9.63c-0.21 0.46-0.03 0.99 0.41 1.23 4.58 2.48 8.71 3.54 13.82 3.54 9.56 0 14.85-4.46 19.5-16.44l22.46-58.037c0.12-0.292 0.08-0.622-0.1-0.881-0.17-0.257-0.46-0.412-0.77-0.412h-13.48c-0.41 0-0.77 0.257-0.9 0.636l-13.81 39.434-15.12-39.46c-0.14-0.367-0.49-0.61-0.88-0.61h-22.12v-0.003zm-28.78-0.057h-12.95c-0.52 0-0.95 0.426-0.95 0.949v56.481c0 0.53 0.43 0.95 0.95 0.95h12.95c0.52 0 0.95-0.42 0.95-0.95v-56.477c0-0.523-0.42-0.949-0.95-0.949v-0.004zm-6.4-25.719c-5.13 0-9.29 4.152-9.29 9.281 0 5.132 4.16 9.289 9.29 9.289s9.28-4.157 9.28-9.289c0-5.128-4.16-9.281-9.28-9.281zm113.42 43.88c-5.12 0-9.11-4.115-9.11-9.112s4.04-9.159 9.16-9.159 9.11 4.114 9.11 9.107c0 4.997-4.04 9.164-9.16 9.164zm0.05-17.365c-4.67 0-8.2 3.71-8.2 8.253 0 4.541 3.51 8.201 8.15 8.201 4.67 0 8.2-3.707 8.2-8.253 0-4.541-3.51-8.201-8.15-8.201zm2.02 9.138l2.58 3.608h-2.18l-2.32-3.31h-1.99v3.31h-1.82v-9.564h4.26c2.23 0 3.69 1.137 3.69 3.051 0.01 1.568-0.9 2.526-2.21 2.905h-0.01zm-1.54-4.315h-2.37v3.025h2.37c1.18 0 1.89-0.579 1.89-1.514 0-0.984-0.71-1.511-1.89-1.511z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+50 -20
View File
@@ -8,27 +8,40 @@ import {getTimeLength} from "@/app/utils/timeManipulator";
const AudioPlayerSkeleton = () => {
return (
<>
<div className="flex">
<div className="w-16 bg-gray-300 h-16 animate-pulse"></div>
<div className="flex flex-col flex-grow justify-center px-2 gap-1">
<h2 className="bg-gray-300 h-6 w-2/3 animate-pulse"></h2>
<div className="flex gap-1 items-center">
<button className="bg-gray-300 h-8 w-8 rounded-full animate-pulse"></button>
<button className="bg-gray-300 h-8 w-8 rounded-full animate-pulse"></button>
<button className="bg-gray-300 h-8 w-8 rounded-full animate-pulse"></button>
</div>
<div className="flex gap-2">
<div className="h-32 w-32 min-w-max animate-pulse bg-gray-300"></div>
<div className="flex flex-grow flex-col">
<div className="flex flex-grow flex-col justify-center gap-1 px-2">
<h2 className="h-6 w-2/3 animate-pulse bg-gray-300"></h2>
<div className="flex items-center gap-1">
<button className="h-8 w-8 animate-pulse rounded-full bg-gray-300"></button>
<button className="h-8 w-8 animate-pulse rounded-full bg-gray-300"></button>
<button className="h-8 w-8 animate-pulse rounded-full bg-gray-300"></button>
</div>
</div>
<div className="flex items-center gap-2">
<div className="bg-gray-300 h-2 w-96 animate-pulse"></div>
<label className="bg-gray-300 h-6 w-10 animate-pulse"></label>
<div className="h-2 w-96 animate-pulse bg-gray-300"></div>
<label className="h-6 w-10 animate-pulse bg-gray-300"></label>
</div>
</div>
</div>
</>
)
}
const PlaylistSkeleton = () => {
return (
<div className="flex h-52 flex-col gap-1 overflow-scroll">
{[...Array(10)].map((_, index) => (
<div key={'skeleton playlist' + index + 1} className="rounded-lg bg-neutral-500">
<button className="flex w-full items-center gap-6 p-3">
<div className="text-neutral-400">{index + 1}</div>
<div className="h-6 w-2/3 animate-pulse bg-gray-300 text-left"></div>
<div className="ml-auto h-6 w-10 animate-pulse bg-gray-300 text-neutral-400"></div>
</button>
</div>))}
</div>
)
}
export default function Page(): React.JSX.Element {
const params = useParams()
@@ -39,6 +52,8 @@ export default function Page(): React.JSX.Element {
const [currentTrackIndex, setCurrentTrackIndex] = useState(0)
const [currentTrack, setCurrentTrack] = useState<SpotifyApi.TrackObjectFull | undefined>()
const playlistDom = useRef<HTMLDivElement | null>(null)
useEffect(() => {
fetch(`/api/playlist?id=${playlistId}`)
.then(response => response.json())
@@ -59,25 +74,40 @@ export default function Page(): React.JSX.Element {
setCurrentTrackIndex(localIndex)
setCurrentTrack(tracks?.at(localIndex) ?? undefined)
playlistDom.current?.scrollTo({
top: localIndex * 52,
behavior: 'smooth'
})
}
return (
<main className="flex flex-col w-screen h-screen justify-center items-center content-center bg-gray-800 text-white fill-white">
<div className="w-full max-w-sm">
<main
className="flex h-screen w-screen flex-col content-center items-center justify-center bg-neutral-800 fill-white text-white">
<div className="flex w-full max-w-xl flex-col overflow-clip rounded-xl bg-neutral-500 shadow-xl">
<div className="p-6">
{currentTrack ?
<AudioPlayer track={currentTrack}
onNext={() => changeTrack('next')}
onPrev={() => changeTrack('prev')}></AudioPlayer> :
<AudioPlayerSkeleton></AudioPlayerSkeleton>}
<div>
</div>
<div className="bg-neutral-600 p-3">
{tracks ? (<div className="flex h-52 flex-col gap-1 overflow-scroll" ref={playlistDom}>
{tracks?.map((track: any, index: number) => (
<div key={track.uri}>
<button onClick={() => {
<div key={track.uri}
className={"rounded-lg hover:bg-neutral-500 " + (currentTrack?.uri === track.uri && 'bg-neutral-700')}>
<button className="flex w-full items-center gap-6 p-3" onClick={() => {
setCurrentTrackIndex(index)
setCurrentTrack(tracks?.at(index) ?? undefined)
}}>{index + 1} {track.name} {getTimeLength(track.duration_ms)}</button>
}}>
<div className="text-neutral-400">{index + 1}</div>
<div className="text-left">{track.name}</div>
<div className="ml-auto text-neutral-400">{getTimeLength(track.duration_ms)}</div>
</button>
</div>
))}
</div>) : <PlaylistSkeleton></PlaylistSkeleton>}
</div>
</div>
</main>
-2
View File
@@ -2,7 +2,6 @@ input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 8px;
width: 8px;
background: #ffffff;
cursor: pointer;
}
@@ -17,7 +16,6 @@ input[type=range]::-ms-thumb {
height: 36px;
width: 16px;
border-radius: 8px;
background: #ffffff;
cursor: pointer;
}
+27 -12
View File
@@ -6,7 +6,11 @@ import Image from "next/image";
import './audioPlayer.css'
import {getTimeLength} from "@/app/utils/timeManipulator";
export default function AudioPlayer(props: { track?: SpotifyApi.TrackObjectFull, onNext: () => void, onPrev: () => void }): React.JSX.Element {
export default function AudioPlayer(props: {
track?: SpotifyApi.TrackObjectFull,
onNext: () => void,
onPrev: () => void
}): React.JSX.Element {
const audio = useRef<HTMLAudioElement | null>(null)
const rangeInput = useRef<HTMLInputElement | null>(null)
@@ -56,21 +60,32 @@ export default function AudioPlayer(props: { track?: SpotifyApi.TrackObjectFull,
}, [props.track]);
return (
<div>
<div className="flex flex-col gap-2">
<audio ref={audio} className="hidden">
<source src={props.track?.preview_url ?? undefined} type="audio/mpeg"/>
Your browser does not support the audio element.
</audio>
<div className="flex">
<div className="w-16">
<Image src={props.track?.album.images[0].url as string} width={64} height={64}
alt="album cover"/>
<div className="flex gap-2">
<div className="w-32 min-w-max">
<Image src={props.track?.album.images[1].url as string} width={128} height={128}
alt="album cover"
className="rounded-lg shadow-lg"/>
</div>
<div className="flex flex-col flex-grow justify-center px-2 gap-1">
<h2>{props.track?.name} {props.track?.preview_url == null ?
<div className="flex flex-grow flex-col justify-center gap-1 px-2">
<a className="cursor-pointer" href={props.track?.external_urls.spotify} target="_blank">
<Image alt="spotify track redirect" src="/Spotify_logo_with_text.svg"
className="ml-auto w-10" width={40} height={12}/>
</a>
<h2 className="text-2xl">{props.track?.name} {props.track?.preview_url == null ?
<b>(no preview available)</b> : undefined}</h2>
<div className="flex gap-1 items-center">
<h3>
{props.track?.artists.map((artist, index) => {
return <span
key={artist.id}>{artist.name}{props.track?.artists[index + 1] !== undefined ? ', ' : undefined}</span>
})}
</h3>
<div className="flex items-center gap-1">
<button onClick={props.onPrev}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path
@@ -96,15 +111,15 @@ export default function AudioPlayer(props: { track?: SpotifyApi.TrackObjectFull,
</svg>
</button>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<input id="default-range" type="range" min={0} max={30000} value={progress}
aria-valuenow={progress} onChange={onRangeInputChange}
className="appearance-none w-full h-2"
className="h-2 w-full appearance-none rounded-lg"
ref={rangeInput}/>
<label htmlFor='default-range'>{getTimeLength(progress ?? 0)}</label>
</div>
</div>
</div>
</div>
)
}