Skip to content

Commit 109e811

Browse files
committed
feat: enhance song fetching and state management in HomePage component
- Updated the HomePage component to utilize fetched recent songs instead of an empty array. - Refactored the RecentSongs context to use Zustand for improved state management, including actions for fetching and managing recent songs. - Introduced hooks for loading recent songs and categories on component mount. - Improved error handling and loading states for better user experience.
1 parent 7837e86 commit 109e811

File tree

5 files changed

+280
-245
lines changed

5 files changed

+280
-245
lines changed

apps/frontend/src/app/(content)/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Metadata } from 'next';
22

3-
import type { FeaturedSongsDto, SongPreviewDto } from '@nbw/database';
3+
import type { FeaturedSongsDto, PageDto, SongPreviewDto } from '@nbw/database';
44
import axiosInstance from '@web/lib/axios';
55
import { HomePageProvider } from '@web/modules/browse/components/client/context/HomePage.context';
66
import { HomePageComponent } from '@web/modules/browse/components/HomePageComponent';
77

88
async function fetchRecentSongs() {
99
try {
10-
const response = await axiosInstance.get<SongPreviewDto[]>('/song', {
10+
const response = await axiosInstance.get<PageDto<SongPreviewDto>>('/song', {
1111
params: {
1212
page: 1, // TODO: fix constants
1313
limit: 16, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination
@@ -16,7 +16,7 @@ async function fetchRecentSongs() {
1616
},
1717
});
1818

19-
return response.data;
19+
return response.data.content;
2020
} catch (error) {
2121
return [];
2222
}
@@ -45,12 +45,12 @@ export const metadata: Metadata = {
4545
};
4646

4747
async function Home() {
48-
//const recentSongs = await fetchRecentSongs();
48+
const recentSongs = await fetchRecentSongs();
4949
const featuredSongs = await fetchFeaturedSongs();
5050

5151
return (
5252
<HomePageProvider
53-
initialRecentSongs={[]}
53+
initialRecentSongs={recentSongs}
5454
initialFeaturedSongs={featuredSongs}
5555
>
5656
<HomePageComponent />

apps/frontend/src/modules/browse/components/HomePageComponent.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ import { WelcomeBanner } from '../WelcomeBanner';
1919

2020
import { CategoryButtonGroup } from './client/CategoryButton';
2121
import { useFeaturedSongsProvider } from './client/context/FeaturedSongs.context';
22-
import { useRecentSongsProvider } from './client/context/RecentSongs.context';
22+
import {
23+
useRecentSongsProvider,
24+
useRecentSongsPageLoader,
25+
useRecentSongsCategoriesLoader,
26+
} from './client/context/RecentSongs.context';
2327
import LoadMoreButton from './client/LoadMoreButton';
2428
import { TimespanButtonGroup } from './client/TimespanButton';
2529
import SongCard from './SongCard';
2630
import SongCardGroup from './SongCardGroup';
2731

2832
export const HomePageComponent = () => {
29-
const { featuredSongsPage } = useFeaturedSongsProvider();
33+
// Initialize sync hooks for proper effect handling
34+
useRecentSongsPageLoader();
35+
useRecentSongsCategoriesLoader();
3036

37+
const { featuredSongsPage, timespan } = useFeaturedSongsProvider();
3138
const { recentSongs, increasePageRecent, hasMore } = useRecentSongsProvider();
32-
const { timespan } = useFeaturedSongsProvider();
3339
return (
3440
<>
3541
{/* Welcome banner/Hero */}
@@ -81,8 +87,8 @@ export const HomePageComponent = () => {
8187
</div>
8288
<div className='h-6' />
8389
<SongCardGroup data-test='recent-songs'>
84-
{recentSongs.map((song, i) =>
85-
song === undefined ? (
90+
{(recentSongs || []).map((song, i) =>
91+
song === undefined || song === null ? (
8692
<SongCardAdSlot key={i} />
8793
) : (
8894
<SongCard key={i} song={song} />
Lines changed: 86 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,111 @@
11
'use client';
22

3-
import { createContext, useContext, useEffect, useState } from 'react';
3+
import { useEffect } from 'react';
4+
import { create } from 'zustand';
45

56
import { TIMESPANS } from '@nbw/config';
67
import { type FeaturedSongsDto, type SongPreviewDto } from '@nbw/database';
78

89
type TimespanType = (typeof TIMESPANS)[number];
910

10-
type FeaturedSongsContextType = {
11+
interface FeaturedSongsState {
12+
featuredSongs: FeaturedSongsDto;
1113
featuredSongsPage: SongPreviewDto[];
1214
timespan: TimespanType;
13-
setTimespan: (timespan: TimespanType) => void;
1415
timespanEmpty: Record<string, boolean>;
15-
};
16-
17-
const FeaturedSongsContext = createContext<FeaturedSongsContextType>(
18-
{} as FeaturedSongsContextType,
19-
);
16+
}
2017

21-
export function FeaturedSongsProvider({
22-
children,
23-
initialFeaturedSongs,
24-
}: {
25-
children: React.ReactNode;
26-
initialFeaturedSongs: FeaturedSongsDto;
27-
}) {
28-
// Helper function to find the first non-empty timespan or default to 'week'
29-
const getInitialTimespan = (): TimespanType => {
30-
// Check if all timespans have songs
31-
const allHaveSongs = TIMESPANS.every(
32-
(ts) => initialFeaturedSongs[ts]?.length > 0,
33-
);
18+
interface FeaturedSongsActions {
19+
initialize: (initialFeaturedSongs: FeaturedSongsDto) => void;
20+
setTimespan: (timespan: TimespanType) => void;
21+
}
3422

35-
// If all have songs, default to 'week'
36-
if (allHaveSongs) {
37-
return 'week';
38-
}
23+
type FeaturedSongsStore = FeaturedSongsState & FeaturedSongsActions;
3924

40-
// Otherwise, find the first timespan that has songs
41-
for (const ts of TIMESPANS) {
42-
if (initialFeaturedSongs[ts]?.length > 0) {
43-
return ts;
44-
}
45-
}
25+
// Helper function to find the first non-empty timespan or default to 'week'
26+
const getInitialTimespan = (featuredSongs: FeaturedSongsDto): TimespanType => {
27+
// Check if all timespans have songs
28+
const allHaveSongs = TIMESPANS.every((ts) => featuredSongs[ts]?.length > 0);
4629

47-
// If none have songs, default to 'week'
30+
// If all have songs, default to 'week'
31+
if (allHaveSongs) {
4832
return 'week';
49-
};
33+
}
5034

51-
const initialTimespan = getInitialTimespan();
35+
// Otherwise, find the first timespan that has songs
36+
for (const ts of TIMESPANS) {
37+
if (featuredSongs[ts]?.length > 0) {
38+
return ts;
39+
}
40+
}
5241

53-
// Featured songs
54-
const [featuredSongs] = useState<FeaturedSongsDto>(initialFeaturedSongs);
55-
const [featuredSongsPage, setFeaturedSongsPage] = useState<SongPreviewDto[]>(
56-
initialFeaturedSongs[initialTimespan],
57-
);
42+
// If none have songs, default to 'week'
43+
return 'week';
44+
};
5845

59-
const [timespan, setTimespan] = useState<TimespanType>(initialTimespan);
46+
export const useFeaturedSongsStore = create<FeaturedSongsStore>((set, get) => ({
47+
// Initial state
48+
featuredSongs: {
49+
hour: [],
50+
day: [],
51+
week: [],
52+
month: [],
53+
year: [],
54+
all: [],
55+
},
56+
featuredSongsPage: [],
57+
timespan: 'week',
58+
timespanEmpty: {},
59+
60+
// Actions
61+
initialize: (initialFeaturedSongs) => {
62+
const initialTimespan = getInitialTimespan(initialFeaturedSongs);
63+
const timespanEmpty = Object.keys(initialFeaturedSongs).reduce(
64+
(result, timespan) => {
65+
result[timespan] =
66+
initialFeaturedSongs[timespan as TimespanType].length === 0;
67+
return result;
68+
},
69+
{} as Record<string, boolean>,
70+
);
6071

61-
const timespanEmpty = Object.keys(featuredSongs).reduce(
62-
(result, timespan) => {
63-
result[timespan] = featuredSongs[timespan as TimespanType].length === 0;
64-
return result;
65-
},
66-
{} as Record<string, boolean>,
67-
);
72+
set({
73+
featuredSongs: initialFeaturedSongs,
74+
featuredSongsPage: initialFeaturedSongs[initialTimespan],
75+
timespan: initialTimespan,
76+
timespanEmpty,
77+
});
78+
},
79+
80+
setTimespan: (timespan) => {
81+
const { featuredSongs } = get();
82+
set({
83+
timespan,
84+
featuredSongsPage: featuredSongs[timespan],
85+
});
86+
},
87+
}));
88+
89+
// Legacy hook name for backward compatibility
90+
export const useFeaturedSongsProvider = () => {
91+
return useFeaturedSongsStore();
92+
};
6893

69-
useEffect(() => {
70-
setFeaturedSongsPage(featuredSongs[timespan]);
71-
}, [featuredSongs, timespan]);
72-
73-
return (
74-
<FeaturedSongsContext.Provider
75-
value={{
76-
featuredSongsPage,
77-
timespan,
78-
setTimespan,
79-
timespanEmpty,
80-
}}
81-
>
82-
{children}
83-
</FeaturedSongsContext.Provider>
84-
);
85-
}
94+
// Provider component for initialization (now just a wrapper)
95+
type FeaturedSongsProviderProps = {
96+
children: React.ReactNode;
97+
initialFeaturedSongs: FeaturedSongsDto;
98+
};
8699

87-
export function useFeaturedSongsProvider() {
88-
const context = useContext(FeaturedSongsContext);
100+
export function FeaturedSongsProvider({
101+
children,
102+
initialFeaturedSongs,
103+
}: FeaturedSongsProviderProps) {
104+
const initialize = useFeaturedSongsStore((state) => state.initialize);
89105

90-
if (context === undefined || context === null) {
91-
throw new Error(
92-
'useFeaturedSongsProvider must be used within a FeaturedSongsProvider',
93-
);
94-
}
106+
useEffect(() => {
107+
initialize(initialFeaturedSongs);
108+
}, [initialFeaturedSongs, initialize]);
95109

96-
return context;
110+
return <>{children}</>;
97111
}
Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
'use client';
22

3-
import { createContext, useContext } from 'react';
4-
53
import { FeaturedSongsDtoType, SongPreviewDtoType } from '@nbw/database';
64

75
import { FeaturedSongsProvider } from './FeaturedSongs.context';
86
import { RecentSongsProvider } from './RecentSongs.context';
9-
10-
type HomePageContextType = null;
11-
12-
const HomePageContext = createContext<HomePageContextType>(
13-
null as HomePageContextType,
14-
);
7+
import { useFeaturedSongsStore } from './FeaturedSongs.context';
8+
import { useRecentSongsStore } from './RecentSongs.context';
9+
10+
/**
11+
* Composed hook that provides access to both FeaturedSongs and RecentSongs stores
12+
*/
13+
export function useHomePageStore() {
14+
const featuredSongs = useFeaturedSongsStore();
15+
const recentSongs = useRecentSongsStore();
16+
17+
return {
18+
featuredSongs,
19+
recentSongs,
20+
};
21+
}
1522

1623
export function HomePageProvider({
1724
children,
@@ -23,24 +30,15 @@ export function HomePageProvider({
2330
initialFeaturedSongs: FeaturedSongsDtoType;
2431
}) {
2532
return (
26-
<HomePageContext.Provider value={null}>
27-
<RecentSongsProvider initialRecentSongs={initialRecentSongs}>
28-
<FeaturedSongsProvider initialFeaturedSongs={initialFeaturedSongs}>
29-
{children}
30-
</FeaturedSongsProvider>
31-
</RecentSongsProvider>
32-
</HomePageContext.Provider>
33+
<RecentSongsProvider initialRecentSongs={initialRecentSongs}>
34+
<FeaturedSongsProvider initialFeaturedSongs={initialFeaturedSongs}>
35+
{children}
36+
</FeaturedSongsProvider>
37+
</RecentSongsProvider>
3338
);
3439
}
3540

41+
// Legacy hook name for backward compatibility
3642
export function useHomePageProvider() {
37-
const context = useContext(HomePageContext);
38-
39-
if (context === undefined || context === null) {
40-
throw new Error(
41-
'useHomePageProvider must be used within a HomepageProvider',
42-
);
43-
}
44-
45-
return context;
43+
return useHomePageStore();
4644
}

0 commit comments

Comments
 (0)