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= 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 1a0aadd..944b1a5 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", @@ -50,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/Api.js b/src/Api.js index 2ff9333..4346221 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) => { @@ -229,3 +226,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 8be4305..35410c1 100644 --- a/src/components/Artist/artist.jsx +++ b/src/components/Artist/artist.jsx @@ -1,17 +1,18 @@ 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"; -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"; 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); @@ -25,6 +26,11 @@ function Artist() { useEffect(() => { const fetching = async () => { + if (!artistId) { + setIsLoading(false); + setData(null); + return; + } try { setIsLoading(true); const res = await Api(`/api/artists/${artistId}`); @@ -34,13 +40,12 @@ function Artist() { // Generate colors from the artist image getImageColors(res.data.data.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); } @@ -64,7 +69,7 @@ function Artist() {

Artist not found

-

Please try again later

+

Please check the URL or try again later.

); @@ -75,31 +80,29 @@ function Artist() {
{/* Hero Section */}
- {/* Dark/Light overlay for better text contrast */}
-
-
- {/* Artist Image */} -
+
+
+
{data.name}
{!imageLoaded && ( -
+
)}
- {/* Artist Info */} -
-
-

- Artist -

-

- {data.name} -

-
+
+

+ {data.name} +

- {/* Action Buttons */} -
+
-
@@ -172,183 +147,62 @@ function Artist() {
- {/* Top Songs Section */} -
-
-
-

Popular

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

- {song.name} -

-
- - {/* Like Button - Mobile */} -
- -
+ {/* Top Songs & Bio Section */} +
+

Popular

+
+ {data.topSongs.map((song, index) => ( +
handleSongClick(song, { artistId })} + > +
+ {index + 1} + +
- {/* Menu Button - Always visible on mobile for better UX */} -
- -
-
+
+ {song.name} +
+

+ {song.name} +

+

{data.name}

+
- {/* 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")} -
- -
- -
+
{data.name}
- {/* Menu Button */} -
- -
-
-
+
+ + + {`${Math.floor(song.duration / 60)}:${(song.duration % 60) + .toString() + .padStart(2, "0")}`} + +
- ))} -
+
+ ))}
+ {data && }
diff --git a/src/hooks/SongCustomHooks.ts b/src/hooks/SongCustomHooks.ts index 10d71ed..01839bd 100644 --- a/src/hooks/SongCustomHooks.ts +++ b/src/hooks/SongCustomHooks.ts @@ -8,14 +8,12 @@ interface SongClickOptions { interface Song { id: string; - [key: string]: any; + // 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); @@ -38,38 +36,27 @@ export const useSongHandlers = () => { }; }; -/** - * Calculate luminance from RGB color and determine appropriate text color - */ export const getTextColor = (color: string): "dark" | "white" => { + if (!color) return "white"; const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); - let r: number | undefined; - let g: number | undefined; - let b: number | undefined; + let r: number, g: number, b: number; if (rgbMatch) { - r = Number.parseInt(rgbMatch[1]); - g = Number.parseInt(rgbMatch[2]); - b = Number.parseInt(rgbMatch[3]); + r = parseInt(rgbMatch[1], 10); + g = parseInt(rgbMatch[2], 10); + b = parseInt(rgbMatch[3], 10); } else { - const hexMatch = color.match(/^#?([a-f\d]{1,2})([a-f\d]{1,2})([a-f\d]{1,2})$/i); - if (!hexMatch) return "white"; - const expandHex = (hex: string) => (hex.length === 1 ? hex + hex : hex); - r = Number.parseInt(expandHex(hexMatch[1]), 16); - g = Number.parseInt(expandHex(hexMatch[2]), 16); - b = Number.parseInt(expandHex(hexMatch[3]), 16); + const hex = color.replace("#", ""); + const bigint = parseInt(hex, 16); + r = (bigint >> 16) & 255; + g = (bigint >> 8) & 255; + b = bigint & 255; } - // 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"; }; -/** - * Hook to handle Play All functionality for albums and artists - */ export const usePlayAll = ( contextId: string, songs: Song[] | undefined, @@ -89,13 +76,9 @@ export const usePlayAll = ( const currentContextId = contextType === "album" ? currentAlbumId : currentArtistId; const setContextId = contextType === "album" ? setAlbumId : setArtistId; - const handlePlayAll = useCallback(() => { + return useCallback(() => { if (currentContextId === contextId) { - if (isPlaying) { - setIsPlaying(false); - } else { - setIsPlaying(true); - } + setIsPlaying(!isPlaying); } else { if (songs && songs.length > 0) { setCurrentList(songs); @@ -114,71 +97,55 @@ export const usePlayAll = ( setCurrentList, setContextId, ]); - - return handlePlayAll; }; -/** - * Hook to handle Shuffle functionality - */ export const useShuffle = ( contextId: string, songs: Song[] | undefined, contextType: "album" | "artist" ) => { - const { setMusicId, setIsPlaying, setAlbumId, setArtistId } = useStore(); + const { setMusicId, setCurrentList, setIsPlaying, setAlbumId, setArtistId } = useStore(); const setContextId = contextType === "album" ? setAlbumId : setArtistId; - const handleShuffle = useCallback(() => { + return useCallback(() => { if (songs && songs.length > 0) { - const randomIndex = Math.floor(Math.random() * songs.length); - setMusicId(songs[randomIndex].id); + const shuffledSongs = [...songs].sort(() => Math.random() - 0.5); + const randomSong = shuffledSongs[0]; + setCurrentList(shuffledSongs); + setMusicId(randomSong.id); setIsPlaying(true); setContextId(contextId); } - }, [songs, contextId, setMusicId, setIsPlaying, setContextId]); - - return handleShuffle; + }, [songs, contextId, setCurrentList, setMusicId, setIsPlaying, setContextId]); }; -/** - * Format artist names with links - */ 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 x = check ? all.length : isMobile ? 1 : 3; + const limit = check ? all.length : isMobile ? 1 : 3; const artists = all - .slice(0, x) + .slice(0, limit) .map( (artist) => `${artist.name.trim()}` ) .join(", "); - return all.length > x ? `${artists} & more` : artists; + return all.length > limit ? `${artists} & more` : artists; }; -/** - * Hook to detect mobile screen size - */ export const useIsMobile = (breakpoint: number = 768) => { - const [isMobile, setIsMobile] = useState(false); + const [isMobile, setIsMobile] = useState(window.innerWidth <= breakpoint); useEffect(() => { - const mql = window.matchMedia(`(max-width: ${breakpoint}px)`); - const onChange = () => setIsMobile(mql.matches); - onChange(); - if (mql.addEventListener) mql.addEventListener("change", onChange); - else mql.addListener(onChange); - return () => { - if (mql.removeEventListener) mql.removeEventListener("change", onChange); - else mql.removeListener(onChange); - }; + 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 index cfc2cb3..b61f288 100644 --- a/src/hooks/useMusicPersistence.ts +++ b/src/hooks/useMusicPersistence.ts @@ -1,12 +1,22 @@ import { useStore } from "@/zustand/store"; import { useEffect } from "react"; -/** - * The Comments are added in order to make it easy for developers to debug any issues or make changes. - */ +// 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 => { - const { currentSong, restoreState, persistState }: any = useStore(); + // FIX: Replaced 'any' with the specific 'MusicPersistenceState' type + const { currentSong, restoreState, persistState }: MusicPersistenceState = useStore(); // Restore state when app loads useEffect(() => { diff --git a/src/lib/IndexedDBUtils.ts b/src/lib/IndexedDBUtils.ts index dd08d78..79f4e7b 100644 --- a/src/lib/IndexedDBUtils.ts +++ b/src/lib/IndexedDBUtils.ts @@ -1,6 +1,13 @@ +// Define a basic type for a Song object +interface Song { + id: string; + [key: string]: unknown; +} + export interface PlaybackData { id: string; - currentSong: any | null; + // FIX: Replaced 'any' with the specific 'Song' type + currentSong: Song | null; } export class IndexedDBUtils { diff --git a/src/zustand/persistHelpers.ts b/src/zustand/persistHelpers.ts index 058ad8c..a9e7d46 100644 --- a/src/zustand/persistHelpers.ts +++ b/src/zustand/persistHelpers.ts @@ -1,9 +1,16 @@ -import { indexedDBUtils, PlaybackData } from "../lib/IndexedDBUtils"; +import { indexedDBUtils, PlaybackData } from "../lib/IndexedDBUtils.ts"; const STORE_NAME = "playback"; const STATE_KEY = "state"; -export async function persistMusicState(currentSong: any | null): Promise { +// 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, diff --git a/src/zustand/store.js b/src/zustand/store.js index c25184b..daba0f5 100644 --- a/src/zustand/store.js +++ b/src/zustand/store.js @@ -1,10 +1,11 @@ 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; const BASE_JIOSAAVAN_URL = "https://jiosaavan-api-2-harsh-patel.vercel.app"; + export const useFetch = create((set) => ({ songs: null, albums: null,