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

This commit is contained in:
mbrucedogs 2025-07-21 19:10:02 -05:00
parent eca5a8d7e1
commit ca5082e5d3
3 changed files with 110 additions and 24 deletions

View File

@ -327,12 +327,24 @@ export const singerService = {
throw new Error('Singer already exists');
}
// Find the next available numeric key
const numericKeys = Object.keys(currentSingers)
.map((key) => parseInt(key, 10))
.filter((num) => !isNaN(num));
const nextKey = numericKeys.length > 0 ? Math.max(...numericKeys) + 1 : 0;
const nextKeyStr = String(nextKey);
// Find the next available sequential key (0, 1, 2, etc.)
const existingKeys = Object.keys(currentSingers)
.filter(key => /^\d+$/.test(key)) // Only consider numerical keys
.map(key => parseInt(key, 10))
.sort((a, b) => a - b);
// Find the first gap in the sequence, or use the next number after the highest
let nextKey = 0;
for (let i = 0; i < existingKeys.length; i++) {
if (existingKeys[i] !== i) {
nextKey = i;
break;
}
nextKey = i + 1;
}
debugLog('addSinger - existing keys:', existingKeys);
debugLog('addSinger - next key:', nextKey);
// Create new singer with current timestamp
const newSinger: Omit<Singer, 'key'> = {
@ -340,11 +352,11 @@ export const singerService = {
lastLogin: new Date().toISOString()
};
// Add to singers list with numeric key
const newSingerRef = ref(database, `controllers/${controllerName}/player/singers/${nextKeyStr}`);
// Add to singers list with sequential key
const newSingerRef = ref(database, `controllers/${controllerName}/player/singers/${nextKey}`);
await set(newSingerRef, newSinger);
return { key: nextKeyStr };
return { key: nextKey.toString() };
},
// Remove singer and all their queue items
@ -370,22 +382,40 @@ export const singerService = {
}
}
// Then, remove the singer from the singers list
// Then, remove the singer from the singers list and reindex
const singersRef = ref(database, `controllers/${controllerName}/player/singers`);
const singersSnapshot = await get(singersRef);
if (singersSnapshot.exists()) {
const singers = singersSnapshot.val();
debugLog('removeSinger - original singers:', singers);
// Get all remaining singers (excluding the one to be removed)
const remainingSingers = Object.entries(singers)
.filter(([, singer]) => singer && (singer as Singer).name !== singerName)
.map(([key, singer]) => ({ key, singer: singer as Singer }))
.sort((a, b) => a.singer.name.localeCompare(b.singer.name)); // Keep alphabetical order
debugLog('removeSinger - remaining singers:', remainingSingers);
// Create a completely new singers list with sequential keys
const updates: Record<string, Singer | null> = {};
// Find the singer by name and mark for removal
Object.entries(singers).forEach(([key, singer]) => {
if (singer && (singer as Singer).name === singerName) {
updates[key] = null; // Mark for removal
}
// First, remove all existing singers
Object.keys(singers).forEach(key => {
updates[key] = null;
});
// Remove the singer
// Then, add back the remaining singers with sequential keys
remainingSingers.forEach(({ singer }, index) => {
const newKey = index.toString();
debugLog(`removeSinger - reindexing: old key ${singer.key} -> new key ${newKey}, singer: ${singer.name}`);
updates[newKey] = singer;
});
debugLog('removeSinger - updates to apply:', updates);
// Apply all updates atomically
if (Object.keys(updates).length > 0) {
await update(singersRef, updates);
}

View File

@ -1,13 +1,15 @@
import { useCallback } from 'react';
import { useAppSelector, selectSingersArray, selectIsAdmin, selectControllerName } from '../redux';
import { useAppSelector, useAppDispatch } from '../redux';
import { selectSingersArray, selectIsAdmin, selectControllerName } from '../redux';
import { addSinger, removeSinger } from '../redux/controllerSlice';
import { useToast } from './useToast';
import { singerService } from '../firebase/services';
import type { Singer } from '../types';
export const useSingers = () => {
const singers = useAppSelector(selectSingersArray);
const isAdmin = useAppSelector(selectIsAdmin);
const controllerName = useAppSelector(selectControllerName);
const dispatch = useAppDispatch();
const toast = useToast();
const showSuccess = toast?.showSuccess;
const showError = toast?.showError;
@ -24,13 +26,13 @@ export const useSingers = () => {
}
try {
await singerService.removeSinger(controllerName, singer.name);
await dispatch(removeSinger({ controllerName, singerName: singer.name })).unwrap();
showSuccess && showSuccess(`${singer.name} removed from singers list and queue`);
} catch (error) {
console.error('Failed to remove singer:', error);
showError && showError('Failed to remove singer');
}
}, [isAdmin, controllerName, showSuccess, showError]);
}, [isAdmin, controllerName, dispatch, showSuccess, showError]);
const handleAddSinger = useCallback(async (singerName: string) => {
if (!isAdmin) {
@ -49,7 +51,7 @@ export const useSingers = () => {
}
try {
await singerService.addSinger(controllerName, singerName.trim());
await dispatch(addSinger({ controllerName, singerName: singerName.trim() })).unwrap();
showSuccess && showSuccess(`${singerName} added to singers list`);
} catch (error) {
console.error('Failed to add singer:', error);
@ -59,7 +61,7 @@ export const useSingers = () => {
showError && showError('Failed to add singer');
}
}
}, [isAdmin, controllerName, showSuccess, showError]);
}, [isAdmin, controllerName, dispatch, showSuccess, showError]);
return {
singers,

View File

@ -1,7 +1,7 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Controller, Song, QueueItem, TopPlayed } from '../types';
import { controllerService } from '../firebase/services';
import type { Controller, Song, QueueItem, TopPlayed, Singer } from '../types';
import { controllerService, singerService } from '../firebase/services';
// Async thunks for Firebase operations
export const fetchController = createAsyncThunk(
@ -23,6 +23,22 @@ export const updateController = createAsyncThunk(
}
);
export const addSinger = createAsyncThunk(
'controller/addSinger',
async ({ controllerName, singerName }: { controllerName: string; singerName: string }) => {
const result = await singerService.addSinger(controllerName, singerName);
return { key: result.key, singerName };
}
);
export const removeSinger = createAsyncThunk(
'controller/removeSinger',
async ({ controllerName, singerName }: { controllerName: string; singerName: string }) => {
await singerService.removeSinger(controllerName, singerName);
return { singerName };
}
);
// Initial state
interface ControllerState {
data: Controller | null;
@ -85,6 +101,13 @@ const controllerSlice = createSlice({
}
},
updateSingers: (state, action: PayloadAction<Record<string, Singer>>) => {
if (state.data) {
state.data.player.singers = action.payload;
state.lastUpdated = Date.now();
}
},
clearError: (state) => {
state.error = null;
},
@ -129,6 +152,36 @@ const controllerSlice = createSlice({
.addCase(updateController.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to update controller';
})
// addSinger
.addCase(addSinger.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(addSinger.fulfilled, (state) => {
state.loading = false;
// The real-time sync will handle the update
state.lastUpdated = Date.now();
state.error = null;
})
.addCase(addSinger.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to add singer';
})
// removeSinger
.addCase(removeSinger.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(removeSinger.fulfilled, (state) => {
state.loading = false;
// The real-time sync will handle the update
state.lastUpdated = Date.now();
state.error = null;
})
.addCase(removeSinger.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || 'Failed to remove singer';
});
},
});
@ -141,6 +194,7 @@ export const {
updateFavorites,
updateHistory,
updateTopPlayed,
updateSingers,
clearError,
resetController,
} = controllerSlice.actions;