Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-07-19 11:38:28 -05:00
parent e3c7879087
commit ba66205fd2
7 changed files with 98 additions and 47 deletions

View File

@ -12,6 +12,7 @@ interface InfiniteScrollListProps<T> {
emptyMessage: string; emptyMessage: string;
loadingTitle?: string; loadingTitle?: string;
loadingMessage?: string; loadingMessage?: string;
showItemCount?: boolean;
} }
const InfiniteScrollList = <T extends string | { key?: string }>({ const InfiniteScrollList = <T extends string | { key?: string }>({
@ -24,6 +25,7 @@ const InfiniteScrollList = <T extends string | { key?: string }>({
emptyMessage, emptyMessage,
loadingTitle = "Loading...", loadingTitle = "Loading...",
loadingMessage = "Please wait while data is being loaded", loadingMessage = "Please wait while data is being loaded",
showItemCount = true,
}: InfiniteScrollListProps<T>) => { }: InfiniteScrollListProps<T>) => {
const observerRef = useRef<HTMLDivElement>(null); const observerRef = useRef<HTMLDivElement>(null);
@ -114,7 +116,7 @@ const InfiniteScrollList = <T extends string | { key?: string }>({
</div> </div>
{/* Stats */} {/* Stats */}
{items.length > 0 && ( {items.length > 0 && showItemCount && (
<div style={{ marginTop: '16px', marginBottom: '20px' }} className="text-sm text-gray-500 text-center"> <div style={{ marginTop: '16px', marginBottom: '20px' }} className="text-sm text-gray-500 text-center">
Showing {items.length} item{items.length !== 1 ? 's' : ''} Showing {items.length} item{items.length !== 1 ? 's' : ''}
{hasMore && ` • Scroll down to load more`} {hasMore && ` • Scroll down to load more`}

View File

@ -13,7 +13,7 @@ import { useDisabledSongs } from '../../hooks/useDisabledSongs';
import { useSelectSinger } from '../../hooks/useSelectSinger'; import { useSelectSinger } from '../../hooks/useSelectSinger';
import { useToast } from '../../hooks/useToast'; import { useToast } from '../../hooks/useToast';
import SelectSinger from './SelectSinger'; import SelectSinger from './SelectSinger';
import SongItem from './SongItem'; import { SongInfoDisplay } from './SongItem';
import type { Song } from '../../types'; import type { Song } from '../../types';
interface SongInfoProps { interface SongInfoProps {
@ -96,17 +96,15 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
<IonContent> <IonContent>
<div className="p-4"> <div className="p-4">
{/* Song Information using SongItem component */} {/* Song Information using SongInfoDisplay component */}
<div className="mb-6"> <div className="mb-6">
<SongItem <div style={{ padding: '16px', marginBottom: '20px' }}>
song={song} <SongInfoDisplay
context="queue" // This context doesn't show any buttons song={song}
isAdmin={isAdmin} showPath={true}
showActions={false} showCount={false}
showPath={false} />
showCount={false} </div>
className="border-b border-gray-200 dark:border-gray-700"
/>
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}

View 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>
);
};

View File

@ -1,10 +1,9 @@
export { default as ActionButton } from './ActionButton'; export { default as ActionButton } from './ActionButton';
export { default as EmptyState } from './EmptyState'; export { default as EmptyState } from './EmptyState';
export { default as Toast } from './Toast';
export { default as ErrorBoundary } from './ErrorBoundary'; export { default as ErrorBoundary } from './ErrorBoundary';
export { default as InfiniteScrollList } from './InfiniteScrollList'; export { default as InfiniteScrollList } from './InfiniteScrollList';
export { default as PageHeader } from './PageHeader'; export { default as PageHeader } from './PageHeader';
export { default as SongItem } from './SongItem';
export { default as PlayerControls } from './PlayerControls'; export { default as PlayerControls } from './PlayerControls';
export { default as SelectSinger } from './SelectSinger'; export { default as SongItem, SongInfoDisplay, SongActionButtons } from './SongItem';
export { default as SongInfo } from './SongInfo'; export { default as Toast } from './Toast';
export { TwoLineDisplay } from './TwoLineDisplay';

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react'; 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 { close, list } from 'ionicons/icons';
import { InfiniteScrollList, SongItem } from '../../components/common'; import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
import { useArtists } from '../../hooks'; import { useArtists } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectSongs } from '../../redux'; import { selectSongs } from '../../redux';
@ -42,15 +42,11 @@ const Artists: React.FC = () => {
// Render artist item for InfiniteScrollList // Render artist item for InfiniteScrollList
const renderArtistItem = (artist: string) => ( const renderArtistItem = (artist: string) => (
<IonItem button onClick={() => handleArtistClick(artist)} detail={false}> <IonItem button onClick={() => handleArtistClick(artist)} detail={false} style={{ '--min-height': '60px' }}>
<IonLabel> <TwoLineDisplay
<h3 className="text-sm font-medium text-gray-900"> primaryText={artist}
{artist} secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
</h3> />
<p className="text-sm text-gray-500">
{getSongCountByArtist(artist)} song{getSongCountByArtist(artist) !== 1 ? 's' : ''}
</p>
</IonLabel>
<IonIcon icon={list} slot="end" color="primary" /> <IonIcon icon={list} slot="end" color="primary" />
</IonItem> </IonItem>
); );

View File

@ -44,17 +44,25 @@ const Singers: React.FC = () => {
debugLog('Singers component - singers:', singers); debugLog('Singers component - singers:', singers);
// Render singer item for InfiniteScrollList // Render singer item for InfiniteScrollList
const renderSingerItem = (singer: Singer) => ( const renderSingerItem = (singer: Singer, index: number) => (
<IonItem detail={false}> <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> <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} {singer.name}
</h3> </div>
</IonLabel> </IonLabel>
{/* Delete Button (Admin Only) */} {/* Delete Button (Admin Only) */}
{isAdmin && ( {isAdmin && (
<div slot="end" className="flex items-center gap-2 ml-2"> <div slot="end" style={{ marginRight: '-16px' }}>
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<ActionButton <ActionButton
onClick={() => handleRemoveSinger(singer)} onClick={() => handleRemoveSinger(singer)}
@ -71,19 +79,18 @@ const Singers: React.FC = () => {
return ( return (
<> <>
<div className="flex justify-end items-center mb-4 pr-4 right-button-container"> <div className="ion-padding ion-text-end">
{isAdmin && ( {isAdmin && (
<IonButton <IonButton
fill="clear" fill="clear"
onClick={handleOpenAddModal} onClick={handleOpenAddModal}
className="text-primary"
> >
<IonIcon icon={add} slot="icon-only" size="large" /> <IonIcon icon={add} slot="icon-only" size="large" />
</IonButton> </IonButton>
)} )}
</div> </div>
<div className="max-w-4xl mx-auto p-6"> <div className="ion-padding">
<InfiniteScrollList<Singer> <InfiniteScrollList<Singer>
items={singers} items={singers}
isLoading={singersCount === 0} isLoading={singersCount === 0}
@ -94,6 +101,7 @@ const Singers: React.FC = () => {
emptyMessage="Singers will appear here when they join the party" emptyMessage="Singers will appear here when they join the party"
loadingTitle="Loading singers..." loadingTitle="Loading singers..."
loadingMessage="Please wait while singers data is being loaded" loadingMessage="Please wait while singers data is being loaded"
showItemCount={false}
/> />
</div> </div>

View File

@ -1,10 +1,10 @@
import React, { useState, useMemo, useCallback } from 'react'; 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 { close, list } from 'ionicons/icons';
import { useTopPlayed } from '../../hooks'; import { useTopPlayed } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectTopPlayed, selectSongsArray } 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 { filterSongs } from '../../utils/dataProcessing';
import { debugLog } from '../../utils/logger'; import { debugLog } from '../../utils/logger';
import { useSongOperations } from '../../hooks'; import { useSongOperations } from '../../hooks';
@ -108,20 +108,19 @@ const Top100: React.FC = () => {
button button
onClick={() => handleTopPlayedClick(item)} onClick={() => handleTopPlayedClick(item)}
detail={false} detail={false}
style={{ '--min-height': '60px' }}
> >
{/* Number */} {/* Number */}
<div slot="start" className="flex-shrink-0 w-12 h-12 flex items-center justify-center text-gray-600 font-medium"> <div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
{index + 1}) <div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
{index + 1}
</div>
</div> </div>
<IonLabel> <TwoLineDisplay
<h3 className="text-sm font-medium text-gray-900"> primaryText={item.title}
{item.title} secondaryText={item.artist}
</h3> />
<p className="text-sm text-gray-500">
{item.artist}
</p>
</IonLabel>
<IonChip slot="end" color="primary"> <IonChip slot="end" color="primary">
{item.count} plays {item.count} plays