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 NowPlaying from './NowPlaying';
|
||||
import AddSongPanel from './AddSongPanel';
|
||||
import { TabView, Tab } from './TabView';
|
||||
import { API, getDisplayTitle, PlaylistItem } from '../api/player';
|
||||
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 [isPlaying, setIsPlaying] = useState(false);
|
||||
@@ -12,6 +25,7 @@ const App: React.FC = () => {
|
||||
const [volume, setVolume] = useState(100);
|
||||
const [volumeSettingIsLocked, setVolumeSettingIsLocked] = useState(false);
|
||||
const [songs, setSongs] = useState<PlaylistItem[]>([]);
|
||||
const [selectedTab, setSelectedTab] = useState<Tabs>(Tabs.Playlist);
|
||||
|
||||
const fetchPlaylist = useCallback(async () => {
|
||||
const playlist = await API.getPlaylist();
|
||||
@@ -122,6 +136,19 @@ const App: React.FC = () => {
|
||||
fetchNowPlaying();
|
||||
}, [fetchPlaylist, fetchNowPlaying]);
|
||||
|
||||
const playlistContent = (
|
||||
songs.length > 0 ? (
|
||||
<SongTable
|
||||
songs={songs}
|
||||
isPlaying={isPlaying}
|
||||
onDelete={handleDelete}
|
||||
onSkipTo={handleSkipTo}
|
||||
/>
|
||||
) : (
|
||||
<EmptyContent label="Playlist is empty" />
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -139,18 +166,14 @@ const App: React.FC = () => {
|
||||
onVolumeDidChange={() => setVolumeSettingIsLocked(false)}
|
||||
/>
|
||||
|
||||
{songs.length > 0 ? (
|
||||
<SongTable
|
||||
songs={songs}
|
||||
isPlaying={isPlaying}
|
||||
onDelete={handleDelete}
|
||||
onSkipTo={handleSkipTo}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-2xl font-bold">Playlist is empty</div>
|
||||
</div>
|
||||
)}
|
||||
<TabView selectedTab={selectedTab} onTabChange={setSelectedTab}>
|
||||
<Tab label="Playlist" identifier={Tabs.Playlist} icon={<FaMusic />}>
|
||||
{playlistContent}
|
||||
</Tab>
|
||||
<Tab label="Favorites" identifier={Tabs.Favorites} icon={<FaHeart />}>
|
||||
<EmptyContent label="Favorites are not implemented yet" />
|
||||
</Tab>
|
||||
</TabView>
|
||||
|
||||
<AddSongPanel onAddURL={handleAddURL} />
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ const SongRow: React.FC<SongRowProps> = ({ song, playState, onDelete, onPlay })
|
||||
|
||||
return (
|
||||
<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,
|
||||
})}>
|
||||
<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">
|
||||
<button
|
||||
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) => {
|
||||
e.stopPropagation();
|
||||
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
|
||||
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