diff --git a/src/components/common/PageHeader.tsx b/src/components/common/PageHeader.tsx index e8e6f6a..7616683 100644 --- a/src/components/common/PageHeader.tsx +++ b/src/components/common/PageHeader.tsx @@ -3,15 +3,23 @@ import React from 'react'; interface PageHeaderProps { title: string; subtitle?: string; + action?: React.ReactNode; } -const PageHeader: React.FC = ({ title, subtitle }) => { +const PageHeader: React.FC = ({ title, subtitle, action }) => { return (
-
-

{title}

- {subtitle && ( -

{subtitle}

+
+
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+ {action && ( +
+ {action} +
)}
diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index 12c108b..5c6cd51 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -102,7 +102,7 @@ const SongItem: React.FC = ({ return ( -

+

{song.title}

diff --git a/src/features/Singers/Singers.tsx b/src/features/Singers/Singers.tsx index a218ef2..2d107f8 100644 --- a/src/features/Singers/Singers.tsx +++ b/src/features/Singers/Singers.tsx @@ -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 = () => { + + + ) + } />

@@ -63,6 +95,57 @@ const Singers: React.FC = () => { loadingMessage="Please wait while singers data is being loaded" />
+ + {/* Add Singer Modal */} + + + + Add New Singer + + + + + + + +
+
+ + Singer Name + + setNewSingerName(e.detail.value || '')} + placeholder="Enter singer name" + clearInput={true} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleSubmitAddSinger(); + } + }} + /> +
+ +
+ + Add Singer + + + Cancel + +
+
+
+
); }; diff --git a/src/firebase/services.ts b/src/firebase/services.ts index 6f3b095..6752923 100644 --- a/src/firebase/services.ts +++ b/src/firebase/services.ts @@ -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 = { + 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 diff --git a/src/hooks/useSingers.ts b/src/hooks/useSingers.ts index 1fa49f4..3a0b109 100644 --- a/src/hooks/useSingers.ts +++ b/src/hooks/useSingers.ts @@ -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, }; }; \ No newline at end of file diff --git a/src/index.css b/src/index.css index f6c5370..7302c87 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +}