mirror of
https://github.com/Floriansylvain/spotify-playlist-embed.git
synced 2026-08-19 11:43:17 +02:00
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:
@@ -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 |
@@ -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 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="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 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>
|
||||
</>
|
||||
</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">
|
||||
{currentTrack ?
|
||||
<AudioPlayer track={currentTrack}
|
||||
onNext={() => changeTrack('next')}
|
||||
onPrev={() => changeTrack('prev')}></AudioPlayer> :
|
||||
<AudioPlayerSkeleton></AudioPlayerSkeleton>}
|
||||
<div>
|
||||
{tracks?.map((track: any, index: number) => (
|
||||
<div key={track.uri}>
|
||||
<button onClick={() => {
|
||||
setCurrentTrackIndex(index)
|
||||
setCurrentTrack(tracks?.at(index) ?? undefined)
|
||||
}}>{index + 1} {track.name} {getTimeLength(track.duration_ms)}</button>
|
||||
</div>
|
||||
))}
|
||||
<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 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}
|
||||
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)
|
||||
}}>
|
||||
<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,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 className="flex items-center gap-2">
|
||||
<input id="default-range" type="range" min={0} max={30000} value={progress}
|
||||
aria-valuenow={progress} onChange={onRangeInputChange}
|
||||
className="h-2 w-full appearance-none rounded-lg"
|
||||
ref={rangeInput}/>
|
||||
<label htmlFor='default-range'>{getTimeLength(progress ?? 0)}</label>
|
||||
</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"
|
||||
ref={rangeInput}/>
|
||||
<label htmlFor='default-range'>{getTimeLength(progress ?? 0)}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user