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
14 changes: 8 additions & 6 deletions src/components/UI/MixCard/MixCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,14 @@ export default function MixCard({
</div>

{/* Subtitle */}
<p
className="text-text-secondary text-[10px] sm:text-xs truncate"
data-test="mix-card-subtitle"
>
{mix.track_count} tracks
</p>
{mix.track_count > 0 && (
<p
className="text-text-secondary text-[10px] sm:text-xs truncate"
data-test="mix-card-subtitle"
>
{mix.track_count} tracks
</p>
)}

{showPlaylistModal && (
<AddToPlaylistModal
Expand Down
54 changes: 45 additions & 9 deletions src/components/UI/PlaylistCard/PlaylistCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { getUserById } from "@/services/user.service";
import { usePlayerStore } from "@/stores/player.store";
import { useLikesStore } from "@/stores/likes.store";
import { useAuthStore } from "@/stores/auth.store";
Expand All @@ -25,6 +26,7 @@ export type PlaylistCardData = {
linkTo?: string;
onLike?: (e: React.MouseEvent) => void;
isLikedOverride?: boolean;
hideOwner?: boolean;
};

interface PlaylistCardProps {
Expand All @@ -46,6 +48,40 @@ export default function PlaylistCard({

// Local State
const [showPlaylistModal, setShowPlaylistModal] = useState(false);
const [dynamicUsername, setDynamicUsername] = useState<string>("");
const [dynamicDisplayName, setDynamicDisplayName] = useState<string>("");

useEffect(() => {
let cancelled = false;
const fallbackDisplayName = item.ownerDisplayName ?? item.ownerUsername ?? item.owner;
const fallbackUsername = item.ownerUsername ?? item.ownerDisplayName ?? item.owner;

if (!UUID_RE.test(item.owner) && !UUID_RE.test(item.ownerUsername ?? "")) {
setDynamicDisplayName(fallbackDisplayName);
setDynamicUsername(fallbackUsername);
return;
}

const idToFetch = UUID_RE.test(item.owner) ? item.owner : (item.ownerUsername ?? item.owner);
setDynamicDisplayName(fallbackDisplayName);
setDynamicUsername(fallbackUsername);

(async () => {
try {
const fetchedUser = await getUserById(idToFetch);
if (!cancelled) {
setDynamicDisplayName(fetchedUser.display_name || fallbackDisplayName);
setDynamicUsername(fetchedUser.username || fallbackUsername);
}
} catch {
// Fallbacks already set
}
})();

return () => {
cancelled = true;
};
}, [item.ownerDisplayName, item.ownerUsername, item.owner]);

// Derived State
const liked =
Expand All @@ -58,16 +94,14 @@ export default function PlaylistCard({
(currentTrack as any)?.context?.playlist_id === item.id;

const ownerDisplay =
item.ownerDisplayName ??
(UUID_RE.test(item.owner)
? (user?.displayName ?? user?.username ?? item.owner)
: item.ownerUsername ?? item.owner);

!UUID_RE.test(dynamicDisplayName)
? dynamicDisplayName
: (user?.displayName ?? user?.username ?? dynamicDisplayName);

// SoundCloud navigation format: /[username]/sets/[slug]
const playlistPath = item.isAlbumView
? `/${item.ownerUsername || item.owner}/album/${item.slug || item.id}`
: `/${item.ownerUsername || item.owner}/sets/${item.slug || item.id}`;
? `/${dynamicUsername}/album/${item.slug || item.id}`
: `/${dynamicUsername}/sets/${item.slug || item.id}`;

const handlePlayClick = (e: React.MouseEvent) => {
e.stopPropagation();
Expand Down Expand Up @@ -141,7 +175,9 @@ export default function PlaylistCard({
)}
<span className="truncate">{item.title}</span>
</p>
<p className="text-gray-400 text-xs truncate w-full">{ownerDisplay}</p>
{!item.hideOwner && (
<p className="text-gray-400 text-xs truncate w-full">{ownerDisplay}</p>
)}
</div>

{showPlaylistModal && (
Expand Down
35 changes: 33 additions & 2 deletions src/components/UI/TrackItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SharePopup from "@/pages/[username]/[trackSlug]/components/SharePopup";
import AddToPlaylistModal from "@/components/playlist/AddToPlaylistModal";
import type { Track } from "@/types/track";
import CoverImage from "@/components/UI/CoverImage";
import { getUsernameFromId } from "@/services/user.service";

interface TrackItemProps {
id: string;
Expand Down Expand Up @@ -95,8 +96,38 @@ const TrackItem: React.FC<TrackItemProps> = ({
}
}, [storeHasRepostInfo, storeReposted, reposted]);

const finalArtistSlug =
artistUsername || (artist ?? "").toLowerCase().replace(/\s+/g, "-");
const [finalArtistSlug, setFinalArtistSlug] = useState<string>(
artistUsername || (artist ?? "").toLowerCase().replace(/\s+/g, "-")
);

useEffect(() => {
let cancelled = false;
const fallbackSlug = artistUsername || (artist ?? "").toLowerCase().replace(/\s+/g, "-");
setFinalArtistSlug(fallbackSlug);

if (!artistId?.trim()) {
return () => {
cancelled = true;
};
}

(async () => {
try {
const username = await getUsernameFromId(artistId.trim());
if (!cancelled) {
setFinalArtistSlug(username || fallbackSlug);
}
} catch {
if (!cancelled) {
setFinalArtistSlug(fallbackSlug);
}
}
})();

return () => {
cancelled = true;
};
}, [artistId, artistUsername, artist]);

const trackPath = `/${finalArtistSlug}/${trackSlug ?? id}`;
const trackForActions: Track = {
Expand Down
6 changes: 3 additions & 3 deletions src/components/discover/sidebar/ArtistToolsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ const styles = {
`,
ctaButton: `
w-full
bg-purple-700 hover:bg-purple-600
bg-[#322C5B]
text-white text-xs my-2
rounded-md py-2 px-3
rounded-sm py-4 px-4
flex items-center gap-2
transition-colors duration-200
`,
Expand Down Expand Up @@ -303,7 +303,7 @@ const ArtistToolsCard = () => {
data-test="button-artist-tools-cta"
onClick={() => navigate("/premium")}
>
<i className="fa-solid fa-circle-plus text-sm"></i>
<i className=" text-[#6E5EE6] fa-solid fa-circle-plus text-sm"></i>
<span>
Unlock Artist tools
{monthlyPrice !== null
Expand Down
97 changes: 69 additions & 28 deletions src/components/library/useLibraryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,38 @@ export function useLibraryData() {
}, []);

useEffect(() => {
let cancelled = false;
getMyFollowing()
.then((items) =>
Promise.all(
items.map((f) =>
getUserById(f.id)
.then((profile) => mapFollowingToUser({ ...f, followers_count: profile.followers_count }))
.catch(() => mapFollowingToUser(f)),
),
),
items.map(async (f) => {
try {
const profile = await getUserById(f.id);
return mapFollowingToUser({ ...f, ...profile, username: profile.username || f.username });
} catch {
return mapFollowingToUser(f);
}
})
)
)
.then(setFollowingUsers)
.catch(() => setFollowingUsers([]));
.then((users) => {
if (!cancelled) setFollowingUsers(users);
})
.catch(() => {
if (!cancelled) setFollowingUsers([]);
});

return () => {
cancelled = true;
};
}, []);

useEffect(() => {
const displayName = user?.displayName ?? user?.username ?? "";
getMyPlaylistsLib()
.then((items) =>
setPlaylists(
items.map((p) => mapPlaylistToCard(p as unknown as Playlist, displayName || p.owner_user_id)),
items.map((p) => mapPlaylistToCard(p as unknown as Playlist, p.owner_user_id === user?.id ? displayName : p.owner_user_id)),
),
)
.catch(() => setPlaylists([]));
Expand Down Expand Up @@ -115,15 +127,57 @@ export function useLibraryData() {
.map((e) => e.item as Track);

const visiblePlaylists: PlaylistCardData[] = (() => {
const nonAlbumLiked = likedPlaylists.filter((p) => !p.isAlbumView);
if (playlistFilter === "Created") return playlists;
if (playlistFilter === "Liked") return nonAlbumLiked;
const mixIds = new Set(likedMixes.map((m) => m.mix_id ?? m.id));
const isExcluded = (id: string) => mixIds.has(id);

const radioMap = new Map(likedRadioTracks.map((r) => [r.playlistId, r]));

const enhancePlaylistCard = (p: PlaylistCardData): PlaylistCardData => {
const r = radioMap.get(p.id);
if (r) {
return {
...p,
title: p.title.replace(/\s+Radio$/i, ""),
hideOwner: true,
};
}
return p;
};

const nonAlbumLiked = likedPlaylists.filter((p) => !p.isAlbumView && !isExcluded(p.id)).map(enhancePlaylistCard);
const nonAlbumCreated = playlists.filter((p) => !p.isAlbumView && !isExcluded(p.id)).map(enhancePlaylistCard);

const seen = new Set<string>();
return [...playlists, ...nonAlbumLiked].filter((p) => {
const basePlaylists = [...nonAlbumCreated, ...nonAlbumLiked].filter((p) => {
if (!p.title || p.title.trim() === "") return false;
if (seen.has(p.id)) return false;
seen.add(p.id);
return true;
});

const radioPlaylists: PlaylistCardData[] = likedRadioTracks
.map((r) => ({
id: r.playlistId,
title: r.title.replace(/\s+Radio$/i, ""),
owner: user?.id ?? "",
coverUrl: r.coverImage,
isPrivate: false,
isLiked: true,
hideOwner: true,
}))
.filter((p) => {
if (seen.has(p.id)) return false;
seen.add(p.id);
return true;
});

if (playlistFilter === "Created") return nonAlbumCreated;
if (playlistFilter === "Liked") {
const likedWithTitles = nonAlbumLiked.filter((p) => p.title && p.title.trim() !== "");
return [...likedWithTitles, ...radioPlaylists];
}

return [...basePlaylists, ...radioPlaylists];
})();

const displayedAlbums = (() => {
Expand All @@ -136,23 +190,10 @@ export function useLibraryData() {
seen.add(p.playlist_id);
return true;
})
.map((p) => mapAlbumToCard(p, displayName || p.owner_user_id));
.map((p) => mapAlbumToCard(p, p.owner_user_id === user?.id ? displayName : p.owner_user_id));
})();

const displayedFollowing = (() => {
const followingIdSet = new Set(user?.following_ids ?? []);
const filtered = followingUsers.filter((u) => followingIdSet.has(u.username));
const seenUsernames = new Set(filtered.map((u) => u.username));
const synthetic: User[] = (user?.following_ids ?? [])
.filter((username) => !seenUsernames.has(username))
.map((username, i) => ({
id: String(-(i + 1)),
username,
displayName: username,
followers: 0,
}));
return [...filtered, ...synthetic];
})();
const displayedFollowing = followingUsers;

return {
recentEntries,
Expand Down
41 changes: 37 additions & 4 deletions src/components/player/StickyPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Import audioService so its module-level code runs once and sets up everything (NOT FINISHED)
import "../../services/audioService";
import { seekAudio } from "../../services/audioService";
import { useState } from "react";
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { usePlayerStore } from "../../stores/player.store";
import { useLikesStore } from "../../stores/likes.store";
Expand All @@ -14,6 +14,7 @@ import { FaHeart, FaUserCheck, FaUserPlus } from "react-icons/fa";
import { MdQueueMusic } from "react-icons/md";
import FollowButton from "../UI/FollowButton";
import CoverImage from "@/components/UI/CoverImage";
import { getUsernameFromId } from "@/services/user.service";

export default function StickyPlayer() {
const {
Expand All @@ -30,6 +31,38 @@ export default function StickyPlayer() {
const { user } = useAuthStore();

const [queueOpen, setQueueOpen] = useState(false);
const [artistUsername, setArtistUsername] = useState<string>("");

useEffect(() => {
if (!currentTrack) return;
let cancelled = false;
const fallbackUsername = (currentTrack.artistUsername ?? "unknown").trim() || "unknown";

setArtistUsername(fallbackUsername);

if (!currentTrack.artistId?.trim()) {
return () => {
cancelled = true;
};
}

(async () => {
try {
const username = await getUsernameFromId(currentTrack.artistId?.trim() ?? "1");
if (!cancelled) {
setArtistUsername(username || fallbackUsername);
}
} catch {
if (!cancelled) {
setArtistUsername(fallbackUsername);
}
}
})();

return () => {
cancelled = true;
};
}, [currentTrack?.artistId, currentTrack?.artistUsername]);

const isLiked = currentTrack ? isTrackLiked(currentTrack.id) : false;
const isFollowing = currentTrack
Expand Down Expand Up @@ -69,7 +102,7 @@ export default function StickyPlayer() {
{/* 7. Artwork + Track Info — far right */}
<div className="flex items-center gap-2 shrink-0 w-40">
<Link
to={`/${currentTrack.artistUsername || currentTrack.artistName || "share"}/${currentTrack.trackSlug || currentTrack.id}`}
to={`/${artistUsername || "share"}/${currentTrack.trackSlug || currentTrack.id}`}
>
<div className="w-10 h-10 rounded overflow-hidden shrink-0">
<CoverImage
Expand All @@ -82,14 +115,14 @@ export default function StickyPlayer() {
</Link>
<div className="flex flex-col min-w-0">
<Link
to={`/${currentTrack.artistUsername || currentTrack.artistName || "share"}/${currentTrack.trackSlug || currentTrack.id}`}
to={`/${artistUsername || "share"}/${currentTrack.trackSlug || currentTrack.id}`}
data-test="player-track-title"
className="text-white text-md font-semibold truncate leading-tight hover:text-accent transition-colors"
>
{currentTrack.title}
</Link>
<Link
to={`/${currentTrack.artistUsername || currentTrack.artistName || "share"}`}
to={`/${artistUsername}`}
data-test="player-artist-name"
className="text-text-muted text-[14px] truncate hover:text-white transition-colors leading-tight"
>
Expand Down
Loading
Loading