Skip to content

Commit d3bd954

Browse files
authored
Add routing to library (#13838)
1 parent 41adfa2 commit d3bd954

7 files changed

Lines changed: 238 additions & 15 deletions

File tree

packages/common/src/utils/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export const SAVED_PAGE = '/favorites'
3737
export const FAVORITES_PAGE = '/favorites'
3838

3939
export const LIBRARY_PAGE = '/library'
40+
export const LIBRARY_TRACKS_PAGE = '/library/tracks'
41+
export const LIBRARY_ALBUMS_PAGE = '/library/albums'
42+
export const LIBRARY_PLAYLISTS_PAGE = '/library/playlists'
4043
export const HISTORY_PAGE = '/history'
4144
export const DASHBOARD_PAGE = '/dashboard'
4245
export const AUDIO_PAGE = '/audio'

packages/web/src/app/web-player/WebPlayer.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ const {
229229
EXPLORE_PAGE,
230230
SAVED_PAGE,
231231
LIBRARY_PAGE,
232+
LIBRARY_TRACKS_PAGE,
233+
LIBRARY_ALBUMS_PAGE,
234+
LIBRARY_PLAYLISTS_PAGE,
232235
HISTORY_PAGE,
233236
DASHBOARD_PAGE,
234237
COIN_DETAIL_PAGE,
@@ -865,8 +868,20 @@ const WebPlayer = (props: WebPlayerProps) => {
865868
/>
866869
</>
867870
)}
868-
<Route path={SAVED_PAGE} element={<LibraryPage />} />
869-
<Route path={LIBRARY_PAGE} element={<LibraryPage />} />
871+
<Route
872+
path={SAVED_PAGE}
873+
element={<Navigate to={LIBRARY_TRACKS_PAGE} replace />}
874+
/>
875+
<Route
876+
path={LIBRARY_PAGE}
877+
element={<Navigate to={LIBRARY_TRACKS_PAGE} replace />}
878+
/>
879+
<Route path={LIBRARY_TRACKS_PAGE} element={<LibraryPage />} />
880+
<Route path={LIBRARY_ALBUMS_PAGE} element={<LibraryPage />} />
881+
<Route
882+
path={LIBRARY_PLAYLISTS_PAGE}
883+
element={<LibraryPage />}
884+
/>
870885
<Route path={HISTORY_PAGE} element={<HistoryPage />} />
871886
{!isProduction ? (
872887
<Route path={DEV_TOOLS_PAGE} element={<DevTools />} />
@@ -1271,8 +1286,20 @@ const WebPlayer = (props: WebPlayerProps) => {
12711286
path={UPLOAD_PAGE}
12721287
element={<UploadPage scrollToTop={scrollToTop} />}
12731288
/>
1274-
<Route path={SAVED_PAGE} element={<LibraryPage />} />
1275-
<Route path={LIBRARY_PAGE} element={<LibraryPage />} />
1289+
<Route
1290+
path={SAVED_PAGE}
1291+
element={<Navigate to={LIBRARY_TRACKS_PAGE} replace />}
1292+
/>
1293+
<Route
1294+
path={LIBRARY_PAGE}
1295+
element={<Navigate to={LIBRARY_TRACKS_PAGE} replace />}
1296+
/>
1297+
<Route path={LIBRARY_TRACKS_PAGE} element={<LibraryPage />} />
1298+
<Route path={LIBRARY_ALBUMS_PAGE} element={<LibraryPage />} />
1299+
<Route
1300+
path={LIBRARY_PLAYLISTS_PAGE}
1301+
element={<LibraryPage />}
1302+
/>
12761303
<Route path={HISTORY_PAGE} element={<HistoryPage />} />
12771304
{!isProduction ? (
12781305
<Route path={DEV_TOOLS_PAGE} element={<DevTools />} />

packages/web/src/pages/library-page/components/desktop/LibraryPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ const LibraryPage = () => {
190190

191191
const { tabs, body } = useTabs({
192192
isMobile: false,
193+
selectedTabLabel: currentTab,
193194
didChangeTabsFrom: (_, to) => {
194195
onChangeTab(to as LibraryPageTabs)
195196
},

packages/web/src/pages/library-page/components/mobile/LibraryPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ const LibraryPage = () => {
593593
const { tabs, body } = useTabs({
594594
tabs: tabHeaders,
595595
elements,
596+
selectedTabLabel: currentTab,
596597
initialScrollOffset: SCROLL_HEIGHT,
597598
onTabClick: handleTabClick
598599
})

packages/web/src/pages/library-page/hooks/useLibraryPage.ts

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22

33
import {
44
useCurrentAccount,
@@ -19,6 +19,7 @@ import {
1919
libraryPageTracksLineupActions as tracksActions,
2020
libraryPageActions as saveActions,
2121
libraryPageSelectors,
22+
LibraryCategory,
2223
LibraryPageTabs,
2324
queueSelectors,
2425
tracksSocialActions as socialActions,
@@ -34,15 +35,29 @@ import { route } from '@audius/common/utils'
3435
import { GetUserLibraryTracksSortMethodEnum } from '@audius/sdk'
3536
import { debounce } from 'lodash'
3637
import { useDispatch, useSelector } from 'react-redux'
38+
import { useLocation, useNavigate, useSearchParams } from 'react-router'
3739

3840
import { TrackEvent, make } from 'common/store/analytics/actions'
3941
import { push } from 'utils/navigation'
4042

43+
import {
44+
getTabFromPathname,
45+
getLibraryPath,
46+
categoryFromFilterParam,
47+
filterParamFromCategory,
48+
LIBRARY_FILTER_PARAM,
49+
LIBRARY_SEARCH_PARAM
50+
} from '../lib/libraryUrl'
51+
4152
const { profilePage } = route
4253
const { makeGetCurrent } = queueSelectors
4354
const { getPlaying, getBuffering } = playerSelectors
44-
const { getLibraryTracksLineup, hasReachedEnd, getTracksCategory } =
45-
libraryPageSelectors
55+
const {
56+
getLibraryTracksLineup,
57+
hasReachedEnd,
58+
getTracksCategory,
59+
getCategory
60+
} = libraryPageSelectors
4661
const { updatedPlaylistViewed } = playlistUpdatesActions
4762

4863
const { selectAllPlaylistUpdateIds } = playlistUpdatesSelectors
@@ -77,6 +92,11 @@ type LibraryPageState = {
7792

7893
export const useLibraryPage = () => {
7994
const dispatch = useDispatch()
95+
const location = useLocation()
96+
const navigate = useNavigate()
97+
const [searchParams, setSearchParams] = useSearchParams()
98+
const lastCategoryUrlRef = useRef<string | null>(null)
99+
80100
const currentTrack = useCurrentTrack()
81101
const tracks = useLineupTable(getLibraryTracksLineup)
82102

@@ -101,16 +121,70 @@ export const useLibraryPage = () => {
101121
}
102122
})
103123

124+
const urlTab = getTabFromPathname(location.pathname)
125+
const urlFilter = searchParams.get(LIBRARY_FILTER_PARAM)
126+
const urlSearch = searchParams.get(LIBRARY_SEARCH_PARAM) ?? ''
127+
104128
const [state, setState] = useState<LibraryPageState>({
105-
filterText: '',
129+
filterText: urlSearch,
106130
sortMethod: '',
107131
sortDirection: '',
108132
initialOrder: null,
109133
allTracksFetched: false,
110-
currentTab: ProfileTabs.TRACKS,
134+
currentTab: urlTab,
111135
shouldReturnToTrackPurchases: false
112136
})
113137

138+
const selectedCategoryForUrlTab = useSelector(
139+
(state: Parameters<typeof getCategory>[0]) =>
140+
getCategory(state, { currentTab: urlTab })
141+
)
142+
143+
// Sync from URL to state and Redux when location changes
144+
useEffect(() => {
145+
const tab = getTabFromPathname(location.pathname)
146+
let category = categoryFromFilterParam(urlFilter)
147+
if (
148+
tab === LibraryPageTabs.PLAYLISTS &&
149+
category === LibraryCategory.Purchase
150+
) {
151+
category = LibraryCategory.All
152+
}
153+
const search = urlSearch
154+
155+
lastCategoryUrlRef.current = filterParamFromCategory(category)
156+
setState((prev) => ({
157+
...prev,
158+
currentTab: tab,
159+
filterText: search
160+
}))
161+
dispatch(
162+
saveActions.setSelectedCategory({
163+
currentTab: tab,
164+
category
165+
})
166+
)
167+
}, [location.pathname, urlFilter, urlSearch, dispatch])
168+
169+
// When user changes category via menu (Redux updates), sync to URL
170+
useEffect(() => {
171+
const urlCategoryParam = filterParamFromCategory(selectedCategoryForUrlTab)
172+
if (lastCategoryUrlRef.current === urlCategoryParam) return
173+
lastCategoryUrlRef.current = urlCategoryParam
174+
setSearchParams(
175+
(prev) => {
176+
const next = new URLSearchParams(prev)
177+
if (urlCategoryParam === 'all') {
178+
next.delete(LIBRARY_FILTER_PARAM)
179+
} else {
180+
next.set(LIBRARY_FILTER_PARAM, urlCategoryParam)
181+
}
182+
return next
183+
},
184+
{ replace: true }
185+
)
186+
}, [selectedCategoryForUrlTab, setSearchParams])
187+
114188
const fetchLibraryTracks = useCallback(
115189
(
116190
query?: string,
@@ -310,17 +384,42 @@ export const useLibraryPage = () => {
310384
handleFetchSavedTracks()
311385
}, [tracksCategory, handleFetchSavedTracks])
312386

387+
const updateSearchParam = useCallback(
388+
(search: string) => {
389+
setSearchParams(
390+
(prev) => {
391+
const next = new URLSearchParams(prev)
392+
if (search.trim() === '') {
393+
next.delete(LIBRARY_SEARCH_PARAM)
394+
} else {
395+
next.set(LIBRARY_SEARCH_PARAM, search)
396+
}
397+
return next
398+
},
399+
{ replace: true }
400+
)
401+
},
402+
[setSearchParams]
403+
)
404+
405+
const debouncedUpdateSearchParam = useMemo(
406+
() => debounce(updateSearchParam, 300),
407+
[updateSearchParam]
408+
)
409+
313410
const onFilterChange = useCallback(
314411
(e: any) => {
412+
const value = e.target.value
315413
const callBack = !state.allTracksFetched
316414
? handleFetchSavedTracks
317415
: undefined
318-
setState((prev) => ({ ...prev, filterText: e.target.value }))
416+
setState((prev) => ({ ...prev, filterText: value }))
417+
debouncedUpdateSearchParam(value)
319418
if (callBack) {
320419
callBack()
321420
}
322421
},
323-
[state.allTracksFetched, handleFetchSavedTracks]
422+
[state.allTracksFetched, handleFetchSavedTracks, debouncedUpdateSearchParam]
324423
)
325424

326425
const onSortChange = useCallback(
@@ -554,9 +653,15 @@ export const useLibraryPage = () => {
554653
[formatMetadata, tracks.entries, state.initialOrder, updateLineupOrder]
555654
)
556655

557-
const onChangeTab = useCallback((tab: LibraryPageTabs) => {
558-
setState((prev) => ({ ...prev, currentTab: tab }))
559-
}, [])
656+
const onChangeTab = useCallback(
657+
(tab: LibraryPageTabs) => {
658+
setState((prev) => ({ ...prev, currentTab: tab }))
659+
const path = getLibraryPath(tab)
660+
const search = searchParams.toString()
661+
navigate(search ? `${path}?${search}` : path)
662+
},
663+
[navigate, searchParams]
664+
)
560665

561666
const isQueuedValue = isQueued()
562667
const playingUid = getPlayingUid()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
LibraryCategory,
3+
LibraryPageTabs,
4+
LibraryCategoryType
5+
} from '@audius/common/store'
6+
import { route } from '@audius/common/utils'
7+
8+
const { LIBRARY_ALBUMS_PAGE, LIBRARY_PLAYLISTS_PAGE, LIBRARY_TRACKS_PAGE } =
9+
route
10+
11+
export const LIBRARY_FILTER_PARAM = 'filter'
12+
export const LIBRARY_SEARCH_PARAM = 'search'
13+
14+
export const LIBRARY_TAB_PATHS = [
15+
LIBRARY_TRACKS_PAGE,
16+
LIBRARY_ALBUMS_PAGE,
17+
LIBRARY_PLAYLISTS_PAGE
18+
] as const
19+
20+
const PATH_TO_TAB: Record<string, LibraryPageTabs> = {
21+
[LIBRARY_TRACKS_PAGE]: LibraryPageTabs.TRACKS,
22+
[LIBRARY_ALBUMS_PAGE]: LibraryPageTabs.ALBUMS,
23+
[LIBRARY_PLAYLISTS_PAGE]: LibraryPageTabs.PLAYLISTS
24+
}
25+
26+
const TAB_TO_PATH: Record<LibraryPageTabs, string> = {
27+
[LibraryPageTabs.TRACKS]: LIBRARY_TRACKS_PAGE,
28+
[LibraryPageTabs.ALBUMS]: LIBRARY_ALBUMS_PAGE,
29+
[LibraryPageTabs.PLAYLISTS]: LIBRARY_PLAYLISTS_PAGE
30+
}
31+
32+
/** URL filter values: all, favorites, reposts, premium (premium = purchase) */
33+
export const FILTER_URL_VALUES = [
34+
'all',
35+
'favorites',
36+
'reposts',
37+
'premium'
38+
] as const
39+
export type LibraryFilterParam = (typeof FILTER_URL_VALUES)[number]
40+
41+
const URL_FILTER_TO_CATEGORY: Record<string, LibraryCategoryType> = {
42+
all: LibraryCategory.All,
43+
favorites: LibraryCategory.Favorite,
44+
reposts: LibraryCategory.Repost,
45+
premium: LibraryCategory.Purchase
46+
}
47+
48+
const CATEGORY_TO_URL_FILTER: Record<LibraryCategoryType, LibraryFilterParam> =
49+
{
50+
[LibraryCategory.All]: 'all',
51+
[LibraryCategory.Favorite]: 'favorites',
52+
[LibraryCategory.Repost]: 'reposts',
53+
[LibraryCategory.Purchase]: 'premium'
54+
}
55+
56+
export function getTabFromPathname(pathname: string): LibraryPageTabs {
57+
const path = LIBRARY_TAB_PATHS.find((p) => pathname === p)
58+
return path ? PATH_TO_TAB[path] : LibraryPageTabs.TRACKS
59+
}
60+
61+
export function getLibraryPath(tab: LibraryPageTabs): string {
62+
return TAB_TO_PATH[tab] ?? LIBRARY_TRACKS_PAGE
63+
}
64+
65+
export function categoryFromFilterParam(
66+
param: string | null
67+
): LibraryCategoryType {
68+
if (!param) return LibraryCategory.All
69+
const category = URL_FILTER_TO_CATEGORY[param.toLowerCase()]
70+
return category ?? LibraryCategory.All
71+
}
72+
73+
export function filterParamFromCategory(
74+
category: LibraryCategoryType
75+
): LibraryFilterParam {
76+
return CATEGORY_TO_URL_FILTER[category] ?? 'all'
77+
}
78+
79+
export function isLibraryFilterParam(
80+
value: string
81+
): value is LibraryFilterParam {
82+
return FILTER_URL_VALUES.includes(value as LibraryFilterParam)
83+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import { makePageRoute } from 'ssr/util'
22

3-
export default makePageRoute(['/library'], 'Library Page')
3+
export default makePageRoute(
4+
['/library', '/library/tracks', '/library/albums', '/library/playlists'],
5+
'Library Page'
6+
)

0 commit comments

Comments
 (0)