Skip to content

Commit ebf4290

Browse files
committed
feat: enhance MySongs context and component for improved song management
- Introduced Zustand for state management in MySongs context, replacing the previous useState approach. - Added new actions for song pagination and deletion, including loadPage, nextpage, prevpage, and deleteSong. - Implemented useMySongsPageLoader hook to synchronize page loading with currentPage changes. - Updated MySongsTable component to utilize the new useMySongsPageLoader for better page management.
1 parent 625e757 commit ebf4290

File tree

2 files changed

+162
-132
lines changed

2 files changed

+162
-132
lines changed

apps/frontend/src/modules/my-songs/components/client/MySongsTable.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { MY_SONGS } from '@nbw/config';
1212
import type { SongPageDtoType, SongPreviewDtoType } from '@nbw/database';
1313
import { ErrorBox } from '@web/modules/shared/components/client/ErrorBox';
1414

15-
import { useMySongsProvider } from './context/MySongs.context';
15+
import {
16+
useMySongsProvider,
17+
useMySongsPageLoader,
18+
} from './context/MySongs.context';
1619
import DeleteConfirmDialog from './DeleteConfirmDialog';
1720
import { SongRow } from './SongRow';
1821

@@ -134,6 +137,9 @@ export const MySongsTable = () => {
134137
};
135138

136139
export const MySongsPageComponent = () => {
140+
// Load page when currentPage changes
141+
useMySongsPageLoader();
142+
137143
const {
138144
error,
139145
isDeleteDialogOpen,
Lines changed: 155 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
'use client';
22

3-
import {
4-
createContext,
5-
useCallback,
6-
useContext,
7-
useEffect,
8-
useState,
9-
} from 'react';
3+
import { useEffect } from 'react';
4+
import { create } from 'zustand';
105
import { toast } from 'react-hot-toast';
116

127
import { MY_SONGS } from '@nbw/config';
@@ -18,69 +13,73 @@ import type {
1813
import axiosInstance from '@web/lib/axios';
1914
import { getTokenLocal } from '@web/lib/axios/token.utils';
2015

21-
type MySongsContextType = {
16+
interface MySongsState {
17+
loadedSongs: SongsFolder;
2218
page: SongPageDtoType | null;
23-
nextpage: () => void;
24-
prevpage: () => void;
25-
gotoPage: (page: number) => void;
2619
totalSongs: number;
2720
totalPages: number;
2821
currentPage: number;
2922
pageSize: number;
3023
isLoading: boolean;
3124
error: string | null;
3225
isDeleteDialogOpen: boolean;
33-
setIsDeleteDialogOpen: (isOpen: boolean) => void;
3426
songToDelete: SongPreviewDtoType | null;
27+
}
28+
29+
interface MySongsActions {
30+
initialize: (
31+
initialSongsFolder: SongsFolder,
32+
totalPagesInit: number,
33+
currentPageInit: number,
34+
pageSizeInit: number,
35+
) => void;
36+
fetchSongsPage: () => Promise<void>;
37+
loadPage: () => Promise<void>;
38+
gotoPage: (page: number) => void;
39+
nextpage: () => void;
40+
prevpage: () => void;
41+
setIsDeleteDialogOpen: (isOpen: boolean) => void;
3542
setSongToDelete: (song: SongPreviewDtoType) => void;
36-
deleteSong: () => void;
37-
};
38-
39-
const MySongsContext = createContext<MySongsContextType>(
40-
{} as MySongsContextType,
41-
);
42-
43-
type MySongProviderProps = {
44-
InitialsongsFolder?: SongsFolder;
45-
children?: React.ReactNode;
46-
totalPagesInit?: number;
47-
currentPageInit?: number;
48-
pageSizeInit?: number;
49-
};
50-
51-
export const MySongProvider = ({
52-
InitialsongsFolder = {},
53-
children,
54-
totalPagesInit = 0,
55-
currentPageInit = 0,
56-
pageSizeInit = MY_SONGS.PAGE_SIZE,
57-
}: MySongProviderProps) => {
58-
const [loadedSongs, setLoadedSongs] =
59-
useState<SongsFolder>(InitialsongsFolder);
60-
61-
const [totalSongs, setTotalSongs] = useState<number>(0);
62-
const [totalPages, setTotalPages] = useState<number>(totalPagesInit);
63-
const [currentPage, setCurrentPage] = useState<number>(currentPageInit);
64-
65-
const [pageSize, _setPageSize] = useState<number>(pageSizeInit);
66-
const [page, setPage] = useState<SongPageDtoType | null>(null);
67-
const [isLoading, setIsLoading] = useState<boolean>(true);
68-
const [error, setError] = useState<string | null>(null);
69-
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState<boolean>(false);
70-
71-
const [songToDelete, setSongToDelete] = useState<SongPreviewDtoType | null>(
72-
null,
73-
);
74-
75-
const putPage = useCallback(
76-
async ({ key, page }: { key: number; page: SongPageDtoType }) => {
77-
setLoadedSongs({ ...loadedSongs, [key]: page });
78-
},
79-
[loadedSongs],
80-
);
81-
82-
const fetchSongsPage = useCallback(async (): Promise<void> => {
83-
setIsLoading(true);
43+
deleteSong: () => Promise<void>;
44+
}
45+
46+
type MySongsStore = MySongsState & MySongsActions;
47+
48+
export const useMySongsStore = create<MySongsStore>((set, get) => ({
49+
// Initial state
50+
loadedSongs: {},
51+
page: null,
52+
totalSongs: 0,
53+
totalPages: 0,
54+
currentPage: 0,
55+
pageSize: MY_SONGS.PAGE_SIZE,
56+
isLoading: true,
57+
error: null,
58+
isDeleteDialogOpen: false,
59+
songToDelete: null,
60+
61+
// Actions
62+
initialize: (
63+
initialSongsFolder,
64+
totalPagesInit,
65+
currentPageInit,
66+
pageSizeInit,
67+
) => {
68+
const initialPage = initialSongsFolder[currentPageInit] || null;
69+
set({
70+
loadedSongs: initialSongsFolder,
71+
totalPages: totalPagesInit,
72+
currentPage: currentPageInit,
73+
pageSize: pageSizeInit,
74+
page: initialPage,
75+
totalSongs: initialPage?.total || 0,
76+
isLoading: false,
77+
});
78+
},
79+
80+
fetchSongsPage: async () => {
81+
const { currentPage, pageSize } = get();
82+
set({ isLoading: true });
8483
const token = getTokenLocal();
8584

8685
try {
@@ -99,67 +98,73 @@ export const MySongProvider = ({
9998
const data = response.data as SongPageDtoType;
10099

101100
// TODO: total, page and pageSize are stored in every page, when it should be stored in the folder (what matters is 'content')
102-
putPage({
103-
key: currentPage,
101+
set((state) => ({
102+
loadedSongs: { ...state.loadedSongs, [currentPage]: data },
103+
totalSongs: data.total,
104+
totalPages: Math.ceil(data.total / pageSize),
104105
page: data,
105-
});
106-
107-
setTotalSongs(data.total);
108-
setTotalPages(Math.ceil(data.total / pageSize));
109-
setPage(data);
106+
error: null,
107+
}));
110108
} catch (error: unknown) {
109+
let errorMessage: string | null = null;
111110
if (error instanceof Error) {
112-
setError(error.message);
113-
}
114-
115-
if (error instanceof Response) {
116-
setError(error.statusText);
111+
errorMessage = error.message;
112+
} else if (error instanceof Response) {
113+
errorMessage = error.statusText;
117114
}
115+
set({ error: errorMessage });
118116
} finally {
119-
setIsLoading(false);
117+
set({ isLoading: false });
120118
}
121-
}, [currentPage, pageSize, putPage]);
119+
},
122120

123-
const loadPage = useCallback(async () => {
124-
if (currentPage in loadedSongs) {
125-
setPage(loadedSongs[currentPage]);
126-
setIsLoading(false);
121+
loadPage: async () => {
122+
const { currentPage, loadedSongs, fetchSongsPage } = get();
127123

124+
set({ isLoading: true });
125+
126+
if (currentPage in loadedSongs) {
127+
set({
128+
page: loadedSongs[currentPage],
129+
isLoading: false,
130+
});
128131
return;
129132
}
130133

131134
await fetchSongsPage();
132-
}, [currentPage, fetchSongsPage, loadedSongs]);
133-
134-
useEffect(() => {
135-
(async () => {
136-
setIsLoading(true);
137-
loadPage();
138-
})();
139-
}, [currentPage, loadPage]);
135+
},
140136

141-
const gotoPage = useCallback(
142-
(page: number) => {
143-
if (page > 0 && page <= totalPages) {
144-
setCurrentPage(page);
145-
}
146-
},
147-
[totalPages],
148-
);
137+
gotoPage: (page) => {
138+
const { totalPages } = get();
139+
if (page > 0 && page <= totalPages) {
140+
set({ currentPage: page });
141+
}
142+
},
149143

150-
const nextpage = useCallback(() => {
144+
nextpage: () => {
145+
const { currentPage, totalPages, gotoPage } = get();
151146
if (currentPage < totalPages) {
152147
gotoPage(currentPage + 1);
153148
}
154-
}, [currentPage, totalPages, gotoPage]);
149+
},
155150

156-
const prevpage = useCallback(() => {
151+
prevpage: () => {
152+
const { currentPage, gotoPage } = get();
157153
if (currentPage > 1) {
158154
gotoPage(currentPage - 1);
159155
}
160-
}, [currentPage, gotoPage]);
156+
},
161157

162-
const deleteSong = useCallback(async () => {
158+
setIsDeleteDialogOpen: (isOpen) => {
159+
set({ isDeleteDialogOpen: isOpen });
160+
},
161+
162+
setSongToDelete: (song) => {
163+
set({ songToDelete: song });
164+
},
165+
166+
deleteSong: async () => {
167+
const { songToDelete, fetchSongsPage } = get();
163168
if (!songToDelete) {
164169
return;
165170
}
@@ -173,8 +178,10 @@ export const MySongProvider = ({
173178
},
174179
});
175180

176-
setIsDeleteDialogOpen(false);
177-
setSongToDelete(null);
181+
set({
182+
isDeleteDialogOpen: false,
183+
songToDelete: null,
184+
});
178185
} catch (error: unknown) {
179186
toast.error('An error occurred while deleting the song!');
180187

@@ -189,39 +196,56 @@ export const MySongProvider = ({
189196

190197
await fetchSongsPage();
191198
toast.success('Song deleted successfully!');
192-
}, [songToDelete, fetchSongsPage]);
193-
194-
return (
195-
<MySongsContext.Provider
196-
value={{
197-
page,
198-
nextpage,
199-
prevpage,
200-
gotoPage,
201-
totalSongs,
202-
totalPages,
203-
currentPage,
204-
pageSize,
205-
isLoading,
206-
error,
207-
isDeleteDialogOpen,
208-
setIsDeleteDialogOpen,
209-
songToDelete,
210-
setSongToDelete,
211-
deleteSong,
212-
}}
213-
>
214-
{children}
215-
</MySongsContext.Provider>
216-
);
199+
},
200+
}));
201+
202+
// Hook to sync currentPage changes with loadPage
203+
export const useMySongsPageLoader = () => {
204+
const currentPage = useMySongsStore((state) => state.currentPage);
205+
const loadPage = useMySongsStore((state) => state.loadPage);
206+
207+
useEffect(() => {
208+
loadPage();
209+
}, [currentPage, loadPage]);
217210
};
218211

212+
// Legacy hook name for backward compatibility
219213
export const useMySongsProvider = () => {
220-
const context = useContext(MySongsContext);
214+
return useMySongsStore();
215+
};
221216

222-
if (context === undefined || context === null) {
223-
throw new Error('useMySongsProvider must be used within a MySongsProvider');
224-
}
217+
// Provider component for initialization (now just a wrapper)
218+
type MySongProviderProps = {
219+
InitialsongsFolder?: SongsFolder;
220+
children?: React.ReactNode;
221+
totalPagesInit?: number;
222+
currentPageInit?: number;
223+
pageSizeInit?: number;
224+
};
225+
226+
export const MySongProvider = ({
227+
InitialsongsFolder = {},
228+
children,
229+
totalPagesInit = 0,
230+
currentPageInit = 0,
231+
pageSizeInit = MY_SONGS.PAGE_SIZE,
232+
}: MySongProviderProps) => {
233+
const initialize = useMySongsStore((state) => state.initialize);
225234

226-
return context;
235+
useEffect(() => {
236+
initialize(
237+
InitialsongsFolder,
238+
totalPagesInit,
239+
currentPageInit,
240+
pageSizeInit,
241+
);
242+
}, [
243+
InitialsongsFolder,
244+
totalPagesInit,
245+
currentPageInit,
246+
pageSizeInit,
247+
initialize,
248+
]);
249+
250+
return <>{children}</>;
227251
};

0 commit comments

Comments
 (0)