From ec95144d23bfbd75a32c7c597aa1e53033e1900d Mon Sep 17 00:00:00 2001 From: SLASH27KushaL Date: Thu, 9 Oct 2025 02:44:37 +0530 Subject: [PATCH 1/5] biography fetaure --- package.json | 1 + src/Api.js | 21 ++++- src/components/Artist/ArtistBio.jsx | 39 +++++++++ src/components/Artist/artist.jsx | 129 +++++++++------------------- src/zustand/store.js | 2 +- 5 files changed, 99 insertions(+), 93 deletions(-) create mode 100644 src/components/Artist/ArtistBio.jsx diff --git a/package.json b/package.json index 1a0aadd..5df5567 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "prepare": "husky" }, "dependencies": { + "@google/generative-ai": "^0.24.1", "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-icons": "^1.3.2", diff --git a/src/Api.js b/src/Api.js index 3e9e5ed..ab76845 100644 --- a/src/Api.js +++ b/src/Api.js @@ -9,16 +9,12 @@ Api.interceptors.response.use( (response) => response, (error) => { if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx toast.error(`Error: ${error.response.status} - ${error.response.statusText}`); console.error("API Error:", error.response.data); } else if (error.request) { - // The request was made but no response was received toast.error("Error: No response from server. Please check your internet connection."); console.error("API Error: No response received", error.request); } else { - // Something happened in setting up the request that triggered an Error toast.error("Error: Something went wrong with the request."); console.error("API Error:", error.message); } @@ -42,6 +38,7 @@ import { setDoc, } from "firebase/firestore"; import { app, db } from "./Auth/firebase"; + export const fetchFireStore = (setPlaylist, setLikedSongs) => { let auth = getAuth(app); onAuthStateChanged(auth, async (user) => { @@ -219,3 +216,19 @@ export async function fetchSongsByIds(songIds) { return { success: false, data: [] }; } } + +export async function fetchArtistBio(artistName) { + try { + const apiKey = import.meta.env.VITE_THEAUDIODB_API_KEY; + const url = `https://www.theaudiodb.com/api/v1/json/${apiKey}/search.php?s=${artistName}`; + const response = await axios.get(url); + + if (response.data && response.data.artists) { + return response.data.artists[0].strBiographyEN; + } + return null; + } catch (error) { + console.error("Error fetching artist biography:", error); + return null; + } +} diff --git a/src/components/Artist/ArtistBio.jsx b/src/components/Artist/ArtistBio.jsx new file mode 100644 index 0000000..ecfc640 --- /dev/null +++ b/src/components/Artist/ArtistBio.jsx @@ -0,0 +1,39 @@ +import PropTypes from "prop-types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const ArtistBio = ({ artistData, bioText }) => { + // Use the fetched bioText. If it's not available, fall back to the placeholder. + const bio = + bioText || + `A celebrated artist known for a unique blend of genres, ${artistData?.name} has captivated audiences worldwide with their soulful melodies and profound lyrics. Rising from humble beginnings, their passion for music has led them on a journey of sonic exploration, resulting in a discography that is both timeless and innovative.`; + + return ( +
+ + + About {artistData?.name} + + + {/* The 'whitespace-pre-wrap' class helps preserve formatting like newlines from the API */} +

{bio}

+
+
+
+ ); +}; + +// Add prop validation to satisfy the linter +ArtistBio.propTypes = { + artistData: PropTypes.shape({ + name: PropTypes.string, + bio: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + text: PropTypes.string, + }), + ]), + }), + bioText: PropTypes.string, +}; + +export default ArtistBio; diff --git a/src/components/Artist/artist.jsx b/src/components/Artist/artist.jsx index ee6e56d..5293806 100644 --- a/src/components/Artist/artist.jsx +++ b/src/components/Artist/artist.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; -import Api from "../../Api"; +import Api, { fetchArtistBio } from "../../Api"; import { getImageColors } from "../color/ColorGenrator"; import { ScrollArea } from "../ui/scroll-area"; import { useStore } from "../../zustand/store"; @@ -8,9 +8,11 @@ import { Play, Pause, Share2, Shuffle } from "lucide-react"; import Menu from "../Menu"; import Like from "../ui/Like"; import { toast } from "sonner"; +import ArtistBio from "./ArtistBio"; function Artist() { - const [data, setData] = useState(); + const [data, setData] = useState(null); + const [bio, setBio] = useState(""); const [bgColor, setBgColor] = useState(); const [isLoading, setIsLoading] = useState(true); const [imageLoaded, setImageLoaded] = useState(false); @@ -20,20 +22,13 @@ function Artist() { useStore(); const artistId = url.search.split("=")[1]; - // Function to calculate luminance and determine text color const getTextColor = (rgbColor) => { - // Extract RGB values from rgb(r, g, b) string - const match = rgbColor.match(/rgb$$(\d+),\s*(\d+),\s*(\d+)$$/); + const match = rgbColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (!match) return "white"; - const r = Number.parseInt(match[1]); const g = Number.parseInt(match[2]); const b = Number.parseInt(match[3]); - - // Calculate relative luminance (WCAG formula) const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - - // If luminance > 0.6, use dark text, otherwise use white text return luminance > 0.6 ? "dark" : "white"; }; @@ -42,19 +37,25 @@ function Artist() { try { setIsLoading(true); const res = await Api(`/api/artists/${artistId}`); - setData(res.data.data); - setQueue(res.data.data.topSongs); + const artistData = res.data.data; + setData(artistData); + setQueue(artistData.topSongs); - // Generate colors from the artist image - getImageColors(res.data.data.image[2].url).then(({ averageColor, dominantColor }) => { + if (artistData.name) { + const artistBio = await fetchArtistBio(artistData.name); + if (artistBio) { + setBio(artistBio); + } + } + + getImageColors(artistData.image[2].url).then(({ averageColor, dominantColor }) => { setBgColor({ bg1: averageColor, bg2: dominantColor }); - // Determine text color based on background brightness setTextColor(getTextColor(dominantColor)); }); } catch (error) { toast.error("Failed to load artist data."); console.error("Error fetching artist data:", error); - setData(null); // Ensure data is null on error to trigger "Artist not found" UI + setData(null); } finally { setIsLoading(false); } @@ -67,21 +68,13 @@ function Artist() { setMusicId(song.id); setArtistId(artistId); } else { - if (isPlaying) { - setIsPlaying(false); - } else { - setIsPlaying(true); - } + setIsPlaying(!isPlaying); } } function handlePlayAll() { - if (currentArtistId == artistId) { - if (isPlaying) { - setIsPlaying(false); - } else { - setIsPlaying(true); - } + if (currentArtistId === artistId) { + setIsPlaying(!isPlaying); } else { if (data.topSongs?.length > 0) { setQueue(data.topSongs); @@ -126,7 +119,6 @@ function Artist() { return (
- {/* Hero Section */}
- {/* Dark/Light overlay for better text contrast */}
- {/* Artist Image */}
- {/* Artist Info */}

- {/* Action Buttons */}
- {/* Top Songs Section */}

Popular

- {/* Songs List - Improved Mobile Layout */}
{data.topSongs.map((song, index) => (
handleSongClick(song)} > - {/* Mobile Layout */}
- {/* Track Number / Play Button */}
{index + 1} @@ -259,9 +241,7 @@ function Artist() { e.stopPropagation(); handleSongClick(song); }} - className={`w-8 h-8 flex items-center justify-center transition-all duration-200 ${ - song.id === musicId ? "block" : "hidden group-hover:block" - }`} + className={`w-8 h-8 flex items-center justify-center transition-all duration-200 ${song.id === musicId ? "block" : "hidden group-hover:block"}`} > {isPlaying && song.id === musicId ? (
- - {/* Song Image */}
- - {/* Song Info - More space on mobile */}

- - {/* Like Button - Mobile */}
- + {" "} + {" "}
- - {/* Menu Button - Always visible on mobile for better UX */} -
- +
e.stopPropagation()} + className="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full hover:bg-muted transition-colors" + > +
- - {/* Desktop/Tablet Layout */}
- {/* Track Number / Play Button */}
{index + 1} @@ -339,9 +303,7 @@ function Artist() { e.stopPropagation(); handleSongClick(song); }} - className={`w-6 h-6 flex items-center justify-center transition-all duration-200 ${ - song.id === musicId ? "block" : "hidden group-hover:block" - }`} + className={`w-6 h-6 flex items-center justify-center transition-all duration-200 ${song.id === musicId ? "block" : "hidden group-hover:block"}`} > {isPlaying && song.id === musicId ? (
- - {/* Song Image */}
- - {/* Song Info */}

{data.name}

- - {/* Duration */}
{Math.floor(song.duration / 60)}: {(song.duration % 60).toString().padStart(2, "0")}
-
- + {" "} + {" "}
- - {/* Menu Button */} -
- +
e.stopPropagation()} + className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full hover:bg-muted transition-colors opacity-0 group-hover:opacity-100" + > +
@@ -402,6 +354,7 @@ function Artist() { ))}
+ {data && }
diff --git a/src/zustand/store.js b/src/zustand/store.js index 7967c17..d1299aa 100644 --- a/src/zustand/store.js +++ b/src/zustand/store.js @@ -4,6 +4,7 @@ import Api from "../Api"; // Constants const INITIAL_SONGS_LIMIT = 5; const SUGGESTIONS_LIMIT = 30; + export const useFetch = create((set) => ({ songs: null, albums: null, @@ -235,7 +236,6 @@ export const useStore = create((set, get) => ({ const newQueue = [...state.queue]; newQueue.splice(insertPos, 0, song); - // Update shuffled queue if shuffle is active let newShuffledQueue = state.shuffledQueue; if (state.shuffle) { newShuffledQueue = [...state.shuffledQueue, song].sort(() => Math.random() - 0.5); From 45f528fe3e2f2a39056a0b929bd8251525d0fc69 Mon Sep 17 00:00:00 2001 From: SLASH27KushaL Date: Thu, 9 Oct 2025 14:13:47 +0530 Subject: [PATCH 2/5] updates --- src/components/Artist/artist.jsx | 96 +++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/src/components/Artist/artist.jsx b/src/components/Artist/artist.jsx index 71a32ae..5ef1f15 100644 --- a/src/components/Artist/artist.jsx +++ b/src/components/Artist/artist.jsx @@ -8,6 +8,7 @@ import { Play, Pause, Share2, Shuffle } from "lucide-react"; import Menu from "../Menu"; import Like from "../ui/Like"; import { toast } from "sonner"; +import { useSongHandlers } from "@/hooks/SongCustomHooks"; import ArtistBio from "./ArtistBio"; function Artist() { @@ -18,11 +19,12 @@ function Artist() { const [imageLoaded, setImageLoaded] = useState(false); const [textColor, setTextColor] = useState("white"); const url = useLocation(); - const { setMusicId, musicId, isPlaying, setIsPlaying, setQueue, currentArtistId, setArtistId } = - useStore(); + // FIX: Removed `setArtistId` as it's not used directly in this component + const { musicId, isPlaying, setIsPlaying, setQueue, currentArtistId } = useStore(); const artistId = url.search.split("=")[1]; const { handleSongClick } = useSongHandlers(); + // Function to calculate luminance and determine text color const getTextColor = (rgbColor) => { const match = rgbColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); if (!match) return "white"; @@ -42,6 +44,7 @@ function Artist() { setData(artistData); setQueue(artistData.topSongs); + // Fetch artist biography if (artistData.name) { const artistBio = await fetchArtistBio(artistData.name); if (artistBio) { @@ -49,6 +52,7 @@ function Artist() { } } + // Generate colors from the artist image getImageColors(artistData.image[2].url).then(({ averageColor, dominantColor }) => { setBgColor({ bg1: averageColor, bg2: dominantColor }); setTextColor(getTextColor(dominantColor)); @@ -64,24 +68,13 @@ function Artist() { fetching(); }, [artistId, setQueue]); - function handleSongClick(song) { - if (song.id !== musicId) { - setMusicId(song.id); - setArtistId(artistId); - } else { - setIsPlaying(!isPlaying); - } - } - function handlePlayAll() { if (currentArtistId === artistId) { setIsPlaying(!isPlaying); } else { - if (data.topSongs?.length > 0) { + if (data?.topSongs?.length > 0) { setQueue(data.topSongs); - setMusicId(data.topSongs[0].id); - setIsPlaying(true); - setArtistId(artistId); + handleSongClick(data.topSongs[0], { artistId: artistId, play: true }); } } } @@ -89,9 +82,8 @@ function Artist() { function handleShuffle() { if (data?.topSongs?.length > 0) { const randomIndex = Math.floor(Math.random() * data.topSongs.length); - setMusicId(data.topSongs[randomIndex].id); - setIsPlaying(true); - setArtistId(artistId); + const randomSong = data.topSongs[randomIndex]; + handleSongClick(randomSong, { artistId: artistId, play: true }); } } @@ -120,6 +112,7 @@ function Artist() { return (
+ {/* Hero Section */}
+ {/* Dark/Light overlay for better text contrast */}
+ {/* Artist Image */}
+ {/* Artist Info */}

+ {/* Action Buttons */}
+ {/* Top Songs & Bio Section */}

Popular

+ {/* Songs List */}
{data.topSongs.map((song, index) => (
handleSongClick(song, { artistId: artistId })} > + {/* Mobile Layout */}
+ {/* Track Number / Play Button */}
{index + 1} @@ -242,21 +245,25 @@ function Artist() { e.stopPropagation(); handleSongClick(song, { artistId: artistId }); }} - className={`w-8 h-8 flex items-center justify-center transition-all duration-200 ${song.id === musicId ? "block" : "hidden group-hover:block"}`} + className={`w-8 h-8 flex items-center justify-center transition-all duration-200 ${ + song.id === musicId ? "block" : "hidden group-hover:block" + }`} > {isPlaying && song.id === musicId ? ( { e.stopPropagation(); setIsPlaying(false); }} /> ) : ( - + )}
+ + {/* Song Image */}
+ + {/* Song Info */}

+ + {/* Like Button */}
- {" "} - {" "} +
+ + {/* Menu Button */}
e.stopPropagation()} className="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full hover:bg-muted transition-colors" @@ -291,11 +305,16 @@ function Artist() {
+ + {/* Desktop/Tablet Layout */}
+ {/* Track Number / Play Button */}
{index + 1} @@ -304,21 +323,25 @@ function Artist() { e.stopPropagation(); handleSongClick(song, { artistId: artistId }); }} - className={`w-6 h-6 flex items-center justify-center transition-all duration-200 ${song.id === musicId ? "block" : "hidden group-hover:block"}`} + className={`w-6 h-6 flex items-center justify-center transition-all duration-200 ${ + song.id === musicId ? "block" : "hidden group-hover:block" + }`} > {isPlaying && song.id === musicId ? ( { e.stopPropagation(); setIsPlaying(false); }} /> ) : ( - + )}
+ + {/* Song Image */}
+ + {/* Song Info */}

{song.name}

{data.name}

+ + {/* Duration */}
{Math.floor(song.duration / 60)}: {(song.duration % 60).toString().padStart(2, "0")}
+ + {/* Like Button */}
- {" "} - {" "} +
+ + {/* Menu Button */}
e.stopPropagation()} className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full hover:bg-muted transition-colors opacity-0 group-hover:opacity-100" From 68d93ea334c976821234d64eb028ced6b43131b0 Mon Sep 17 00:00:00 2001 From: SLASH27KushaL Date: Tue, 14 Oct 2025 00:18:44 +0530 Subject: [PATCH 3/5] conflicts resolved --- src/components/Artist/artist.jsx | 288 +++++++----------------- src/zustand/store.js | 372 +++++++++++++++++++------------ 2 files changed, 317 insertions(+), 343 deletions(-) diff --git a/src/components/Artist/artist.jsx b/src/components/Artist/artist.jsx index 5ef1f15..e257518 100644 --- a/src/components/Artist/artist.jsx +++ b/src/components/Artist/artist.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; -import Api, { fetchArtistBio } from "../../Api"; +import Api, { fetchArtistBio } from "../../Api"; // Assumes fetchArtistBio is exported from Api.js import { getImageColors } from "../color/ColorGenrator"; import { ScrollArea } from "../ui/scroll-area"; import { useStore } from "../../zustand/store"; @@ -8,43 +8,39 @@ import { Play, Pause, Share2, Shuffle } from "lucide-react"; import Menu from "../Menu"; import Like from "../ui/Like"; import { toast } from "sonner"; -import { useSongHandlers } from "@/hooks/SongCustomHooks"; -import ArtistBio from "./ArtistBio"; +import { useSongHandlers, getTextColor, usePlayAll, useShuffle } from "@/hooks/SongCustomHooks"; +import ArtistBio from "./ArtistBio"; // Import the new ArtistBio component function Artist() { const [data, setData] = useState(null); - const [bio, setBio] = useState(""); + const [bio, setBio] = useState(""); // New state for the artist's biography const [bgColor, setBgColor] = useState(); const [isLoading, setIsLoading] = useState(true); const [imageLoaded, setImageLoaded] = useState(false); const [textColor, setTextColor] = useState("white"); const url = useLocation(); - // FIX: Removed `setArtistId` as it's not used directly in this component - const { musicId, isPlaying, setIsPlaying, setQueue, currentArtistId } = useStore(); + const { musicId, isPlaying, setIsPlaying, setCurrentList, currentArtistId } = useStore(); const artistId = url.search.split("=")[1]; - const { handleSongClick } = useSongHandlers(); - // Function to calculate luminance and determine text color - const getTextColor = (rgbColor) => { - const match = rgbColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); - if (!match) return "white"; - const r = Number.parseInt(match[1]); - const g = Number.parseInt(match[2]); - const b = Number.parseInt(match[3]); - const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - return luminance > 0.6 ? "dark" : "white"; - }; + // Using custom hooks for consistency + const { handleSongClick } = useSongHandlers(); + const handlePlayAll = usePlayAll(artistId, data?.topSongs, "artist"); + const handleShuffle = useShuffle(artistId, data?.topSongs, "artist"); useEffect(() => { const fetching = async () => { + if (!artistId) { + setIsLoading(false); + return; + } try { setIsLoading(true); const res = await Api(`/api/artists/${artistId}`); const artistData = res.data.data; setData(artistData); - setQueue(artistData.topSongs); + setCurrentList(artistData.topSongs); - // Fetch artist biography + // Fetch artist biography (from your PR) if (artistData.name) { const artistBio = await fetchArtistBio(artistData.name); if (artistBio) { @@ -66,26 +62,7 @@ function Artist() { } }; fetching(); - }, [artistId, setQueue]); - - function handlePlayAll() { - if (currentArtistId === artistId) { - setIsPlaying(!isPlaying); - } else { - if (data?.topSongs?.length > 0) { - setQueue(data.topSongs); - handleSongClick(data.topSongs[0], { artistId: artistId, play: true }); - } - } - } - - function handleShuffle() { - if (data?.topSongs?.length > 0) { - const randomIndex = Math.floor(Math.random() * data.topSongs.length); - const randomSong = data.topSongs[randomIndex]; - handleSongClick(randomSong, { artistId: artistId, play: true }); - } - } + }, [artistId, setCurrentList]); if (isLoading) { return ( @@ -121,7 +98,6 @@ function Artist() { : "linear-gradient(180deg, hsl(var(--muted)) 0%, transparent 100%)", }} > - {/* Dark/Light overlay for better text contrast */}
- {/* Top Songs & Bio Section */} + {/* Top Songs Section */}
-
-
-

Popular

-
- - {/* Songs List */} -
- {data.topSongs.map((song, index) => ( -
handleSongClick(song, { artistId: artistId })} - > - {/* Mobile Layout */} -
-
- {/* Track Number / Play Button */} -
- - {index + 1} - - -
- - {/* Song Image */} -
- {song.name} -
- - {/* Song Info */} -
-

- {song.name} -

-
- - {/* Like Button */} -
- -
- - {/* Menu Button */} -
e.stopPropagation()} - className="flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full hover:bg-muted transition-colors" - > - -
-
+ ) : ( + + )} +
- - {/* Desktop/Tablet Layout */} -
-
- {/* Track Number / Play Button */} -
- - {index + 1} - - -
- - {/* Song Image */} -
- {song.name} -
- - {/* Song Info */} -
-

- {song.name} -

-

{data.name}

-
- - {/* Duration */} -
- {Math.floor(song.duration / 60)}: - {(song.duration % 60).toString().padStart(2, "0")} -
- - {/* Like Button */} -
- -
- - {/* Menu Button */} -
e.stopPropagation()} - className="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full hover:bg-muted transition-colors opacity-0 group-hover:opacity-100" - > - -
+ {/* Song Image */} + {song.name} + {/* Song Info */} +
+

+ {song.name} +

+

+ {data.name} +

+
+ {/* Duration (Desktop) */} +
+ {`${Math.floor(song.duration / 60)}:${(song.duration % 60).toString().padStart(2, "0")}`} +
+ {/* Like & Menu Buttons */} +
+ +
e.stopPropagation()} + className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-muted transition-colors sm:opacity-0 sm:group-hover:opacity-100" + > +
- ))} -
+
+ ))}
+ {/* Artist Biography Section */} {data && }
diff --git a/src/zustand/store.js b/src/zustand/store.js index d1299aa..495cfdf 100644 --- a/src/zustand/store.js +++ b/src/zustand/store.js @@ -1,9 +1,10 @@ import { create } from "zustand"; import Api from "../Api"; +import { persistMusicState, restoreMusicState } from "./persistHelpers"; -// Constants -const INITIAL_SONGS_LIMIT = 5; -const SUGGESTIONS_LIMIT = 30; +const INITIAL_SONGS_LIMIT = 1; +const SUGGESTIONS_LIMIT = 9; +const BASE_JIOSAAVAN_URL = "https://jiosaavan-api-2-harsh-patel.vercel.app"; export const useFetch = create((set) => ({ songs: null, @@ -20,7 +21,7 @@ export const useFetch = create((set) => ({ const initialSongs = res.data.data.results.slice(0, INITIAL_SONGS_LIMIT); const suggestionsRes = await fetch( - `https://jiosaavan-api-2-harsh-patel.vercel.app/api/songs/${topResult.id}/suggestions?limit=${SUGGESTIONS_LIMIT}` + `${BASE_JIOSAAVAN_URL}/api/songs/${topResult.id}/suggestions?limit=${SUGGESTIONS_LIMIT}` ); const suggestionsData = await suggestionsRes.json(); @@ -81,8 +82,10 @@ export const useStore = create((set, get) => ({ currentSong: null, isPlaying: false, queue: [], + songList: [], + currentList: [], + previous: [], likedSongs: [], - currentIndex: 0, volume: typeof window !== "undefined" ? localStorage.getItem("volume") === null @@ -95,56 +98,44 @@ export const useStore = create((set, get) => ({ played: 0, duration: 0, shuffleHistory: [], - shuffleHistoryIndex: -1, shuffledQueue: [], + autoPlay: false, + playedSongIds: new Set(), setPlaylist: (prop) => set((state) => ({ playlist: [...state.playlist, prop], })), emptyPlaylist: () => set({ playlist: [] }), + setAutoPlay: (prop) => set({ autoPlay: prop }), setIsUser: (prop) => set({ isUser: prop }), setDialogOpen: (prop) => set({ dialogOpen: prop }), setMusicId: (id) => { - const { queue, shuffle, shuffleHistory, shuffleHistoryIndex } = get(); - const newIndex = queue.findIndex((song) => song.id === id); - const currentSong = queue.find((song) => song.id === id); - - if (!currentSong) { - console.warn(`Song with id ${id} not found in queue`); - return; - } - - if (shuffle) { - let newShuffleHistory = [...shuffleHistory]; - let newShuffleHistoryIndex = shuffleHistoryIndex; - - newShuffleHistory = [...shuffleHistory, currentSong]; - newShuffleHistoryIndex = newShuffleHistory.length - 1; - - set({ - musicId: id, - currentAlbumId: null, - currentArtistId: null, - currentSong: currentSong, - currentIndex: newIndex >= 0 ? newIndex : 0, - played: 0, - isPlaying: false, - shuffleHistory: newShuffleHistory, - shuffleHistoryIndex: newShuffleHistoryIndex, - }); - } else { - set({ - musicId: id, - currentAlbumId: null, - currentArtistId: null, - currentSong: currentSong, - currentIndex: newIndex >= 0 ? newIndex : 0, - played: 0, - isPlaying: false, - }); - } + const { shuffle, currentList } = get(); + + if (!Array.isArray(currentList) || currentList.length === 0) return; + + const currentSong = currentList.find((s) => s.id === id); + if (!currentSong) return console.warn(`Song with id ${id} not found`); + const newQueue = currentList.slice(currentList.indexOf(currentSong) + 1); + const newState = { + musicId: id, + currentSong, + queue: newQueue, + songList: currentList, + currentAlbumId: null, + currentArtistId: null, + played: 0, + isPlaying: false, + autoPlay: false, + shuffleHistory: shuffle ? [currentSong] : [], + shuffledQueue: shuffle ? [...newQueue].sort(() => Math.random() - 0.5) : [], + previous: [], + playedSongIds: new Set([id]), + }; + + set(newState); }, setAlbumId: (id) => set({ currentAlbumId: id }), @@ -161,16 +152,20 @@ export const useStore = create((set, get) => ({ : shuffledQueue; const preservedHistory = shuffle && currentSong ? [currentSong] : []; - const preservedIndex = shuffle && currentSong ? 0 : -1; set({ queue: prop, - currentIndex: 0, shuffledQueue: filteredShuffledQueue, shuffleHistory: preservedHistory, - shuffleHistoryIndex: preservedIndex, }); }, + + setCurrentList: (prop) => { + set({ + currentList: prop, + }); + }, + setLikedSongs: (songs) => set({ likedSongs: songs }), addLikedSong: (songId) => set((state) => ({ @@ -190,7 +185,7 @@ export const useStore = create((set, get) => ({ }, setMuted: (muted) => set({ muted }), setShuffle: (shuffle) => { - const { queue, currentSong } = get(); + const { queue, currentSong, previous, shuffleHistory } = get(); if (shuffle) { const shuffledQueue = [...queue].sort(() => Math.random() - 0.5); @@ -198,26 +193,24 @@ export const useStore = create((set, get) => ({ ? shuffledQueue.filter((song) => song.id !== currentSong.id) : shuffledQueue; - let shuffleHistory = []; - let shuffleHistoryIndex = -1; - + // Migrate previous history when turning shuffle ON + let newShuffleHistory = [...previous]; if (currentSong) { - shuffleHistory = [currentSong]; - shuffleHistoryIndex = 0; + newShuffleHistory = [...previous, currentSong]; } set({ shuffle: true, shuffledQueue: filteredShuffledQueue, - shuffleHistory: shuffleHistory, - shuffleHistoryIndex: shuffleHistoryIndex, + shuffleHistory: newShuffleHistory, }); } else { + // Migrate shuffle history back to previous when turning shuffle OFF set({ shuffle: false, shuffledQueue: [], + previous: shuffleHistory, shuffleHistory: [], - shuffleHistoryIndex: -1, }); } }, @@ -225,17 +218,35 @@ export const useStore = create((set, get) => ({ setPlayed: (played) => set({ played }), setDuration: (duration) => set({ duration }), - addToQueue: (song) => - set((state) => ({ - queue: [...state.queue, song], - })), + addToQueue: (song) => { + const { autoPlay, shuffle } = get(); + if (autoPlay) { + set((state) => ({ + queue: [song], + autoPlay: false, + shuffledQueue: shuffle ? [song] : state.shuffledQueue, + })); + return; + } else { + set((state) => { + const nextQueue = [...state.queue, song]; + const nextShuffled = shuffle + ? [...state.shuffledQueue, song].sort(() => Math.random() - 0.5) + : state.shuffledQueue; + return { + queue: nextQueue, + shuffledQueue: nextShuffled, + }; + }); + } + }, addToQueueNext: (song) => set((state) => { - const insertPos = Math.min(state.currentIndex + 1, state.queue.length); const newQueue = [...state.queue]; - newQueue.splice(insertPos, 0, song); + newQueue.unshift(song); + // Update shuffled queue if shuffle is active let newShuffledQueue = state.shuffledQueue; if (state.shuffle) { newShuffledQueue = [...state.shuffledQueue, song].sort(() => Math.random() - 0.5); @@ -250,116 +261,186 @@ export const useStore = create((set, get) => ({ playNext: () => { const { queue, - currentIndex, + songList, shuffle, repeat, shuffleHistory, - shuffleHistoryIndex, shuffledQueue, + previous, + currentSong, } = get(); - if (queue.length === 0) return; if (repeat === "one") { set({ played: 0 }); return; } - if (shuffle) { - const nextHistoryIndex = shuffleHistoryIndex + 1; - - if (nextHistoryIndex < shuffleHistory.length) { - const nextSong = shuffleHistory[nextHistoryIndex]; - set({ - shuffleHistoryIndex: nextHistoryIndex, - musicId: nextSong.id, - currentSong: nextSong, + if (queue.length === 0) { + if (repeat === "all") { + if (!songList || songList.length === 0) return; + set((state) => ({ + musicId: songList[0].id, + currentSong: songList[0], + queue: songList.slice(1), played: 0, isPlaying: false, - }); + previous: [...previous, currentSong], + playedSongIds: new Set([...state.playedSongIds, songList[0].id]), + })); } else { - if (shuffledQueue.length === 0) { - if (repeat === "all") { - const newShuffledQueue = [...queue].sort(() => Math.random() - 0.5); - const firstSong = newShuffledQueue[0]; - const remainingQueue = newShuffledQueue.slice(1); - - set({ - shuffledQueue: remainingQueue, - shuffleHistory: [firstSong], - shuffleHistoryIndex: 0, - musicId: firstSong.id, - currentSong: firstSong, - played: 0, - isPlaying: false, - }); - } - return; + const { currentSong, currentAlbumId, currentArtistId, playedSongIds } = get(); + + if (currentAlbumId === null && currentArtistId === null && currentSong?.id) { + (async () => { + try { + const res = await fetch( + `${BASE_JIOSAAVAN_URL}/api/songs/${currentSong.id}/suggestions?limit=10` + ); + const data = await res.json(); + const suggested = data?.data || []; + + // Filter out songs that have already been played + const filteredSuggestions = suggested.filter((song) => !playedSongIds.has(song.id)); + + if (filteredSuggestions.length > 0) { + const [first, ...rest] = filteredSuggestions; + set((state) => ({ + queue: rest, + shuffledQueue: state.shuffle + ? rest.slice().sort(() => Math.random() - 0.5) + : state.shuffledQueue, + shuffleHistory: + state.shuffle && state.currentSong + ? [...state.shuffleHistory, state.currentSong] + : state.shuffleHistory, + musicId: first.id, + currentSong: first, + played: 0, + isPlaying: true, + autoPlay: true, + previous: state.currentSong + ? [...state.previous, state.currentSong] + : state.previous, + playedSongIds: new Set([...state.playedSongIds, first.id]), + })); + } else if (suggested.length > 0) { + // If all suggestions were already played, clear history and use fresh suggestions + const [first, ...rest] = suggested; + set((state) => ({ + queue: rest, + shuffledQueue: state.shuffle + ? rest.slice().sort(() => Math.random() - 0.5) + : state.shuffledQueue, + shuffleHistory: + state.shuffle && state.currentSong + ? [...state.shuffleHistory, state.currentSong] + : state.shuffleHistory, + musicId: first.id, + currentSong: first, + played: 0, + isPlaying: true, + autoPlay: true, + previous: state.currentSong + ? [...state.previous, state.currentSong] + : state.previous, + playedSongIds: new Set([first.id]), + })); + } + } catch (error) { + console.error("Failed to fetch suggested songs:", error); + } + })(); } + return; + } + } - const randomIndex = Math.floor(Math.random() * shuffledQueue.length); - const nextSong = shuffledQueue[randomIndex]; + if (shuffle) { + if (shuffledQueue.length === 0) { + if (repeat === "all") { + const newShuffledQueue = [...songList].sort(() => Math.random() - 0.5); + const firstSong = newShuffledQueue[0]; + const remainingQueue = newShuffledQueue.slice(1); + + set((state) => ({ + shuffledQueue: remainingQueue, + shuffleHistory: [...shuffleHistory, firstSong], + musicId: firstSong.id, + currentSong: firstSong, + played: 0, + isPlaying: false, + playedSongIds: new Set([...state.playedSongIds, firstSong.id]), + })); + } + return; + } - const newShuffleHistory = [...shuffleHistory, nextSong]; - const newShuffledQueue = shuffledQueue.filter((_, index) => index !== randomIndex); + let nextSong = shuffledQueue[0]; - set({ - shuffleHistory: newShuffleHistory, - shuffleHistoryIndex: newShuffleHistory.length - 1, - shuffledQueue: newShuffledQueue, - musicId: nextSong.id, - currentSong: nextSong, - played: 0, - isPlaying: false, - }); - } + set((state) => ({ + shuffleHistory: [...shuffleHistory, currentSong], + shuffledQueue: shuffledQueue.slice(1), + musicId: nextSong.id, + currentSong: nextSong, + played: 0, + isPlaying: false, + playedSongIds: new Set([...state.playedSongIds, nextSong.id]), + })); } else { - let nextIndex = currentIndex + 1; - if (nextIndex >= queue.length) { - if (repeat === "all") nextIndex = 0; - else return; - } - set({ - currentIndex: nextIndex, - musicId: queue[nextIndex]?.id, - currentSong: queue[nextIndex], + let nextSong = queue[0]; + set((state) => ({ + musicId: nextSong?.id, + currentSong: nextSong, played: 0, isPlaying: false, - }); + queue: queue.slice(1), + previous: [...previous, currentSong], + playedSongIds: new Set([...state.playedSongIds, nextSong?.id]), + })); } }, playPrevious: () => { - const { queue, currentIndex, shuffle, shuffleHistory, shuffleHistoryIndex } = get(); - if (queue.length === 0) return; + const { queue, shuffle, shuffleHistory, shuffledQueue, previous, currentSong } = get(); if (shuffle) { - if (shuffleHistoryIndex > 0) { - const prevSong = shuffleHistory[shuffleHistoryIndex - 1]; - set({ - shuffleHistoryIndex: shuffleHistoryIndex - 1, - musicId: prevSong.id, - currentSong: prevSong, - played: 0, - isPlaying: false, - }); - } else if (shuffleHistoryIndex === 0) { - const firstSong = shuffleHistory[0]; + const newHistory = [...shuffleHistory]; + const newShuffledQueue = [...shuffledQueue]; + newShuffledQueue.unshift(currentSong); + if (shuffleHistory.length === 0) { set({ - musicId: firstSong.id, - currentSong: firstSong, + shuffledQueue: newShuffledQueue, played: 0, - isPlaying: false, }); + return; } + const prevSong = newHistory.pop(); + + set({ + musicId: prevSong?.id, + currentSong: prevSong, + played: 0, + isPlaying: false, + shuffledQueue: newShuffledQueue, + shuffleHistory: newHistory, + }); } else { - let prevIndex = currentIndex - 1; - if (prevIndex < 0) prevIndex = queue.length - 1; + const newPrevious = [...previous]; + const newQueue = [...queue]; + if (newPrevious.length === 0) { + set({ played: 0 }); + return; + } + const prevSong = newPrevious.pop(); + newQueue.unshift(currentSong); + set({ - currentIndex: prevIndex, - musicId: queue[prevIndex]?.id, - currentSong: queue[prevIndex], + musicId: prevSong.id, + currentSong: prevSong, played: 0, isPlaying: false, + queue: newQueue, + previous: newPrevious, }); } }, @@ -369,4 +450,21 @@ export const useStore = create((set, get) => ({ if (repeat === "one") set({ played: 0 }); else playNext(); }, + + // Persist and restore methods + persistState: async () => { + const { currentSong } = get(); + await persistMusicState(currentSong); + }, + + restoreState: async () => { + try { + const data = await restoreMusicState(); + if (data?.currentSong) { + set({ currentSong: data.currentSong }); + } + } catch (err) { + console.error("Failed to restore state:", err); + } + }, })); From 4df23701a13fa1744a3770a2c1043feb56555a15 Mon Sep 17 00:00:00 2001 From: SLASH27KushaL Date: Tue, 14 Oct 2025 01:28:48 +0530 Subject: [PATCH 4/5] solved issues --- eslint.config.js | 32 +++++- package.json | 2 + src/components/Artist/artist.jsx | 191 +++++++++++-------------------- src/hooks/SongCustomHooks.ts | 126 +++++++++++++++++++- src/hooks/useMusicPersistence.ts | 32 ++++++ src/lib/IndexedDBUtils.ts | 73 ++++++++++++ src/zustand/persistHelpers.ts | 41 +++++++ src/zustand/store.js | 2 +- 8 files changed, 372 insertions(+), 127 deletions(-) create mode 100644 src/hooks/useMusicPersistence.ts create mode 100644 src/lib/IndexedDBUtils.ts create mode 100644 src/zustand/persistHelpers.ts diff --git a/eslint.config.js b/eslint.config.js index 2f9b2aa..e47012d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,8 @@ import globals from "globals"; import react from "eslint-plugin-react"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import typescriptParser from "@typescript-eslint/parser"; +import typescriptPlugin from "@typescript-eslint/eslint-plugin"; export default [ { @@ -18,6 +20,7 @@ export default [ "public/", ], }, + // JavaScript and JSX files configuration { files: ["**/*.{js,jsx}"], languageOptions: { @@ -28,7 +31,7 @@ export default [ sourceType: "module", }, }, - settings: { react: { version: "18.3" } }, + settings: { react: { version: "detect" } }, plugins: { react, "react-hooks": reactHooks, @@ -39,8 +42,35 @@ export default [ ...react.configs.recommended.rules, ...react.configs["jsx-runtime"].rules, ...reactHooks.configs.recommended.rules, + "react/prop-types": "off", "react/jsx-no-target-blank": "off", "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, }, + // TypeScript and TSX files configuration + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + globals: globals.browser, + parser: typescriptParser, + parserOptions: { + ecmaVersion: "latest", + ecmaFeatures: { jsx: true }, + sourceType: "module", + }, + }, + settings: { react: { version: "detect" } }, + plugins: { + "@typescript-eslint": typescriptPlugin, + react, + "react-hooks": reactHooks, + }, + rules: { + ...typescriptPlugin.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs["jsx-runtime"].rules, + ...reactHooks.configs.recommended.rules, + "react/prop-types": "off", + }, + }, ]; diff --git a/package.json b/package.json index 5df5567..944b1a5 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,8 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.46.1", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9.17.0", diff --git a/src/components/Artist/artist.jsx b/src/components/Artist/artist.jsx index e257518..922cf2a 100644 --- a/src/components/Artist/artist.jsx +++ b/src/components/Artist/artist.jsx @@ -1,28 +1,28 @@ import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; -import Api, { fetchArtistBio } from "../../Api"; // Assumes fetchArtistBio is exported from Api.js +import Api, { fetchArtistBio } from "../../Api"; import { getImageColors } from "../color/ColorGenrator"; import { ScrollArea } from "../ui/scroll-area"; import { useStore } from "../../zustand/store"; -import { Play, Pause, Share2, Shuffle } from "lucide-react"; +import { Play, Pause, Shuffle } from "lucide-react"; import Menu from "../Menu"; import Like from "../ui/Like"; import { toast } from "sonner"; import { useSongHandlers, getTextColor, usePlayAll, useShuffle } from "@/hooks/SongCustomHooks"; -import ArtistBio from "./ArtistBio"; // Import the new ArtistBio component +import ArtistBio from "./ArtistBio"; function Artist() { const [data, setData] = useState(null); - const [bio, setBio] = useState(""); // New state for the artist's biography + const [bio, setBio] = useState(""); const [bgColor, setBgColor] = useState(); const [isLoading, setIsLoading] = useState(true); const [imageLoaded, setImageLoaded] = useState(false); const [textColor, setTextColor] = useState("white"); - const url = useLocation(); - const { musicId, isPlaying, setIsPlaying, setCurrentList, currentArtistId } = useStore(); - const artistId = url.search.split("=")[1]; + const { search } = useLocation(); + const { musicId, isPlaying, setCurrentList, currentArtistId } = useStore(); + + const artistId = new URLSearchParams(search).get("Id"); - // Using custom hooks for consistency const { handleSongClick } = useSongHandlers(); const handlePlayAll = usePlayAll(artistId, data?.topSongs, "artist"); const handleShuffle = useShuffle(artistId, data?.topSongs, "artist"); @@ -31,6 +31,7 @@ function Artist() { const fetching = async () => { if (!artistId) { setIsLoading(false); + setData(null); return; } try { @@ -40,7 +41,6 @@ function Artist() { setData(artistData); setCurrentList(artistData.topSongs); - // Fetch artist biography (from your PR) if (artistData.name) { const artistBio = await fetchArtistBio(artistData.name); if (artistBio) { @@ -48,7 +48,6 @@ function Artist() { } } - // Generate colors from the artist image getImageColors(artistData.image[2].url).then(({ averageColor, dominantColor }) => { setBgColor({ bg1: averageColor, bg2: dominantColor }); setTextColor(getTextColor(dominantColor)); @@ -80,7 +79,7 @@ function Artist() {

Artist not found

-

Please try again later

+

Please check the URL or try again later.

); @@ -91,7 +90,7 @@ function Artist() {
{/* Hero Section */}
-
-
- {/* Artist Image */} -
+
+
+
{data.name}
{!imageLoaded && ( -
+
)}
- {/* Artist Info */} -
-
-

- Artist -

-

- {data.name} -

-
+
+

+ {data.name} +

- {/* Action Buttons */} -
+
-
@@ -187,82 +157,61 @@ function Artist() {
- {/* Top Songs Section */} -
-

Popular

-
+ {/* Top Songs & Bio Section */} +
+

Popular

+
{data.topSongs.map((song, index) => (
handleSongClick(song, { artistId })} > - {/* Unified Layout for both Mobile and Desktop */} -
- {/* Track Number / Play Button */} -
- - {index + 1} - - -
- {/* Song Image */} +
+ {index + 1} + +
+ +
{song.name} - {/* Song Info */}

{song.name}

-

- {data.name} -

-
- {/* Duration (Desktop) */} -
- {`${Math.floor(song.duration / 60)}:${(song.duration % 60).toString().padStart(2, "0")}`} -
- {/* Like & Menu Buttons */} -
- -
e.stopPropagation()} - className="w-10 h-10 flex items-center justify-center rounded-full hover:bg-muted transition-colors sm:opacity-0 sm:group-hover:opacity-100" - > - -
+

{data.name}

+ +
{data.name}
+ +
+ + + {`${Math.floor(song.duration / 60)}:${(song.duration % 60) + .toString() + .padStart(2, "0")}`} + + +
))}
- {/* Artist Biography Section */} {data && }
diff --git a/src/hooks/SongCustomHooks.ts b/src/hooks/SongCustomHooks.ts index 19910c4..01839bd 100644 --- a/src/hooks/SongCustomHooks.ts +++ b/src/hooks/SongCustomHooks.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useStore } from "../zustand/store"; interface SongClickOptions { @@ -6,11 +6,14 @@ interface SongClickOptions { artistId?: string; } +interface Song { + id: string; + // FIX: Replaced 'any' with 'unknown' for a type-safe index signature + [key: string]: unknown; +} + export const useSongHandlers = () => { - // state const musicId = useStore((state) => state.musicId); - - // actions const setMusicId = useStore((state) => state.setMusicId); const setIsPlaying = useStore((state) => state.setIsPlaying); const setAlbumId = useStore((state) => state.setAlbumId); @@ -32,3 +35,118 @@ export const useSongHandlers = () => { handleSongClick, }; }; + +export const getTextColor = (color: string): "dark" | "white" => { + if (!color) return "white"; + const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + let r: number, g: number, b: number; + + if (rgbMatch) { + r = parseInt(rgbMatch[1], 10); + g = parseInt(rgbMatch[2], 10); + b = parseInt(rgbMatch[3], 10); + } else { + const hex = color.replace("#", ""); + const bigint = parseInt(hex, 16); + r = (bigint >> 16) & 255; + g = (bigint >> 8) & 255; + b = bigint & 255; + } + + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return luminance > 0.6 ? "dark" : "white"; +}; + +export const usePlayAll = ( + contextId: string, + songs: Song[] | undefined, + contextType: "album" | "artist" +) => { + const { + isPlaying, + setIsPlaying, + setMusicId, + setCurrentList, + currentAlbumId, + currentArtistId, + setAlbumId, + setArtistId, + } = useStore(); + + const currentContextId = contextType === "album" ? currentAlbumId : currentArtistId; + const setContextId = contextType === "album" ? setAlbumId : setArtistId; + + return useCallback(() => { + if (currentContextId === contextId) { + setIsPlaying(!isPlaying); + } else { + if (songs && songs.length > 0) { + setCurrentList(songs); + setMusicId(songs[0].id); + setIsPlaying(true); + setContextId(contextId); + } + } + }, [ + contextId, + songs, + currentContextId, + isPlaying, + setIsPlaying, + setMusicId, + setCurrentList, + setContextId, + ]); +}; + +export const useShuffle = ( + contextId: string, + songs: Song[] | undefined, + contextType: "album" | "artist" +) => { + const { setMusicId, setCurrentList, setIsPlaying, setAlbumId, setArtistId } = useStore(); + const setContextId = contextType === "album" ? setAlbumId : setArtistId; + + return useCallback(() => { + if (songs && songs.length > 0) { + const shuffledSongs = [...songs].sort(() => Math.random() - 0.5); + const randomSong = shuffledSongs[0]; + setCurrentList(shuffledSongs); + setMusicId(randomSong.id); + setIsPlaying(true); + setContextId(contextId); + } + }, [songs, contextId, setCurrentList, setMusicId, setIsPlaying, setContextId]); +}; + +export const formatArtist = ( + song: { artists: { primary: Array<{ id: string; name: string }> } }, + check: boolean = false, + isMobile: boolean = false +): string => { + if (!song?.artists?.primary) return ""; + const all = song.artists.primary; + const limit = check ? all.length : isMobile ? 1 : 3; + + const artists = all + .slice(0, limit) + .map( + (artist) => + `${artist.name.trim()}` + ) + .join(", "); + + return all.length > limit ? `${artists} & more` : artists; +}; + +export const useIsMobile = (breakpoint: number = 768) => { + const [isMobile, setIsMobile] = useState(window.innerWidth <= breakpoint); + + useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth <= breakpoint); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [breakpoint]); + + return isMobile; +}; diff --git a/src/hooks/useMusicPersistence.ts b/src/hooks/useMusicPersistence.ts new file mode 100644 index 0000000..b61f288 --- /dev/null +++ b/src/hooks/useMusicPersistence.ts @@ -0,0 +1,32 @@ +import { useStore } from "@/zustand/store"; +import { useEffect } from "react"; + +// Define a basic type for a Song object +interface Song { + id: string; + [key: string]: unknown; +} + +// Define the shape of the state we're using from the store +interface MusicPersistenceState { + currentSong: Song | null; + restoreState: () => Promise; + persistState: () => Promise; +} + +export const useMusicPersistence = (): void => { + // FIX: Replaced 'any' with the specific 'MusicPersistenceState' type + const { currentSong, restoreState, persistState }: MusicPersistenceState = useStore(); + + // Restore state when app loads + useEffect(() => { + restoreState(); + }, [restoreState]); + + // Persist only when current song changes + useEffect(() => { + if (currentSong) { + persistState(); + } + }, [currentSong, persistState]); +}; diff --git a/src/lib/IndexedDBUtils.ts b/src/lib/IndexedDBUtils.ts new file mode 100644 index 0000000..79f4e7b --- /dev/null +++ b/src/lib/IndexedDBUtils.ts @@ -0,0 +1,73 @@ +// Define a basic type for a Song object +interface Song { + id: string; + [key: string]: unknown; +} + +export interface PlaybackData { + id: string; + // FIX: Replaced 'any' with the specific 'Song' type + currentSong: Song | null; +} + +export class IndexedDBUtils { + private dbName: string; + private version: number; + private db: IDBDatabase | null = null; + + constructor(dbName = "MusicAppDB", version = 1) { + this.dbName = dbName; + this.version = version; + } + + async init(): Promise { + if (this.db) return this.db; + + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + + if (!db.objectStoreNames.contains("playback")) { + db.createObjectStore("playback", { keyPath: "id" }); + } + }; + + request.onsuccess = () => { + this.db = request.result; + resolve(this.db); + }; + + request.onerror = () => reject(request.error); + }); + } + + async save(store: string, data: T): Promise { + const db = await this.init(); + return new Promise((resolve, reject) => { + const tx = db.transaction(store, "readwrite"); + tx.objectStore(store).put(data); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + async get(store: string, id: string): Promise { + const db = await this.init(); + return new Promise((resolve, reject) => { + const tx = db.transaction(store, "readonly"); + const req = tx.objectStore(store).get(id); + req.onsuccess = () => resolve((req.result as T) ?? null); + req.onerror = () => reject(req.error); + }); + } + + async clear(store: string): Promise { + const db = await this.init(); + const tx = db.transaction(store, "readwrite"); + tx.objectStore(store).clear(); + } +} + +export const indexedDBUtils = new IndexedDBUtils(); diff --git a/src/zustand/persistHelpers.ts b/src/zustand/persistHelpers.ts new file mode 100644 index 0000000..a9e7d46 --- /dev/null +++ b/src/zustand/persistHelpers.ts @@ -0,0 +1,41 @@ +import { indexedDBUtils, PlaybackData } from "../lib/IndexedDBUtils.ts"; + +const STORE_NAME = "playback"; +const STATE_KEY = "state"; + +// Define a basic type for a Song object +interface Song { + id: string; + [key: string]: unknown; +} + +// FIX: Replaced 'any' with the specific 'Song' type for the function parameter +export async function persistMusicState(currentSong: Song | null): Promise { + const data: PlaybackData = { + id: STATE_KEY, + currentSong, + }; + + try { + await indexedDBUtils.save(STORE_NAME, data); + } catch (err) { + console.error("Failed to persist music state:", err); + } +} + +export async function restoreMusicState(): Promise { + try { + return await indexedDBUtils.get(STORE_NAME, STATE_KEY); + } catch (err) { + console.error("Failed to restore music state:", err); + return null; + } +} + +export async function clearMusicState(): Promise { + try { + await indexedDBUtils.clear(STORE_NAME); + } catch (err) { + console.error("Failed to clear music state:", err); + } +} diff --git a/src/zustand/store.js b/src/zustand/store.js index 495cfdf..daba0f5 100644 --- a/src/zustand/store.js +++ b/src/zustand/store.js @@ -1,6 +1,6 @@ import { create } from "zustand"; import Api from "../Api"; -import { persistMusicState, restoreMusicState } from "./persistHelpers"; +import { persistMusicState, restoreMusicState } from "./persistHelpers.ts"; const INITIAL_SONGS_LIMIT = 1; const SUGGESTIONS_LIMIT = 9; From eb197e42fd4afdd9e4c62103c83c0488ab21d416 Mon Sep 17 00:00:00 2001 From: Kushal Kishore <139056534+SLASH27KushaL@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:26:05 +0530 Subject: [PATCH 5/5] Update .env.example --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index b266ec7..819d5b5 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,5 @@ VITE_FIREBASE_AUTH_DOMAIN= VITE_FIREBASE_PROJECT_ID= VITE_FIREBASE_STORAGE_BUCKET= VITE_FIREBASE_MESSAGING_SENDER_ID= -VITE_FIREBASE_APP_ID= \ No newline at end of file +VITE_FIREBASE_APP_ID= +VITE_THEAUDIODB_API_KEY=