Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
e3c7879087
commit
ba66205fd2
@ -12,6 +12,7 @@ interface InfiniteScrollListProps<T> {
|
||||
emptyMessage: string;
|
||||
loadingTitle?: string;
|
||||
loadingMessage?: string;
|
||||
showItemCount?: boolean;
|
||||
}
|
||||
|
||||
const InfiniteScrollList = <T extends string | { key?: string }>({
|
||||
@ -24,6 +25,7 @@ const InfiniteScrollList = <T extends string | { key?: string }>({
|
||||
emptyMessage,
|
||||
loadingTitle = "Loading...",
|
||||
loadingMessage = "Please wait while data is being loaded",
|
||||
showItemCount = true,
|
||||
}: InfiniteScrollListProps<T>) => {
|
||||
const observerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -114,7 +116,7 @@ const InfiniteScrollList = <T extends string | { key?: string }>({
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{items.length > 0 && (
|
||||
{items.length > 0 && showItemCount && (
|
||||
<div style={{ marginTop: '16px', marginBottom: '20px' }} className="text-sm text-gray-500 text-center">
|
||||
Showing {items.length} item{items.length !== 1 ? 's' : ''}
|
||||
{hasMore && ` • Scroll down to load more`}
|
||||
|
||||
@ -13,7 +13,7 @@ import { useDisabledSongs } from '../../hooks/useDisabledSongs';
|
||||
import { useSelectSinger } from '../../hooks/useSelectSinger';
|
||||
import { useToast } from '../../hooks/useToast';
|
||||
import SelectSinger from './SelectSinger';
|
||||
import SongItem from './SongItem';
|
||||
import { SongInfoDisplay } from './SongItem';
|
||||
import type { Song } from '../../types';
|
||||
|
||||
interface SongInfoProps {
|
||||
@ -96,17 +96,15 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
||||
|
||||
<IonContent>
|
||||
<div className="p-4">
|
||||
{/* Song Information using SongItem component */}
|
||||
{/* Song Information using SongInfoDisplay component */}
|
||||
<div className="mb-6">
|
||||
<SongItem
|
||||
song={song}
|
||||
context="queue" // This context doesn't show any buttons
|
||||
isAdmin={isAdmin}
|
||||
showActions={false}
|
||||
showPath={false}
|
||||
showCount={false}
|
||||
className="border-b border-gray-200 dark:border-gray-700"
|
||||
/>
|
||||
<div style={{ padding: '16px', marginBottom: '20px' }}>
|
||||
<SongInfoDisplay
|
||||
song={song}
|
||||
showPath={true}
|
||||
showCount={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
|
||||
49
src/components/common/TwoLineDisplay.tsx
Normal file
49
src/components/common/TwoLineDisplay.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { IonLabel } from '@ionic/react';
|
||||
|
||||
interface TwoLineDisplayProps {
|
||||
primaryText: string;
|
||||
secondaryText: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
primarySize?: string;
|
||||
secondarySize?: string;
|
||||
}
|
||||
|
||||
export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
|
||||
primaryText,
|
||||
secondaryText,
|
||||
primaryColor = 'black',
|
||||
secondaryColor = '#6b7280',
|
||||
primarySize = '1rem',
|
||||
secondarySize = '0.875rem'
|
||||
}) => {
|
||||
return (
|
||||
<IonLabel>
|
||||
{/* Primary Text - styled like song title */}
|
||||
<div
|
||||
className="ion-text-bold"
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: primarySize,
|
||||
color: primaryColor,
|
||||
lineHeight: '1.5'
|
||||
}}
|
||||
>
|
||||
{primaryText}
|
||||
</div>
|
||||
{/* Secondary Text - styled like artist name */}
|
||||
<div
|
||||
className="ion-text-italic ion-color-medium"
|
||||
style={{
|
||||
fontSize: secondarySize,
|
||||
fontStyle: 'italic',
|
||||
color: secondaryColor,
|
||||
lineHeight: '1.5'
|
||||
}}
|
||||
>
|
||||
{secondaryText}
|
||||
</div>
|
||||
</IonLabel>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,9 @@
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as EmptyState } from './EmptyState';
|
||||
export { default as Toast } from './Toast';
|
||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
||||
export { default as InfiniteScrollList } from './InfiniteScrollList';
|
||||
export { default as PageHeader } from './PageHeader';
|
||||
export { default as SongItem } from './SongItem';
|
||||
export { default as PlayerControls } from './PlayerControls';
|
||||
export { default as SelectSinger } from './SelectSinger';
|
||||
export { default as SongInfo } from './SongInfo';
|
||||
export { default as SongItem, SongInfoDisplay, SongActionButtons } from './SongItem';
|
||||
export { default as Toast } from './Toast';
|
||||
export { TwoLineDisplay } from './TwoLineDisplay';
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IonSearchbar, IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react';
|
||||
import { IonSearchbar, IonItem, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react';
|
||||
import { close, list } from 'ionicons/icons';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
|
||||
import { useArtists } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectSongs } from '../../redux';
|
||||
@ -42,15 +42,11 @@ const Artists: React.FC = () => {
|
||||
|
||||
// Render artist item for InfiniteScrollList
|
||||
const renderArtistItem = (artist: string) => (
|
||||
<IonItem button onClick={() => handleArtistClick(artist)} detail={false}>
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
{artist}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</IonLabel>
|
||||
<IonItem button onClick={() => handleArtistClick(artist)} detail={false} style={{ '--min-height': '60px' }}>
|
||||
<TwoLineDisplay
|
||||
primaryText={artist}
|
||||
secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
|
||||
/>
|
||||
<IonIcon icon={list} slot="end" color="primary" />
|
||||
</IonItem>
|
||||
);
|
||||
|
||||
@ -44,17 +44,25 @@ const Singers: React.FC = () => {
|
||||
debugLog('Singers component - singers:', singers);
|
||||
|
||||
// Render singer item for InfiniteScrollList
|
||||
const renderSingerItem = (singer: Singer) => (
|
||||
<IonItem detail={false}>
|
||||
const renderSingerItem = (singer: Singer, index: number) => (
|
||||
<IonItem detail={false} style={{ '--padding-start': '0px', '--min-height': '60px' }}>
|
||||
{/* Order Number */}
|
||||
<div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
|
||||
<div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Singer Name */}
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
<div className="ion-text-bold ion-color-primary" style={{ lineHeight: '1.5', fontSize: '1rem' }}>
|
||||
{singer.name}
|
||||
</h3>
|
||||
</div>
|
||||
</IonLabel>
|
||||
|
||||
{/* Delete Button (Admin Only) */}
|
||||
{isAdmin && (
|
||||
<div slot="end" className="flex items-center gap-2 ml-2">
|
||||
<div slot="end" style={{ marginRight: '-16px' }}>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionButton
|
||||
onClick={() => handleRemoveSinger(singer)}
|
||||
@ -71,19 +79,18 @@ const Singers: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-end items-center mb-4 pr-4 right-button-container">
|
||||
<div className="ion-padding ion-text-end">
|
||||
{isAdmin && (
|
||||
<IonButton
|
||||
fill="clear"
|
||||
onClick={handleOpenAddModal}
|
||||
className="text-primary"
|
||||
>
|
||||
<IonIcon icon={add} slot="icon-only" size="large" />
|
||||
</IonButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="ion-padding">
|
||||
<InfiniteScrollList<Singer>
|
||||
items={singers}
|
||||
isLoading={singersCount === 0}
|
||||
@ -94,6 +101,7 @@ const Singers: React.FC = () => {
|
||||
emptyMessage="Singers will appear here when they join the party"
|
||||
loadingTitle="Loading singers..."
|
||||
loadingMessage="Please wait while singers data is being loaded"
|
||||
showItemCount={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { IonItem, IonLabel, IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react';
|
||||
import { IonItem, IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react';
|
||||
import { close, list } from 'ionicons/icons';
|
||||
import { useTopPlayed } from '../../hooks';
|
||||
import { useAppSelector } from '../../redux';
|
||||
import { selectTopPlayed, selectSongsArray } from '../../redux';
|
||||
import { InfiniteScrollList, SongItem } from '../../components/common';
|
||||
import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
|
||||
import { filterSongs } from '../../utils/dataProcessing';
|
||||
import { debugLog } from '../../utils/logger';
|
||||
import { useSongOperations } from '../../hooks';
|
||||
@ -108,20 +108,19 @@ const Top100: React.FC = () => {
|
||||
button
|
||||
onClick={() => handleTopPlayedClick(item)}
|
||||
detail={false}
|
||||
style={{ '--min-height': '60px' }}
|
||||
>
|
||||
{/* Number */}
|
||||
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center text-gray-600 font-medium">
|
||||
{index + 1})
|
||||
<div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
|
||||
<div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
|
||||
{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IonLabel>
|
||||
<h3 className="text-sm font-medium text-gray-900">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{item.artist}
|
||||
</p>
|
||||
</IonLabel>
|
||||
<TwoLineDisplay
|
||||
primaryText={item.title}
|
||||
secondaryText={item.artist}
|
||||
/>
|
||||
|
||||
<IonChip slot="end" color="primary">
|
||||
{item.count} plays
|
||||
|
||||
Loading…
Reference in New Issue
Block a user