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

This commit is contained in:
Matt Bruce 2025-07-18 10:41:40 -05:00
parent d2d9d9691b
commit 0b7c75d3ab
6 changed files with 207 additions and 9 deletions

View File

@ -3,15 +3,23 @@ import React from 'react';
interface PageHeaderProps {
title: string;
subtitle?: string;
action?: React.ReactNode;
}
const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle }) => {
const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle, action }) => {
return (
<div className="max-w-4xl mx-auto p-6">
<div style={{ marginBottom: '24px', paddingLeft: '16px' }}>
<h1 className="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
{subtitle && (
<p className="text-sm text-gray-600">{subtitle}</p>
<div style={{ marginBottom: '24px', paddingLeft: '16px' }} className="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">{title}</h1>
{subtitle && (
<p className="text-sm text-gray-600">{subtitle}</p>
)}
</div>
{action && (
<div className="flex-shrink-0">
{action}
</div>
)}
</div>
</div>

View File

@ -102,7 +102,7 @@ const SongItem: React.FC<SongItemProps> = ({
return (
<IonItem className={className}>
<IonLabel className="flex-1 min-w-0">
<h3 className="text-base font-extrabold text-gray-900 break-words">
<h3 className="text-base bold-title break-words">
{song.title}
</h3>
<p className="text-sm italic text-gray-500 break-words">

View File

@ -1,6 +1,6 @@
import React from 'react';
import { IonItem, IonLabel, IonIcon } from '@ionic/react';
import { trash } from 'ionicons/icons';
import React, { useState } from 'react';
import { IonItem, IonLabel, IonIcon, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonContent, IonInput, IonLabel as IonInputLabel } from '@ionic/react';
import { trash, add, close } from 'ionicons/icons';
import { InfiniteScrollList, PageHeader } from '../../components/common';
import { useSingers } from '../../hooks';
import { useAppSelector } from '../../redux';
@ -12,9 +12,30 @@ const Singers: React.FC = () => {
singers,
isAdmin,
handleRemoveSinger,
handleAddSinger,
} = useSingers();
const [showAddModal, setShowAddModal] = useState(false);
const [newSingerName, setNewSingerName] = useState('');
const singersData = useAppSelector(selectSingers);
const handleOpenAddModal = () => {
setShowAddModal(true);
setNewSingerName('');
};
const handleCloseAddModal = () => {
setShowAddModal(false);
setNewSingerName('');
};
const handleSubmitAddSinger = async () => {
if (newSingerName.trim()) {
await handleAddSinger(newSingerName);
handleCloseAddModal();
}
};
const singersCount = Object.keys(singersData).length;
// Debug logging
@ -48,6 +69,17 @@ const Singers: React.FC = () => {
<PageHeader
title="Singers"
subtitle={`${singersCount} singers in the party`}
action={
isAdmin && (
<IonButton
fill="clear"
onClick={handleOpenAddModal}
className="text-primary"
>
<IonIcon icon={add} slot="icon-only" />
</IonButton>
)
}
/>
<div className="max-w-4xl mx-auto p-6">
@ -63,6 +95,57 @@ const Singers: React.FC = () => {
loadingMessage="Please wait while singers data is being loaded"
/>
</div>
{/* Add Singer Modal */}
<IonModal isOpen={showAddModal} onDidDismiss={handleCloseAddModal}>
<IonHeader>
<IonToolbar>
<IonTitle>Add New Singer</IonTitle>
<IonButton slot="end" fill="clear" onClick={handleCloseAddModal}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent className="ion-padding">
<div>
<div style={{ marginBottom: '2rem' }}>
<IonInputLabel className="bold-label">
Singer Name
</IonInputLabel>
<IonInput
className="visible-input"
value={newSingerName}
onIonInput={(e) => setNewSingerName(e.detail.value || '')}
placeholder="Enter singer name"
clearInput={true}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSubmitAddSinger();
}
}}
/>
</div>
<div className="flex gap-2">
<IonButton
expand="block"
onClick={handleSubmitAddSinger}
disabled={!newSingerName.trim()}
>
Add Singer
</IonButton>
<IonButton
expand="block"
fill="outline"
onClick={handleCloseAddModal}
>
Cancel
</IonButton>
</div>
</div>
</IonContent>
</IonModal>
</>
);
};

View File

@ -203,6 +203,35 @@ export const favoritesService = {
// Singer management operations
export const singerService = {
// Add a new singer
addSinger: async (controllerName: string, singerName: string) => {
const singersRef = ref(database, `controllers/${controllerName}/player/singers`);
const singersSnapshot = await get(singersRef);
const currentSingers = singersSnapshot.exists() ? singersSnapshot.val() : {};
// Check if singer already exists
const existingSinger = Object.values(currentSingers).find((singer) =>
(singer as Singer).name.toLowerCase() === singerName.toLowerCase()
);
if (existingSinger) {
throw new Error('Singer already exists');
}
// Create new singer with current timestamp
const newSinger: Omit<Singer, 'key'> = {
name: singerName,
lastLogin: new Date().toISOString()
};
// Add to singers list
const newSingerRef = push(singersRef);
await set(newSingerRef, newSinger);
return { key: newSingerRef.key };
},
// Remove singer and all their queue items
removeSinger: async (controllerName: string, singerName: string) => {
// First, remove all queue items for this singer

View File

@ -30,9 +30,39 @@ export const useSingers = () => {
}
}, [isAdmin, controllerName, showSuccess, showError]);
const handleAddSinger = useCallback(async (singerName: string) => {
if (!isAdmin) {
showError('Only admins can add singers');
return;
}
if (!controllerName) {
showError('Controller not found');
return;
}
if (!singerName.trim()) {
showError('Singer name cannot be empty');
return;
}
try {
await singerService.addSinger(controllerName, singerName.trim());
showSuccess(`${singerName} added to singers list`);
} catch (error) {
console.error('Failed to add singer:', error);
if (error instanceof Error && error.message === 'Singer already exists') {
showError('Singer already exists');
} else {
showError('Failed to add singer');
}
}
}, [isAdmin, controllerName, showSuccess, showError]);
return {
singers,
isAdmin,
handleRemoveSinger,
handleAddSinger,
};
};

View File

@ -66,3 +66,51 @@ ion-accordion {
ion-accordion ion-item {
--background: transparent;
}
/* Custom modal styling for Singers component */
ion-modal ion-input-label,
ion-modal .ion-input-label,
ion-modal ion-label {
font-weight: bold !important;
font-size: 1rem !important;
color: #111827 !important;
margin-bottom: 0.5rem !important;
display: block !important;
}
ion-modal ion-button {
height: 40px !important;
min-height: 40px !important;
--padding-top: 8px !important;
--padding-bottom: 8px !important;
}
/* Bold label styling for forms and titles */
.bold-label {
font-weight: bold !important;
font-size: 1rem !important;
color: #111827 !important;
margin-bottom: 0.5rem !important;
display: block !important;
}
/* Bold song title styling */
.bold-title {
font-weight: bold !important;
color: #111827 !important;
}
/* Input field styling to match search box */
.visible-input {
border: 1px solid #d1d5db !important;
border-radius: 8px !important;
padding: 0 12px !important;
background: #ffffff !important;
}
.visible-input ion-input {
border: 1px solid #d1d5db !important;
border-radius: 8px !important;
padding: 0 12px !important;
background: #ffffff !important;
}