diff --git a/packages/common/src/models/SsrPageProps.ts b/packages/common/src/models/SsrPageProps.ts index bdbb3f72734..f5ddfd4145c 100644 --- a/packages/common/src/models/SsrPageProps.ts +++ b/packages/common/src/models/SsrPageProps.ts @@ -3,5 +3,6 @@ import { full as FullSdk } from '@audius/sdk' export type SsrPageProps = { track?: FullSdk.TrackFull user?: FullSdk.UserFull + collection?: FullSdk.PlaylistFull error?: { isErrorPageOpen: boolean } } diff --git a/packages/common/src/store/cache/collections/reducer.ts b/packages/common/src/store/cache/collections/reducer.ts index 28e0c9ccdb8..b655a9cba28 100644 --- a/packages/common/src/store/cache/collections/reducer.ts +++ b/packages/common/src/store/cache/collections/reducer.ts @@ -1,7 +1,16 @@ +import snakecaseKeys from 'snakecase-keys' + +import { makePlaylist } from '~/services/audius-api-client/ResponseAdapter' import { initialCacheState } from '~/store/cache/reducer' import { makeUid } from '~/utils' -import { Collection, ID, Kind, PlaylistTrackId } from '../../../models' +import { + Collection, + ID, + Kind, + PlaylistTrackId, + SsrPageProps +} from '../../../models' import { AddEntriesAction, AddSuccededAction, @@ -89,9 +98,47 @@ const actionsMap = { } } -const reducer = (state = initialState, action: any) => { - const matchingReduceFunction = actionsMap[action.type] - if (!matchingReduceFunction) return state - return matchingReduceFunction(state, action) +const buildInitialState = (ssrPageProps?: SsrPageProps) => { + // If we have preloaded data from the server, populate the initial + // cache state with it + if (ssrPageProps?.collection) { + // @ts-ignore + const collection = makePlaylist(snakecaseKeys(ssrPageProps.collection)) + if (!collection) return initialState + + const id = collection.playlist_id + const uid = makeUid(Kind.COLLECTIONS, id) + + return { + ...initialState, + entries: { + [id]: { + metadata: collection, + _timestamp: Date.now() + } + }, + uids: { + [uid]: collection.playlist_id + }, + statuses: { + [id]: 'SUCCESS' + } + } + } + return initialState } + +const reducer = + (ssrPageProps?: SsrPageProps) => + (state: CollectionsCacheState, action: any) => { + if (!state) { + // @ts-ignore + state = buildInitialState(ssrPageProps) + } + + const matchingReduceFunction = actionsMap[action.type] + if (!matchingReduceFunction) return state + return matchingReduceFunction(state, action) + } + export default reducer diff --git a/packages/common/src/store/cache/users/reducer.ts b/packages/common/src/store/cache/users/reducer.ts index 634eaa050ff..60a56bb4501 100644 --- a/packages/common/src/store/cache/users/reducer.ts +++ b/packages/common/src/store/cache/users/reducer.ts @@ -67,7 +67,11 @@ const buildInitialState = (ssrPageProps?: SsrPageProps) => { // If we have preloaded data from the server, populate the initial // cache state with it if (ssrPageProps) { - const apiUser = ssrPageProps.user ?? ssrPageProps.track?.user ?? null + const apiUser = + ssrPageProps.user ?? + ssrPageProps.track?.user ?? + ssrPageProps.collection?.user ?? + null // @ts-ignore const user = apiUser ? makeUser(snakecaseKeys(apiUser)) : null @@ -76,7 +80,7 @@ const buildInitialState = (ssrPageProps?: SsrPageProps) => { const id = user.user_id const uid = makeUid(Kind.USERS, id) - const initialCacheState = { + return { ...initialState, entries: { [id]: { @@ -91,7 +95,6 @@ const buildInitialState = (ssrPageProps?: SsrPageProps) => { [id]: 'SUCCESS' } } - return initialCacheState } return initialState } diff --git a/packages/common/src/store/pages/collection/reducer.ts b/packages/common/src/store/pages/collection/reducer.ts index 8d9e211d5f6..7a533f1ab4e 100644 --- a/packages/common/src/store/pages/collection/reducer.ts +++ b/packages/common/src/store/pages/collection/reducer.ts @@ -1,7 +1,12 @@ +import snakecaseKeys from 'snakecase-keys' + +import { Kind, SsrPageProps } from '~/models' import { Collection } from '~/models/Collection' +import { makePlaylist } from '~/services/audius-api-client/ResponseAdapter' import tracksReducer, { initialState as initialLineupState } from '~/store/pages/collection/lineup/reducer' +import { makeUid } from '~/utils' import { Status } from '../../../models/Status' import { LineupActions, asLineup } from '../../../store/lineup/reducer' @@ -88,18 +93,47 @@ const actionsMap = { const tracksLineupReducer = asLineup(tracksPrefix, tracksReducer) -const reducer = ( - state = initialState, - action: CollectionPageAction | LineupActions -) => { - const updatedTracks = tracksLineupReducer( - state.tracks, - action as LineupActions - ) - if (updatedTracks !== state.tracks) return { ...state, tracks: updatedTracks } - const matchingReduceFunction = actionsMap[action.type] - if (!matchingReduceFunction) return state - return matchingReduceFunction(state, action as CollectionPageAction) +const buildInitialState = (ssrPageProps?: SsrPageProps) => { + // If we have preloaded data from the server, populate the initial + // page state with it + if (ssrPageProps?.collection) { + // @ts-ignore + const collection = makePlaylist(snakecaseKeys(ssrPageProps.collection)) + if (!collection) return initialState + + return { + ...initialState, + collectionId: collection.playlist_id, + collectionUid: makeUid(Kind.COLLECTIONS, collection.playlist_id), + userUid: makeUid(Kind.USERS, collection.user.user_id), + status: Status.SUCCESS, + collectionPermalink: collection.permalink + } + } + return initialState } +const reducer = + (ssrPageProps?: SsrPageProps) => + ( + state: CollectionsPageState, + action: CollectionPageAction | LineupActions + ) => { + if (!state) { + // @ts-ignore + state = buildInitialState(ssrPageProps) + } + + const updatedTracks = tracksLineupReducer( + // @ts-ignore + state.tracks, + action as LineupActions + ) + if (updatedTracks !== state.tracks) + return { ...state, tracks: updatedTracks } + const matchingReduceFunction = actionsMap[action.type] + if (!matchingReduceFunction) return state + return matchingReduceFunction(state, action as CollectionPageAction) + } + export default reducer diff --git a/packages/common/src/store/pages/profile/reducer.ts b/packages/common/src/store/pages/profile/reducer.ts index 65397476e94..3d22614a74a 100644 --- a/packages/common/src/store/pages/profile/reducer.ts +++ b/packages/common/src/store/pages/profile/reducer.ts @@ -398,7 +398,6 @@ const reducer = } const tracks = tracksLineupReducer( - // TODO: KJ - Fix this later, weird never type // @ts-ignore newEntry.tracks, action as LineupActions diff --git a/packages/common/src/store/reducers.ts b/packages/common/src/store/reducers.ts index 26a2cb13b9c..80dc1c2bc74 100644 --- a/packages/common/src/store/reducers.ts +++ b/packages/common/src/store/reducers.ts @@ -164,7 +164,7 @@ export const reducers = ( // Cache // @ts-ignore - collections: asCache(collectionsReducer, Kind.COLLECTIONS), + collections: asCache(collectionsReducer(ssrPageProps), Kind.COLLECTIONS), // TODO: Fix type error // @ts-ignore tracks: asCache(tracksReducer(ssrPageProps), Kind.TRACKS), @@ -239,7 +239,7 @@ export const reducers = ( audioRewards: audioRewardsSlice.reducer, audioTransactions: audioTransactionsSlice.reducer, chat: chatReducer, - collection, + collection: collection(ssrPageProps), deactivateAccount: deactivateAccountReducer, feed, explore: explorePageReducer, diff --git a/packages/common/src/utils/urlUtils.ts b/packages/common/src/utils/urlUtils.ts index 28a48908fbc..eb6db9e60eb 100644 --- a/packages/common/src/utils/urlUtils.ts +++ b/packages/common/src/utils/urlUtils.ts @@ -30,7 +30,7 @@ const collectionUrlRegex = export const isCollectionUrl = (url: string) => new RegExp(collectionUrlRegex).test(url) export const getPathFromPlaylistUrl = (url: string) => { - const results = new RegExp(trackUrlRegex).exec(url) + const results = new RegExp(collectionUrlRegex).exec(url) if (!results) return null return results[3] } diff --git a/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx b/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx index 1a52a7a017f..0728f833a71 100644 --- a/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx +++ b/packages/web/src/pages/collection-page/components/desktop/CollectionPage.tsx @@ -16,6 +16,7 @@ import { CollectionPageTrackRecord } from '@audius/common/store' +import { ClientOnly } from 'components/client-only/ClientOnly' import { CollectiblesPlaylistTableColumn, CollectiblesPlaylistTable @@ -252,57 +253,59 @@ const CollectionPage = ({ contentClassName={styles.pageContent} scrollableSearch > - -
{topSection}
- {!collectionLoading && isEmpty ? ( - - ) : ( -
- + +
{topSection}
+ {!collectionLoading && isEmpty ? ( + -
- )} -
- {isOwner && (!isAlbum || isEditAlbumsEnabled) && !isNftPlaylist ? ( - <> - - - - ) : null} + ) : ( +
+ +
+ )} + + {isOwner && (!isAlbum || isEditAlbumsEnabled) && !isNftPlaylist ? ( + <> + + + + ) : null} + ) } diff --git a/packages/web/src/ssr/collection/+Page.ts b/packages/web/src/ssr/collection/+Page.ts new file mode 100644 index 00000000000..3d380236636 --- /dev/null +++ b/packages/web/src/ssr/collection/+Page.ts @@ -0,0 +1,4 @@ +// Empty page, everything is handled in +onRenderHtml and +onRenderClient +export default function render() { + return null +} diff --git a/packages/web/src/ssr/collection/+onBeforeRender.ts b/packages/web/src/ssr/collection/+onBeforeRender.ts new file mode 100644 index 00000000000..58fd2a186b0 --- /dev/null +++ b/packages/web/src/ssr/collection/+onBeforeRender.ts @@ -0,0 +1,46 @@ +import { Maybe } from '@audius/common/utils' +import type { full as FullSdk } from '@audius/sdk' +import type { PageContextServer } from 'vike/types' + +import { audiusSdk } from '../sdk' + +export type CollectionPageProps = { + collection: Maybe +} + +export async function onBeforeRender(pageContext: PageContextServer) { + const { handle, slug } = pageContext.routeParams + + try { + // NOTE: This is the playlist api, but works for both albums and playlists + const { data: collections } = + await audiusSdk.full.playlists.getPlaylistByHandleAndSlug({ + handle, + slug + }) + const collection = collections?.[0] + + return { + pageContext: { + pageProps: { + collection + } + } + } + } catch (e) { + console.error( + 'Error fetching collection for collection page SSR', + 'handle', + handle, + 'slug', + slug, + 'error', + e + ) + return { + pageContext: { + pageProps: {} + } + } + } +} diff --git a/packages/web/src/ssr/collection/route.ts b/packages/web/src/ssr/collection/route.ts new file mode 100644 index 00000000000..cbe8fb13590 --- /dev/null +++ b/packages/web/src/ssr/collection/route.ts @@ -0,0 +1,6 @@ +import { makePageRoute } from 'ssr/util' + +export default makePageRoute( + ['/@handle/playlist/@slug', '/@handle/album/@slug'], + 'Collection Page' +) diff --git a/packages/web/src/ssr/profile/+route.tsx b/packages/web/src/ssr/profile/+route.tsx index 0997b37958e..07192086646 100644 --- a/packages/web/src/ssr/profile/+route.tsx +++ b/packages/web/src/ssr/profile/+route.tsx @@ -1,3 +1,3 @@ import { makePageRoute } from 'ssr/util' -export default makePageRoute('/@handle', 'Profile Page') +export default makePageRoute(['/@handle'], 'Profile Page') diff --git a/packages/web/src/ssr/track/+route.tsx b/packages/web/src/ssr/track/+route.tsx index da1d9dda6ea..52b4c77654a 100644 --- a/packages/web/src/ssr/track/+route.tsx +++ b/packages/web/src/ssr/track/+route.tsx @@ -1,3 +1,3 @@ import { makePageRoute } from 'ssr/util' -export default makePageRoute('/@handle/@slug', 'Track Page') +export default makePageRoute(['/@handle/@slug'], 'Track Page') diff --git a/packages/web/src/ssr/util.ts b/packages/web/src/ssr/util.ts index 8ff30d99039..ecc0396cfff 100644 --- a/packages/web/src/ssr/util.ts +++ b/packages/web/src/ssr/util.ts @@ -8,31 +8,31 @@ const assetPaths = new Set(['src', 'assets', 'scripts', 'fonts', 'favicons']) const invalidPaths = new Set(['undefined']) export const makePageRoute = - (route: string, pageName?: string) => (pageContext: PageContextServer) => { - // Don't render page if the route matches any of the asset routes - if (assetPaths.has(pageContext.urlPathname.split('/')[1])) { - return false + (routes: string[], pageName?: string) => + ({ urlPathname }: PageContextServer) => { + for (let i = 0; i < routes.length; i++) { + const route = routes[i] + + // Don't render page if the route matches any of the asset, invalid, or static routes + if ( + assetPaths.has(urlPathname.split('/')[1]) || + invalidPaths.has(urlPathname.split('/')[1]) || + staticRoutes.has(urlPathname) + ) { + continue + } + + if ( + urlPathname.split('/')[route.split('/').length - 1] === 'index.css.map' + ) { + continue + } + + const result = resolveRoute(route, urlPathname) + if (result.match) { + console.info(`Rendering ${pageName ?? route}`, urlPathname) + return result + } } - - if (invalidPaths.has(pageContext.urlPathname.split('/')[1])) { - return false - } - - if ( - pageContext.urlPathname.split('/')[route.split('/').length - 1] === - 'index.css.map' - ) { - return false - } - - // Don't render page if the route matches any of the static routes - if (staticRoutes.has(pageContext.urlPathname)) { - return false - } - - const result = resolveRoute(route, pageContext.urlPathname) - if (result.match) { - console.info(`Rendering ${pageName ?? route}`, pageContext.urlPathname) - } - return result + return false }