frontend: add scaffolding for favorites tab
This commit is contained in:
@@ -2,8 +2,21 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
import SongTable from './SongTable';
|
import SongTable from './SongTable';
|
||||||
import NowPlaying from './NowPlaying';
|
import NowPlaying from './NowPlaying';
|
||||||
import AddSongPanel from './AddSongPanel';
|
import AddSongPanel from './AddSongPanel';
|
||||||
|
import { TabView, Tab } from './TabView';
|
||||||
import { API, getDisplayTitle, PlaylistItem } from '../api/player';
|
import { API, getDisplayTitle, PlaylistItem } from '../api/player';
|
||||||
import { useEventWebSocket } from '../hooks/useEventWebsocket';
|
import { useEventWebSocket } from '../hooks/useEventWebsocket';
|
||||||
|
import { FaMusic, FaHeart } from 'react-icons/fa';
|
||||||
|
|
||||||
|
enum Tabs {
|
||||||
|
Playlist = "playlist",
|
||||||
|
Favorites = "favorites",
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyContent: React.FC<{ label: string}> = ({label}) => (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-white text-2xl font-bold">{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -12,6 +25,7 @@ const App: React.FC = () => {
|
|||||||
const [volume, setVolume] = useState(100);
|
const [volume, setVolume] = useState(100);
|
||||||
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
|
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
|
||||||
const [songs, setSongs] = useState<PlaylistItem[]>([]);
|
const [songs, setSongs] = useState<PlaylistItem[]>([]);
|
||||||
|
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.Playlist);
|
||||||
|
|
||||||
const fetchPlaylist = useCallback(async () => {
|
const fetchPlaylist = useCallback(async () => {
|
||||||
const playlist = await API.getPlaylist();
|
const playlist = await API.getPlaylist();
|
||||||
@@ -122,6 +136,19 @@ const App: React.FC = () => {
|
|||||||
fetchNowPlaying();
|
fetchNowPlaying();
|
||||||
}, [fetchPlaylist, fetchNowPlaying]);
|
}, [fetchPlaylist, fetchNowPlaying]);
|
||||||
|
|
||||||
|
const playlistContent = (
|
||||||
|
songs.length > 0 ? (
|
||||||
|
<SongTable
|
||||||
|
songs={songs}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onSkipTo={handleSkipTo}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyContent label="Playlist is empty" />
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
|
<div className="flex items-center justify-center h-screen w-screen bg-black md:py-10">
|
||||||
<div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
|
<div className="bg-violet-900 w-full md:max-w-2xl h-full md:max-h-xl md:border md:rounded-2xl flex flex-col">
|
||||||
@@ -139,18 +166,14 @@ const App: React.FC = () => {
|
|||||||
onVolumeDidChange={() => setVolumeSettingIsLocked(false)}
|
onVolumeDidChange={() => setVolumeSettingIsLocked(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{songs.length > 0 ? (
|
<TabView selectedTab={selectedTab} onTabChange={setSelectedTab}>
|
||||||
<SongTable
|
<Tab label="Playlist" identifier={Tabs.Playlist} icon={<FaMusic />}>
|
||||||
songs={songs}
|
{playlistContent}
|
||||||
isPlaying={isPlaying}
|
</Tab>
|
||||||
onDelete={handleDelete}
|
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
|
||||||
onSkipTo={handleSkipTo}
|
<EmptyContent label="Favorites are not implemented yet" />
|
||||||
/>
|
</Tab>
|
||||||
) : (
|
</TabView>
|
||||||
<div className="flex items-center justify-center h-full">
|
|
||||||
<div className="text-white text-2xl font-bold">Playlist is empty</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AddSongPanel onAddURL={handleAddURL} />
|
<AddSongPanel onAddURL={handleAddURL} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-row w-full h-24 px-2 py-5 items-center border-b gap-2 transition-colors", {
|
<div className={classNames("flex flex-row w-full h-24 px-2 py-5 items-center border-b gap-2 transition-colors", {
|
||||||
"bg-black/10": (playState === PlayState.Playing || playState === PlayState.Paused),
|
"qc-highlighted": (playState === PlayState.Playing || playState === PlayState.Paused),
|
||||||
"bg-black/30": playState === PlayState.NotPlaying,
|
"bg-black/30": playState === PlayState.NotPlaying,
|
||||||
})}>
|
})}>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
@@ -78,7 +78,7 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className="text-red-500 px-3 py-1 bg-red-500/10 rounded"
|
className="text-red-100 px-3 py-1 bg-red-500/40 rounded"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (showDeleteConfirm) {
|
if (showDeleteConfirm) {
|
||||||
|
|||||||
64
frontend/src/components/TabView.tsx
Normal file
64
frontend/src/components/TabView.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface TabProps<T> {
|
||||||
|
label: string;
|
||||||
|
identifier: T;
|
||||||
|
icon?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tab = <T,>({ children }: TabProps<T>) => {
|
||||||
|
// Wrapper component
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TabViewProps<T> {
|
||||||
|
children: ReactNode;
|
||||||
|
selectedTab: T;
|
||||||
|
onTabChange: (tab: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabView = <T,>({ children, selectedTab, onTabChange }: TabViewProps<T>) => {
|
||||||
|
// Filter and validate children to only get Tab components
|
||||||
|
const tabs = React.Children.toArray(children).filter(
|
||||||
|
(child) => React.isValidElement(child) && child.type === Tab
|
||||||
|
) as React.ReactElement<TabProps<T>>[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex flex-row h-11 border-b border-white/20">
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
const isSelected = selectedTab === tab.props.identifier;
|
||||||
|
const rowClassName = classNames(
|
||||||
|
"flex flex-row items-center justify-center w-full gap-2 text-white",
|
||||||
|
{ "qc-highlighted": isSelected }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={rowClassName}
|
||||||
|
onClick={() => onTabChange(tab.props.identifier)}
|
||||||
|
>
|
||||||
|
{tab.props.icon}
|
||||||
|
<div className="text-sm font-bold">{tab.props.label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={classNames("w-full h-full", {
|
||||||
|
hidden: selectedTab !== tab.props.identifier,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{tab.props.children}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,4 +8,8 @@
|
|||||||
[&::-webkit-slider-thumb]:rounded-full
|
[&::-webkit-slider-thumb]:rounded-full
|
||||||
hover:[&::-webkit-slider-thumb]:bg-violet-300;
|
hover:[&::-webkit-slider-thumb]:bg-violet-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qc-highlighted {
|
||||||
|
@apply shadow-[inset_0_0_35px_rgba(147,51,234,0.8)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user