From 2211765bf332b8fe7821153d54581d2fc47ba7f3 Mon Sep 17 00:00:00 2001 From: Pratheek Nathani Date: Tue, 17 Feb 2026 14:37:11 -0500 Subject: [PATCH] Add persistent now-playing bar to dashboard (#221) Adds a compact mini-player bar fixed to the bottom of the dashboard that shows the currently playing track, artist, playback controls, and a progress indicator. The bar hides on routes that already render a full AudioPlayer component. --- src/apps/admin/layout/Dashboard.scss | 3 +- src/apps/admin/layout/Dashboard.tsx | 2 + src/components/index.ts | 1 + .../now-playing-bar/NowPlayingBar.scss | 87 +++++++++++++++++++ .../now-playing-bar/NowPlayingBar.tsx | 59 +++++++++++++ src/components/now-playing-bar/index.ts | 1 + 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 src/components/now-playing-bar/NowPlayingBar.scss create mode 100644 src/components/now-playing-bar/NowPlayingBar.tsx create mode 100644 src/components/now-playing-bar/index.ts diff --git a/src/apps/admin/layout/Dashboard.scss b/src/apps/admin/layout/Dashboard.scss index 8d9b709..3a11a29 100644 --- a/src/apps/admin/layout/Dashboard.scss +++ b/src/apps/admin/layout/Dashboard.scss @@ -17,8 +17,9 @@ &__outlet { padding: var(--dashboard-gutter); + padding-bottom: 5rem; height: 100%; - } + } } } diff --git a/src/apps/admin/layout/Dashboard.tsx b/src/apps/admin/layout/Dashboard.tsx index 305f69a..37a9bda 100644 --- a/src/apps/admin/layout/Dashboard.tsx +++ b/src/apps/admin/layout/Dashboard.tsx @@ -2,6 +2,7 @@ import { createContext, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { Outlet } from 'react-router-dom' import { selectCurrentJukebox, selectCurrentMembership } from 'src/store' +import { NowPlayingBar } from 'src/components' import { Sidebar, Topbar } from '../components' import './Dashboard.scss' @@ -52,6 +53,7 @@ export const Dashboard = () => { + ) diff --git a/src/components/index.ts b/src/components/index.ts index c778cfe..061d55e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,5 +1,6 @@ export * from './audio-player' export * from './form' +export * from './now-playing-bar' export * from './overlays' export * from './track-list/TrackItem' export * from './track-list/TrackList' diff --git a/src/components/now-playing-bar/NowPlayingBar.scss b/src/components/now-playing-bar/NowPlayingBar.scss new file mode 100644 index 0000000..c080c52 --- /dev/null +++ b/src/components/now-playing-bar/NowPlayingBar.scss @@ -0,0 +1,87 @@ +.now-playing-bar { + position: fixed; + bottom: 0; + left: var(--sidebar-width); + right: 0; + height: 4.5rem; + z-index: 50; + @include color-role('surface'); + border-top: $border-thin; + + &__progress-line { + position: absolute; + top: 0; + left: 0; + height: 3px; + background-color: var(--color-primary-contrast); + transition: width 1s linear; + } + + &__content { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; + padding: 0 1.5rem; + gap: 1rem; + } + + &__track { + display: flex; + flex-direction: column; + min-width: 0; + flex: 1; + + &__name { + @include font-label('md'); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__artists { + @include font-body('sm'); + color: var(--color-label); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__controls { + display: flex; + align-items: center; + gap: 0.25rem; + + &__btn { + background: none; + border: none; + padding: 0.5rem; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 100px; + color: var(--color-primary-contrast); + + svg { + width: 2rem; + height: 2rem; + } + } + } + + &__time { + @include font-body('sm'); + color: var(--color-label); + display: flex; + gap: 0.25rem; + white-space: nowrap; + } +} + +@include breakpoint('lg') { + .now-playing-bar { + left: 0; + } +} diff --git a/src/components/now-playing-bar/NowPlayingBar.tsx b/src/components/now-playing-bar/NowPlayingBar.tsx new file mode 100644 index 0000000..7aec190 --- /dev/null +++ b/src/components/now-playing-bar/NowPlayingBar.tsx @@ -0,0 +1,59 @@ +import { useContext } from 'react' +import { useLocation } from 'react-router-dom' +import NextIcon from 'src/assets/svg/NextIcon.svg?react' +import PauseIcon from 'src/assets/svg/PauseIcon.svg?react' +import PlayIcon from 'src/assets/svg/PlayIcon.svg?react' +import PreviousIcon from 'src/assets/svg/PreviousIcon.svg?react' +import { PlayerContext } from 'src/context' +import { formatDuration } from 'src/utils' +import './NowPlayingBar.scss' + +const ROUTES_WITH_PLAYER = ['/dashboard', '/dashboard/player', '/dashboard/debug'] + +export const NowPlayingBar = () => { + const { currentTrack, playerState, liveProgress, togglePlay, nextTrack, prevTrack } = + useContext(PlayerContext) + + const location = useLocation() + + const hideBar = ROUTES_WITH_PLAYER.includes(location.pathname) + + if (hideBar || !currentTrack?.name) return null + + const progress = liveProgress ?? 0 + const duration = currentTrack.duration_ms ?? 0 + const progressPercent = duration > 0 ? (progress / duration) * 100 : 0 + + return ( +
+
+
+
+ {currentTrack.name} + + {currentTrack.artists?.join(', ') || 'Unknown Artist'} + +
+
+ + + +
+
+ {formatDuration(progress)} + / + {formatDuration(duration)} +
+
+
+ ) +} diff --git a/src/components/now-playing-bar/index.ts b/src/components/now-playing-bar/index.ts new file mode 100644 index 0000000..8d26099 --- /dev/null +++ b/src/components/now-playing-bar/index.ts @@ -0,0 +1 @@ +export * from './NowPlayingBar'