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 */}
-
+
+
+
{!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 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}
+
+
{data.name}
+
- {/* Desktop/Tablet Layout */}
-
-
- {/* Track Number / Play Button */}
-
-
- {index + 1}
-
-
-
-
- {/* Song Image */}
-
-

-
-
- {/* 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,