diff --git a/src/Router.tsx b/src/Router.tsx index 046da6fd..eed0ca83 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -128,6 +128,9 @@ const TrackSlugPage = lazyWithRetry( const TrackEngagementPage = lazyWithRetry( () => import("@/pages/[username]/[trackSlug]/TrackEngagementPage"), ); +const TrackRelatedPage = lazyWithRetry( + () => import("@/pages/[username]/[trackSlug]/TrackRelatedPage"), +); // Social const NotificationsPage = lazyWithRetry( @@ -349,6 +352,7 @@ export const router = createBrowserRouter([ { path: ":trackId", element: }, { path: ":trackId/likes", element: }, { path: ":trackId/reposts", element: }, + { path: ":trackId/related", element: }, { path: "sets/:playlistSlug", element: , diff --git a/src/pages/[username]/[trackSlug]/TrackRelatedPage.tsx b/src/pages/[username]/[trackSlug]/TrackRelatedPage.tsx new file mode 100644 index 00000000..5a4277e8 --- /dev/null +++ b/src/pages/[username]/[trackSlug]/TrackRelatedPage.tsx @@ -0,0 +1,113 @@ +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { getTrackBySlug, getRelatedTracks } from "@/services/track.service"; +import type { Track } from "@/types/track"; +import Spinner from "@/components/UI/Spinner"; +import TrackList from "./components/TrackList"; +import { usePlayerStore } from "@/stores/player.store"; + +export default function TrackRelatedPage() { + const { username, trackId } = useParams<{ username: string; trackId: string }>(); + const navigate = useNavigate(); + const { setTrack: setPlayerTrack, currentTrack, isPlaying } = usePlayerStore(); + + const [track, setTrack] = useState(null); + const [relatedTracks, setRelatedTracks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!username || !trackId) return; + + async function fetchData() { + try { + const fetchedTrack = await getTrackBySlug(username!, trackId!); + setTrack(fetchedTrack); + const { tracks } = await getRelatedTracks(fetchedTrack.id); + setRelatedTracks(tracks); + } catch (err) { + console.error("Failed to load related tracks:", err); + } finally { + setLoading(false); + } + } + fetchData(); + }, [username, trackId]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!track) { + return ( +
+ Track not found. +
+ ); + } + + const effectiveArtistUsername = track.artistUsername || username || "unknown"; + const effectiveTrackSlug = track.trackSlug || trackId || ""; + + return ( +
+ {/* Header */} +
+
navigate(`/${effectiveArtistUsername}/${effectiveTrackSlug}`)} + > + {track.title} +
+
+

navigate(`/${effectiveArtistUsername}/${effectiveTrackSlug}`)} + > + {track.title} +

+

navigate(`/${effectiveArtistUsername}`)} + > + {track.artistName} +

+
+
+ + {/* Title */} +
+

Related Tracks

+
+ + {/* List */} + {relatedTracks.length === 0 ? ( +
+

+ No related tracks found. +

+
+ ) : ( + { + if (currentTrack?.id === t.id) { + usePlayerStore.getState().togglePlay(); + } else { + setPlayerTrack(t, [track, ...relatedTracks]); + } + }} + /> + )} +
+ ); +} diff --git a/src/pages/[username]/[trackSlug]/TrackSlugPage.tsx b/src/pages/[username]/[trackSlug]/TrackSlugPage.tsx index 97c215bb..b6dd7dce 100644 --- a/src/pages/[username]/[trackSlug]/TrackSlugPage.tsx +++ b/src/pages/[username]/[trackSlug]/TrackSlugPage.tsx @@ -106,7 +106,7 @@ export default function TrackSlugPage() { t, [track!, ...relatedTracks].filter(Boolean) as Track[] ); - navigate(`/${t.artistUsername}/${t.id}`); + navigate(`/${t.artistUsername || "unknown"}/${t.trackSlug || t.id}`); } }; diff --git a/src/pages/[username]/[trackSlug]/components/TrackList.tsx b/src/pages/[username]/[trackSlug]/components/TrackList.tsx index 669a9e94..d57c03a8 100644 --- a/src/pages/[username]/[trackSlug]/components/TrackList.tsx +++ b/src/pages/[username]/[trackSlug]/components/TrackList.tsx @@ -154,11 +154,13 @@ function TrackRow({ · - e.stopPropagation()} + className={`text-sm font-bold truncate hover:underline ${isCurrent ? "text-[var(--color-accent)]" : "text-[var(--color-text-hover)]"}`} > {track.title} - + {/* ── Right: hover icons OR play count ── */} diff --git a/src/pages/[username]/[trackSlug]/components/TrackSidebar.tsx b/src/pages/[username]/[trackSlug]/components/TrackSidebar.tsx index fa627291..f14b0239 100644 --- a/src/pages/[username]/[trackSlug]/components/TrackSidebar.tsx +++ b/src/pages/[username]/[trackSlug]/components/TrackSidebar.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useParams } from "react-router-dom"; import * as Tooltip from "@radix-ui/react-tooltip"; import type { Track } from "../../../../types/track"; import type { MockUser } from "../../../../services/mocks/users"; @@ -13,6 +13,9 @@ interface TrackSidebarProps { } export default function TrackSidebar({ track, featuredArtists, relatedTracks = [] }: TrackSidebarProps) { + const { username: urlUsername } = useParams<{ username: string }>(); + const artistUsername = track.artistUsername || urlUsername || "unknown"; + return (