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 - ) : ( -
- )} +
{/* Circle 2 — center, largest */}
- {artists[1]?.avatarUrl ? ( - {artists[1].displayName - ) : ( -
- )} +
{/* Circle 3 — bottom-right, small */}
- {artists[2]?.avatarUrl ? ( - {artists[2].displayName - ) : ( -
- )} +
{/* 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 (
(
- {t.title} 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 ? ( - {track.title} - ) : ( -
- {/* 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"];