Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
d2d9d9691b
commit
0b7c75d3ab
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user