diff --git a/src/components/MessagingComponents/MessageBox.tsx b/src/components/MessagingComponents/MessageBox.tsx
index 37813e4c..a9ede257 100644
--- a/src/components/MessagingComponents/MessageBox.tsx
+++ b/src/components/MessagingComponents/MessageBox.tsx
@@ -195,6 +195,7 @@ export function MessageBox({
data-test="message-input"
value={value}
onChange={handleChange}
+ autoFocus
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
diff --git a/src/components/Profile/ProfileSideBar/ProfileSideBar.tsx b/src/components/Profile/ProfileSideBar/ProfileSideBar.tsx
index 878a40eb..ddefbe79 100644
--- a/src/components/Profile/ProfileSideBar/ProfileSideBar.tsx
+++ b/src/components/Profile/ProfileSideBar/ProfileSideBar.tsx
@@ -33,6 +33,8 @@ interface LikedTrack {
comments?: number;
audioUrl?: string;
artistId?: string;
+ artistUsername?: string;
+ trackSlug?: string;
}
interface ProfileSideBarProps {
diff --git a/src/components/UI/StationCard/StationCard.tsx b/src/components/UI/StationCard/StationCard.tsx
index 1ab9fd79..c188968e 100644
--- a/src/components/UI/StationCard/StationCard.tsx
+++ b/src/components/UI/StationCard/StationCard.tsx
@@ -8,6 +8,7 @@ import { useNavigate } from "react-router-dom";
import CardOverlay, {
AddToPlaylistIcon,
} from "@/components/UI/CardOverlay/CardOverlay";
+import CoverImage from "@/components/UI/CoverImage";
import AddToPlaylistModal from "@/components/playlist/AddToPlaylistModal";
import { getStationTracks } from "@/services/api/playlist/playlist.service";
// ─── Color Schemes ────────────────────────────────────────
@@ -147,41 +148,29 @@ export default function StationCard({
{/* Three artist circles */}
{/* Circle 1 — top-left, small */}
- {artists[0]?.avatarUrl ? (
-
![{artists[0].displayName]({artists[0].avatarUrl})
- ) : (
-
- )}
+
{/* Circle 2 — center, largest */}
- {artists[1]?.avatarUrl ? (
-
![{artists[1].displayName]({artists[1].avatarUrl})
- ) : (
-
- )}
+
{/* Circle 3 — bottom-right, small */}
- {artists[2]?.avatarUrl ? (
-
![{artists[2].displayName]({artists[2].avatarUrl})
- ) : (
-
- )}
+
{/* SoundCloud logo — top-right */}
diff --git a/src/components/UI/TrackItem.tsx b/src/components/UI/TrackItem.tsx
index 761f2f6f..1abbb513 100644
--- a/src/components/UI/TrackItem.tsx
+++ b/src/components/UI/TrackItem.tsx
@@ -81,7 +81,7 @@ const TrackItem: React.FC = ({
const finalArtistSlug =
artistUsername || (artist ?? "").toLowerCase().replace(/\s+/g, "-");
- const trackPath = `/discover/personalised/${trackSlug ?? ""}:${id}`;
+ const trackPath = `/${finalArtistSlug}/${trackSlug ?? id}`;
const trackForActions: Track = {
id,
title,
@@ -183,7 +183,7 @@ const TrackItem: React.FC = ({
className="w-12 h-12 cursor-pointer flex-shrink-0 bg-border rounded overflow-hidden relative"
onMouseEnter={() => setCoverHovered(true)}
onMouseLeave={() => setCoverHovered(false)}
- onClick={() => navigate(`/${finalArtistSlug}/${id}`)}
+ onClick={() => navigate(`/${finalArtistSlug}/${trackSlug ?? id}`)}
>
{
const navigate = useNavigate();
- // ✅ Early return if no children
- if (!children) {
- return null;
- }
-
return (
{/* Header */}
diff --git a/src/components/discover/sidebar/DiscoverSideBar.tsx b/src/components/discover/sidebar/DiscoverSideBar.tsx
index ac3848f5..0f58ab53 100644
--- a/src/components/discover/sidebar/DiscoverSideBar.tsx
+++ b/src/components/discover/sidebar/DiscoverSideBar.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useLayoutEffect, useRef } from "react";
+import { useState, useEffect, useRef } from "react";
import ArtistToolsCard from "./ArtistToolsCard";
import TrackItem from "@/components/UI/TrackItem";
import TrackListSection from "@/components/UI/TrackListSection/TrackListSection";
@@ -19,26 +19,6 @@ import type { Track } from "@/types/track";
const DiscoverSidebar = () => {
const asideRef = useRef
(null);
- useLayoutEffect(() => {
- const aside = asideRef.current;
- if (!aside) return;
-
- const update = () => {
- aside.style.top = `${window.innerHeight - aside.offsetHeight}px`;
- };
-
- update();
-
- const ro = new ResizeObserver(update);
- ro.observe(aside);
- window.addEventListener("resize", update, { passive: true });
-
- return () => {
- ro.disconnect();
- window.removeEventListener("resize", update);
- };
- }, []);
-
const [suggestedArtists, setSuggestedArtists] = useState<
ReturnType[]
>([]);
@@ -101,7 +81,7 @@ const DiscoverSidebar = () => {
diff --git a/src/pages/[username]/UsernamePage.tsx b/src/pages/[username]/UsernamePage.tsx
index 7da9a869..10aeb08b 100644
--- a/src/pages/[username]/UsernamePage.tsx
+++ b/src/pages/[username]/UsernamePage.tsx
@@ -238,6 +238,8 @@ export default function UsernamePage() {
plays: t.play_count,
likes: t.like_count,
artistId: t.user_id,
+ artistUsername: t.artist_username ?? "",
+ trackSlug: t.track_slug ?? t.id,
}),
);
diff --git a/src/pages/[username]/[trackSlug]/components/TrackHero.tsx b/src/pages/[username]/[trackSlug]/components/TrackHero.tsx
index 9a7a5676..26853893 100644
--- a/src/pages/[username]/[trackSlug]/components/TrackHero.tsx
+++ b/src/pages/[username]/[trackSlug]/components/TrackHero.tsx
@@ -39,16 +39,18 @@ export default function TrackHero({
const currentSec = Math.floor(currentTime);
if (currentSec !== lastSecondRef.current) {
lastSecondRef.current = currentSec;
-
const commentAtThisTime = comments.find(c => Math.floor(c.track_timestamp) === currentSec);
if (commentAtThisTime) {
setActiveComment(commentAtThisTime);
setShowFloating(true);
- const timer = setTimeout(() => setShowFloating(false), 3000);
- return () => clearTimeout(timer);
+ return; // skip hide check so the new comment isn't immediately dismissed
}
}
- }, [currentTime, comments]);
+ // Hide once the playhead has passed the comment
+ if (activeComment && currentTime > activeComment.track_timestamp + 1) {
+ setShowFloating(false);
+ }
+ }, [currentTime, comments, activeComment]);
return (
(
-

diff --git a/src/pages/[username]/tracks/TracksPage.tsx b/src/pages/[username]/tracks/TracksPage.tsx
index a796c741..7c6d8f75 100644
--- a/src/pages/[username]/tracks/TracksPage.tsx
+++ b/src/pages/[username]/tracks/TracksPage.tsx
@@ -120,7 +120,7 @@ export default function TracksPage() {
tracks.map((track) => (
navigator.clipboard.writeText(
`${window.location.origin}/${track.artistUsername}/${track.id}`,
diff --git a/src/pages/home/LandingPage.tsx b/src/pages/home/LandingPage.tsx
index e53e9587..2ba138a2 100644
--- a/src/pages/home/LandingPage.tsx
+++ b/src/pages/home/LandingPage.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import type { Track } from "@/types/track";
+import CoverImage from "@/components/UI/CoverImage";
import GuestPageFooter from "@/components/Upload/GuestPageFooter";
// ── Types ──────────────────────────────────────────────────────────────
interface SlideData {
@@ -62,17 +63,11 @@ const TrendingTrackCard = ({ track }: { track: Track }) => (
className="group block"
>
- {track.coverUrl ? (
-

- ) : (
-
- {/* Placeholder */}
-
- )}
+
{/* Hover overlay with action icons */}
diff --git a/src/pages/you/stations/StationSlugPage.tsx b/src/pages/you/stations/StationSlugPage.tsx
index 99a3bf9c..f0ef9657 100644
--- a/src/pages/you/stations/StationSlugPage.tsx
+++ b/src/pages/you/stations/StationSlugPage.tsx
@@ -169,7 +169,7 @@ export default function StationSlugPage() {
return (
()(
.entries.filter((e) => e.type === "station")
.map((e) => (e as { type: "station"; item: Station; playedAt: string }).item),
}),
- { name: "rythmify-history", version: 1 },
+ { name: "rythmify-history", version: 1, storage: createJSONStorage(() => createUserScopedStorage("history")) },
),
);
diff --git a/src/stores/player.store.ts b/src/stores/player.store.ts
index de3a8c3e..8a37cc88 100644
--- a/src/stores/player.store.ts
+++ b/src/stores/player.store.ts
@@ -30,6 +30,15 @@ function debouncedSave(userId: string, state: PlayerState) {
}, 2000);
}
+function buildShuffledOrder(queueLength: number, currentIndex: number): number[] {
+ const others = Array.from({ length: queueLength }, (_, i) => i).filter(i => i !== currentIndex);
+ for (let i = others.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [others[i], others[j]] = [others[j], others[i]];
+ }
+ return [currentIndex, ...others];
+}
+
// Dynamic import for audioService to avoid circular dependencies
let _seekAudio: ((time: number) => void) | null = null;
function getSeekAudio() {
@@ -51,6 +60,8 @@ interface PlayerState {
volume: number;
isMuted: boolean;
isShuffle: boolean;
+ shuffledIndices: number[];
+ shuffledPos: number;
repeatMode: "none" | "one" | "all";
isLiked: boolean;
isAutoplay: boolean;
@@ -97,6 +108,8 @@ export const usePlayerStore = create()(
volume: 1,
isMuted: false,
isShuffle: false,
+ shuffledIndices: [],
+ shuffledPos: 0,
repeatMode: "none",
isLiked: false,
isAutoplay: true,
@@ -106,34 +119,39 @@ export const usePlayerStore = create()(
const newQueue = queue ?? get().queue;
const index = newQueue.findIndex((t) => t.id === track.id);
const isSameTrack = get().currentTrack?.id === track.id;
+ const { isShuffle } = get();
const canPlayThisTrack = getPlaybackState(track) !== "blocked";
if (isSameTrack) {
+ const newIndex = index >= 0 ? index : get().queueIndex;
set({
currentTrack: {
...(get().currentTrack ?? track),
...track,
},
queue: newQueue,
- queueIndex: index >= 0 ? index : get().queueIndex,
- isPlaying: canPlayThisTrack,
+ queueIndex: newIndex,
+ isPlaying: true,
currentTime: startTime ?? get().currentTime,
activeSourceId: activeSourceId ?? get().activeSourceId,
+ ...(isShuffle && { shuffledIndices: buildShuffledOrder(newQueue.length, newIndex), shuffledPos: 1 }),
});
return;
}
const isFromNullState = get().currentTrack === null;
const nextTime = startTime ?? (isFromNullState ? get().currentTime : 0);
+ const newIndex = index >= 0 ? index : 0;
set({
currentTrack: track,
queue: newQueue,
- queueIndex: index >= 0 ? index : 0,
- isPlaying: canPlayThisTrack,
+ queueIndex: newIndex,
+ isPlaying: true,
currentTime: nextTime,
isLiked: false,
activeSourceId: activeSourceId ?? null,
+ ...(isShuffle && { shuffledIndices: buildShuffledOrder(newQueue.length, newIndex), shuffledPos: 1 }),
});
},
@@ -194,11 +212,13 @@ export const usePlayerStore = create()(
} as Track;
});
- const qIndex = mappedQueue.findIndex(t => String(t.id) === String(fallbackTrack.id));
-
+ const qIndex = Math.max(0, mappedQueue.findIndex(t => String(t.id) === String(fallbackTrack.id)));
+ const { isShuffle } = get();
+
set({
queue: mappedQueue,
- queueIndex: Math.max(0, qIndex)
+ queueIndex: qIndex,
+ ...(isShuffle && { shuffledIndices: buildShuffledOrder(mappedQueue.length, qIndex), shuffledPos: 1 }),
});
}
} catch (e) {
@@ -211,9 +231,9 @@ export const usePlayerStore = create()(
togglePlay: () => set((s) => ({ isPlaying: !s.isPlaying })),
next: () => {
- const { queue, queueIndex, isShuffle, isAutoplay, currentTrack } = get();
+ const { queue, queueIndex, isShuffle, shuffledIndices, shuffledPos, isAutoplay, currentTrack } = get();
if (!queue.length) return;
-
+
if (queue.length === 1 && queueIndex >= queue.length - 1 && isAutoplay && currentTrack) {
import("../services/track.service").then(async (m) => {
try {
@@ -221,7 +241,7 @@ export const usePlayerStore = create()(
if (tracks && tracks.length > 0) {
const filtered = tracks.filter((t: Track) => !queue.some(q => q.id === t.id));
if (filtered.length > 0) {
- set((s) => ({
+ set((s) => ({
queue: [...s.queue, ...filtered],
currentTrack: filtered[0],
queueIndex: s.queueIndex + 1,
@@ -234,18 +254,33 @@ export const usePlayerStore = create()(
} catch (err) {
console.error("Autoplay failed", err);
}
- const nextIndex = isShuffle ? Math.floor(Math.random() * queue.length) : (queueIndex + 1) % queue.length;
+ const nextIndex = (queueIndex + 1) % queue.length;
set({ currentTrack: queue[nextIndex], queueIndex: nextIndex, isPlaying: true, currentTime: 0 });
});
return;
}
- let nextIndex: number;
if (isShuffle) {
- nextIndex = Math.floor(Math.random() * queue.length);
- } else {
- nextIndex = (queueIndex + 1) % queue.length;
+ let indices = shuffledIndices;
+ let pos = shuffledPos;
+ if (pos >= indices.length) {
+ // All tracks played — re-shuffle for next cycle
+ indices = buildShuffledOrder(queue.length, queueIndex);
+ pos = 0;
+ }
+ const nextIndex = indices[pos];
+ set({
+ currentTrack: queue[nextIndex],
+ queueIndex: nextIndex,
+ isPlaying: true,
+ currentTime: 0,
+ shuffledIndices: indices,
+ shuffledPos: pos + 1,
+ });
+ return;
}
+
+ const nextIndex = (queueIndex + 1) % queue.length;
set({
currentTrack: queue[nextIndex],
queueIndex: nextIndex,
@@ -287,7 +322,13 @@ export const usePlayerStore = create()(
setDuration: (duration) => set({ duration }),
setVolume: (volume) => set({ volume, isMuted: volume === 0 }),
toggleMute: () => set((s) => ({ isMuted: !s.isMuted })),
- toggleShuffle: () => set((s) => ({ isShuffle: !s.isShuffle })),
+ toggleShuffle: () => set((s) => {
+ if (s.isShuffle) {
+ return { isShuffle: false, shuffledIndices: [], shuffledPos: 0 };
+ }
+ const indices = buildShuffledOrder(s.queue.length, s.queueIndex);
+ return { isShuffle: true, shuffledIndices: indices, shuffledPos: 1 };
+ }),
toggleRepeat: () =>
set((s) => {
const modes: ("none" | "all" | "one")[] = ["none", "all", "one"];