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;
|
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`}
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
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 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';
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user