Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/common/src/hooks/useShareContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export const useShareContent = (

if (request.type === 'track' || request.type === 'contest') {
if (!track || !trackArtist) return null
if (request.type === 'contest') {
return {
type: 'contest',
track,
artist: trackArtist,
...(request.eventPermalink ? { eventPermalink: request.eventPermalink } : {})
}
}
return { type: request.type, track, artist: trackArtist }
}

Expand Down
7 changes: 5 additions & 2 deletions packages/common/src/models/Event.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Event as EventSDK } from '@audius/sdk'
import type { OverrideProperties } from 'type-fest'

import { Nullable } from '../utils/typeUtils'

import { ID } from './Identifiers'

export type Event = OverrideProperties<
Expand All @@ -11,5 +9,10 @@ export type Event = OverrideProperties<
eventId: ID
userId: ID
entityId: Nullable<ID>
/** Canonical contest permalink (e.g. /{handle}/contest-title).
* Set once the API returns a permalink field from event_routes.
* Optional - consumers fall back to deriving the URL from the
* associated track's permalink when this is absent. */
permalink?: string
}
>
6 changes: 5 additions & 1 deletion packages/common/src/store/social/tracks/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,5 +129,9 @@ export const shareTrack = createCustomAction(
*/
export const shareContest = createCustomAction(
SHARE_CONTEST,
(trackId: ID, source: ShareSource) => ({ trackId, source })
(trackId: ID, source: ShareSource, eventPermalink?: string) => ({
trackId,
source,
eventPermalink
})
)
6 changes: 5 additions & 1 deletion packages/common/src/store/ui/share-modal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ type ShareTrackContent = {
* Contest shares use the same underlying data as a track share (the
* contest is keyed off a parent track) but link to the contest page
* (`{trackPermalink}/contest`) instead of the track itself.
* When the event has its own permalink from event_routes, that value is
* passed as `eventPermalink` and used directly.
*/
type ShareContestContent = {
type: 'contest'
track: Track
artist: User
/** Canonical contest permalink from event_routes, if available. */
eventPermalink?: string
}

type ShareProfileContent = {
Expand Down Expand Up @@ -55,7 +59,7 @@ export type ShareContent =

export type ShareModalRequest =
| { type: 'track'; trackId: ID }
| { type: 'contest'; trackId: ID }
| { type: 'contest'; trackId: ID; eventPermalink?: string }
| { type: 'profile'; profileId: ID }
| { type: 'collection'; collectionId: ID }

Expand Down
10 changes: 5 additions & 5 deletions packages/mobile/src/components/share-drawer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const getContentUrl = (content: ShareContent) => {
return getTrackRoute(track, true)
}
case 'contest': {
// Contest shares point at the parent track's contest page
// `{permalink}/contest` — so sharing copies the contest URL
// rather than the underlying track.
const { track } = content
return getContestRoute(track, true)
// Contest shares point at the contest page. When the event has its
// own permalink from event_routes, use it directly; otherwise derive
// from the parent track's permalink.
const { track, eventPermalink } = content
return getContestRoute({ permalink: track.permalink, contestPermalink: eventPermalink }, true)
}
case 'profile': {
const { profile } = content
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/src/screens/contest-screen/ContestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ export const ContestScreen = () => {
shareModalUIActions.requestOpen({
type: 'contest',
trackId,
source: ShareSource.PAGE
source: ShareSource.PAGE,
...(contest?.permalink ? { eventPermalink: contest.permalink } : {})
})
)
}, [dispatch, trackId])
Expand Down
11 changes: 8 additions & 3 deletions packages/mobile/src/utils/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ export const getTrackRoute = (
}

export const getContestRoute = (
track: { permalink: string },
track: { permalink: string; contestPermalink?: string },
fullUrl = false
) => {
// Permalink shape: `/{handle}/{slug}` → contest URL is
// `/{handle}/contest/{slug}`. Mirror the web `contestPage` helper.
// If the event has its own permalink from event_routes, use it directly.
if (track.contestPermalink) {
const route = track.contestPermalink
return fullUrl ? `${AUDIUS_URL}${route}` : route
}
// Fallback: derive contest URL from the track permalink.
// `/{handle}/{slug}` → `/{handle}/contest/{slug}`.
const [, handle, ...rest] = track.permalink.split('/')
const route = `/${handle}/contest/${rest.join('/')}`
return fullUrl ? `${AUDIUS_URL}${route}` : route
Expand Down
9 changes: 9 additions & 0 deletions packages/sdk/src/sdk/api/generated/default/models/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ export interface Event {
* @memberof Event
*/
eventData: object;
/**
* Canonical contest permalink derived from event_routes.
* Optional - absent until the API begins returning this field.
* @type {string}
* @memberof Event
*/
permalink?: string;
}


Expand Down Expand Up @@ -138,6 +145,7 @@ export function EventFromJSONTyped(json: any, ignoreDiscriminator: boolean): Eve
'createdAt': json['created_at'],
'updatedAt': json['updated_at'],
'eventData': json['event_data'],
'permalink': !exists(json, 'permalink') ? undefined : json['permalink'],
};
}

Expand All @@ -160,6 +168,7 @@ export function EventToJSON(value?: Event | null): any {
'created_at': value.createdAt,
'updated_at': value.updatedAt,
'event_data': value.eventData,
'permalink': value.permalink,
};
}

7 changes: 5 additions & 2 deletions packages/web/src/common/store/social/tracks/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,15 +807,18 @@ function* watchShareContest() {
yield* takeEvery(
socialActions.SHARE_CONTEST,
function* (action: ReturnType<typeof socialActions.shareContest>) {
const { trackId } = action
const { trackId, eventPermalink } = action

const track = yield* queryTrack(trackId)
if (!track) return

const user = yield* queryUser(track.owner_id)
if (!user) return

const link = fullContestPage(track.permalink)
// Prefer event_routes permalink; fall back to deriving from track permalink.
const link = eventPermalink
? fullContestPage(eventPermalink)
: fullContestPage(track.permalink)
const share = yield* getContext('share')
share(link, formatShareText(track.title, user.name))

Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/components/share-modal/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export const ShareModal = NiceModal.create(() => {
// Contest copy-link uses the dedicated `shareContest` saga,
// which writes the contest URL (`{permalink}/contest`) to
// the clipboard rather than the track permalink.
dispatch(shareContest(content.track.track_id, source))
dispatch(
shareContest(content.track.track_id, source, content.eventPermalink)
)
break
case 'profile':
dispatch(shareUser(content.profile.user_id, source))
Expand Down
8 changes: 6 additions & 2 deletions packages/web/src/components/share-modal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ export const getXShareText = async (
case 'contest': {
const {
track: { title, permalink, track_id },
artist
artist,
eventPermalink
} = content
xText = messageConfig.contestShareText(title, getXShareHandle(artist))
link = fullContestPage(permalink)
// Prefer event_routes permalink; fall back to deriving from track permalink.
link = eventPermalink
? fullContestPage(eventPermalink)
: fullContestPage(permalink)
// Contest shares route through the parent track's analytics
// entry — there's no dedicated 'contest' event kind yet and
// the parent track is what gets credited for engagement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
if (trackId) {
dispatch(fetchTrackSucceeded({ trackId }))
}
}, [dispatch, trackId])
}, [contest?.permalink, dispatch, trackId])

useEffect(() => {
return function cleanup() {
Expand Down Expand Up @@ -405,10 +405,11 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
shareModalUIActions.requestOpen({
type: 'contest',
trackId,
source: ShareSource.PAGE
source: ShareSource.PAGE,
...(contest?.permalink ? { eventPermalink: contest.permalink } : {})
})
)
}, [dispatch, trackId])
}, [contest?.permalink, dispatch, trackId])

const renderActions = useCallback(() => {
if (!eventId) return null
Expand Down Expand Up @@ -490,7 +491,7 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
return (
<Page
title={messages.title}
canonicalUrl={fullContestPage(track.permalink)}
canonicalUrl={fullContestPage(contest?.permalink ?? track.permalink)}
variant='flush'
>
<Box
Expand Down Expand Up @@ -554,7 +555,7 @@ const ContestPage = ({ containerRef: _containerRef }: ContestPageProps) => {
return (
<Page
title={messages.title}
canonicalUrl={fullContestPage(track.permalink)}
canonicalUrl={fullContestPage(contest?.permalink ?? track.permalink)}
variant='flush'
>
{/* Content is centered to MAX_CONTENT_WIDTH and sits on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,11 @@ const ContestPage = ({
shareModalUIActions.requestOpen({
type: 'contest',
trackId,
source: ShareSource.PAGE
source: ShareSource.PAGE,
...(contest?.permalink ? { eventPermalink: contest.permalink } : {})
})
)
}, [dispatch, trackId])
}, [contest?.permalink, dispatch, trackId])

const { imageUrl: trackCoverArtUrl } = useTrackCoverArt({
trackId,
Expand Down Expand Up @@ -392,7 +393,7 @@ const ContestPage = ({
if (trackId) {
dispatch(remixesPageActions.fetchTrackSucceeded({ trackId }))
}
}, [dispatch, trackId])
}, [contest?.permalink, dispatch, trackId])

useEffect(() => {
return function cleanup() {
Expand Down Expand Up @@ -505,7 +506,7 @@ const ContestPage = ({
return (
<Page
title={messages.title}
canonicalUrl={fullContestPage(track.permalink)}
canonicalUrl={fullContestPage(contest?.permalink ?? track.permalink)}
variant='flush'
>
{/* Top section: hero banner + meta (title, CTA, deadline,
Expand Down
13 changes: 13 additions & 0 deletions packages/web/src/utils/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,16 @@ describe('CONTEST_PAGE route pattern', () => {
expect(CONTEST_PAGE).not.toBe(TRACK_REMIXES_PAGE)
})
})

describe('contestPage with event permalink passthrough', () => {
it('returns an event-routes permalink unchanged when it already contains contest', () => {
// event_routes produces slugs like /Protohype/best-remix-contest
// which already contain a 'contest' segment -- return as-is.
expect(contestPage('/Protohype/contest/best-remix-contest')).toBe(
'/Protohype/contest/best-remix-contest'
)
})
it('still rewrites a plain track permalink when no contest segment exists', () => {
expect(contestPage('/Artist/my-track')).toBe('/Artist/contest/my-track')
})
})
8 changes: 6 additions & 2 deletions packages/web/src/utils/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,13 @@ export const fullPickWinnersPage = (permalink: string) => {
}

export const contestPage = (permalink: string) => {
// Permalink shape: `/{handle}/{slug}`. Contest URL injects the literal
// If permalink already contains a "contest" segment (i.e. it came from
// event_routes rather than track_routes) return it as-is.
const parts = permalink.split('/')
if (parts.includes('contest')) return permalink
// Track permalink shape: `/{handle}/{slug}`. Inject the literal
// "contest" segment between handle and slug → `/{handle}/contest/{slug}`.
const [, handle, ...rest] = permalink.split('/')
const [, handle, ...rest] = parts
return `/${handle}/contest/${rest.join('/')}`
}
export const fullContestPage = (permalink: string) => {
Expand Down
Loading