Fixed unintentional Home component render on track change, implemented new mechanism with caching for spotifyAPI using react-query & refresh access token method. Moved to BoxView from View for frame bugs of react-nodegui

This commit is contained in:
krtirtho 2021-03-01 22:47:15 +06:00
parent 5a0486bc4f
commit faea24e86e
15 changed files with 478 additions and 8150 deletions

12
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"problemMatcher": [],
"label": "npm: start",
"detail": "qode ./dist/index.js"
}
]
}

8216
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"build": "webpack --mode=production",
"dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development",
"start": "qode ./dist/index.js",
"start:trace": "qode ./dist/index.js --trace",
"debug": "qode --inspect ./dist/index.js"
},
"dependencies": {
@ -16,6 +17,7 @@
"@nodegui/react-nodegui": "^0.10.0",
"axios": "^0.21.1",
"base64url": "^3.0.1",
"chalk": "^4.1.0",
"dotenv": "^8.2.0",
"du": "^1.0.0",
"express": "^4.17.1",
@ -25,9 +27,12 @@
"node-mpv": "^2.0.0-beta.1",
"open": "^7.4.1",
"react": "^16.14.0",
"react-dom": "^17.0.1",
"react-query": "^3.12.0",
"react-router": "^5.2.0",
"scrape-yt": "^1.4.7",
"spotify-web-api-node": "^5.0.2"
"spotify-web-api-node": "^5.0.2",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/core": "^7.11.6",
@ -43,6 +48,7 @@
"@types/react": "^16.9.49",
"@types/react-router": "^5.1.11",
"@types/spotify-web-api-node": "^5.0.0",
"@types/uuid": "^8.3.0",
"@types/webpack-env": "^1.15.3",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react";
import { Window, hot, View, useEventHandler } from "@nodegui/react-nodegui";
import { QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
import { Window, hot, View, useEventHandler, BoxView } from "@nodegui/react-nodegui";
import { Direction, QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
import nodeguiIcon from "../assets/nodegui.jpg";
import { MemoryRouter } from "react-router";
import Routes from "./routes";
@ -8,9 +8,11 @@ import { LocalStorage } from "node-localstorage";
import authContext from "./context/authContext";
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
import Player, { audioPlayer } from "./components/Player";
import { QueryClient, QueryClientProvider } from "react-query";
import express from "express";
import open from "open";
import spotifyApi from "./initializations/spotifyApi";
import showError from "./helpers/showError";
export enum CredentialKeys {
credentials = "credentials",
@ -25,6 +27,15 @@ export interface Credentials {
const minSize = { width: 700, height: 750 };
const winIcon = new QIcon(nodeguiIcon);
global.localStorage = new LocalStorage("./local");
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError(error) {
showError(error);
},
},
},
});
function RootApp() {
const windowRef = useRef<QMainWindow>();
@ -66,10 +77,9 @@ function RootApp() {
const [expires_in, setExpires_in] = useState<number>(0);
const [access_token, setAccess_token] = useState<string>("");
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration * 1000 /* 1s = 1000 ms */);
useEffect(() => {
setIsLoggedIn(!!cachedCredentials);
@ -128,10 +138,14 @@ function RootApp() {
<MemoryRouter>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
<playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}>
<QueryClientProvider client={queryClient}>
{/* <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> */}
<BoxView direction={Direction.TopToBottom}>
<Routes />
{isLoggedIn && <Player />}
</View>
</BoxView>
{/* </View> */}
</QueryClientProvider>
</playerContext.Provider>
</authContext.Provider>
</MemoryRouter>

View File

@ -1,39 +1,38 @@
import React, { useContext, useEffect, useState } from "react";
import { Button, View, ScrollArea } from "@nodegui/react-nodegui";
import playerContext from "../context/playerContext";
import authContext from "../context/authContext";
import { Button, ScrollArea, BoxView } from "@nodegui/react-nodegui";
import { useHistory } from "react-router";
import CachedImage from "./shared/CachedImage";
import { CursorShape } from "@nodegui/nodegui";
import { CursorShape, Direction } from "@nodegui/nodegui";
import useSpotifyApi from "../hooks/useSpotifyApi";
import showError from "../helpers/showError";
import authContext from "../context/authContext";
import useSpotifyApiError from "../hooks/useSpotifyApiError";
function Home() {
const { currentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const { access_token } = useContext(authContext);
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => {
if (access_token) {
(async () => {
try {
const categoriesReceived = await spotifyApi.getCategories({ country: "US" });
setCategories(categoriesReceived.body.categories.items);
} catch (error) {
console.error("Spotify featured playlist loading failed: ", error);
}
})();
if (categories.length === 0) {
spotifyApi
.getCategories({ country: "US" })
.then((categoriesReceived) => setCategories(categoriesReceived.body.categories.items))
.catch((error) => {
showError(error, "[Spotify genre loading failed]: ");
handleSpotifyError(error);
});
}
}, [access_token]);
return (
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
{currentPlaylist && <CategoryCard key={((Math.random() * Date.now()) / Math.random()) * 100} id="current" name="Currently Playing" />}
<ScrollArea style={`flex-grow: 1; border: none;`}>
<BoxView direction={Direction.TopToBottom}>
{categories.map((category, index) => {
return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} {...category} />;
return <CategoryCard key={index+category.id} id={category.id} name={category.name} />;
})}
</View>
</BoxView>
</ScrollArea>
);
}
@ -45,39 +44,42 @@ interface CategoryCardProps {
name: string;
}
function CategoryCard({ id, name }: CategoryCardProps) {
const CategoryCard = ({ id, name }: CategoryCardProps) => {
const history = useHistory();
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { currentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => {
let mounted = true;
(async () => {
try {
if (id !== "current") {
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 });
mounted && setPlaylists(playlistsRes.body.playlists.items);
if (playlists.length === 0) {
spotifyApi
.getPlaylistsForCategory(id, { limit: 4 })
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
.catch((error) => {
showError(error, `[Failed to get playlists of category ${name}]: `);
handleSpotifyError(error);
});
}
} catch (error) {
console.error(`Failed to get playlists of category ${name} for: `, error);
}
})();
return () => {
mounted = false;
};
}, []);
function goToGenre() {
history.push(`/genre/playlists/${id}`, { name });
}
return (
<BoxView id="container" styleSheet={categoryStylesheet} direction={Direction.TopToBottom}>
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
<BoxView direction={Direction.LeftToRight}>
{playlists.map((playlist, index) => {
return <PlaylistCard key={index+playlist.id} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</BoxView>
</BoxView>
);
};
const categoryStylesheet = `
#container{
flex: 1;
flex-direction: column;
justify-content: 'center';
margin-bottom: 20px;
}
#anchor-heading{
@ -87,41 +89,22 @@ function CategoryCard({ id, name }: CategoryCardProps) {
outline: none;
font-size: 20px;
font-weight: bold;
align-self: 'flex-start';
text-align: left;
}
#anchor-heading:hover{
border: none;
outline: none;
text-decoration: underline;
}
#child-view{
flex: 1;
justify-content: 'space-evenly';
align-items: 'center';
flex-wrap: 'wrap';
}
`;
return (
<View id="container" styleSheet={categoryStylesheet}>
{(playlists.length > 0 || id === "current") && <Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />}
<View id="child-view">
{id === "current" && currentPlaylist && <PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />}
{playlists.map((playlist, index) => {
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
})}
</View>
</View>
);
}
interface PlaylistCardProps {
thumbnail: string;
name: string;
id: string;
}
export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
export const PlaylistCard = React.memo(({ id, name, thumbnail }: PlaylistCardProps) => {
const history = useHistory();
function gotoPlaylist() {
@ -130,9 +113,9 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
const playlistStyleSheet = `
#playlist-container{
max-width: 250px;
flex-direction: column;
padding: 2px;
max-width: 150px;
max-height: 150px;
min-height: 150px;
}
#playlist-container:hover{
border: 1px solid green;
@ -143,8 +126,8 @@ export function PlaylistCard({ id, name, thumbnail }: PlaylistCardProps) {
`;
return (
<View id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
<BoxView size={{height: 150, width: 150, fixed: true}} direction={Direction.TopToBottom} id="playlist-container" cursor={CursorShape.PointingHandCursor} styleSheet={playlistStyleSheet} on={{ MouseButtonRelease: gotoPlaylist }}>
<CachedImage src={thumbnail} maxSize={{ height: 150, width: 150 }} scaledContents alt={name} />
</View>
</BoxView>
);
}
});

View File

@ -10,6 +10,7 @@ import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegul
import IconButton from "./shared/IconButton";
import authContext from "../context/authContext";
import useSpotifyApi from "../hooks/useSpotifyApi";
import showError from "../helpers/showError";
export const audioPlayer = new NodeMpv(
{
@ -59,6 +60,7 @@ function Player(): ReactElement {
}
} catch (error) {
console.error("Failed to start audio player", error);
showError(error, "[Failed starting audio player]: ")
}
})();
@ -98,7 +100,7 @@ function Player(): ReactElement {
setIsStopped(true);
setIsPaused(true);
}
console.error(error);
showError(error, "[Failure at track change]: ");
}
})();
}, [currentTrack]);
@ -170,14 +172,13 @@ function Player(): ReactElement {
setIsPaused(true);
}
} catch (error) {
console.error(error);
showError(error, "[Track control failed]: ");
}
};
const prevOrNext = (constant: number) => {
if (currentTrack && playlistTracksIds && currentPlaylist) {
const index = playlistTracksIds.indexOf(currentTrack.id) + constant;
console.log("index:", index);
setCurrentTrack(currentPlaylist.tracks[index > playlistTracksIds?.length - 1 ? 0 : index < 0 ? playlistTracksIds.length - 1 : index].track);
}
};
@ -188,7 +189,7 @@ function Player(): ReactElement {
await audioPlayer.stop();
}
} catch (error) {
console.error("Failed to stop the audio player: ", error);
showError(error, "[Failed at audio-player stop]: ")
}
}

View File

@ -2,7 +2,9 @@ import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { useContext, useEffect, useState } from "react";
import { useLocation, useParams } from "react-router";
import authContext from "../context/authContext";
import showError from "../helpers/showError";
import useSpotifyApi from "../hooks/useSpotifyApi";
import useSpotifyApiError from "../hooks/useSpotifyApiError";
import BackButton from "./BackButton";
import { PlaylistCard } from "./Home";
@ -12,23 +14,18 @@ function PlaylistGenreView() {
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { access_token, isLoggedIn } = useContext(authContext);
const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
useEffect(() => {
let mounted = true;
(async () => {
try {
if (access_token) {
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id);
mounted && setPlaylists(playlistsRes.body.playlists.items);
if (playlists.length === 0 && access_token) {
spotifyApi
.getPlaylistsForCategory(id)
.then((playlistsRes) => setPlaylists(playlistsRes.body.playlists.items))
.catch((error) => {
showError(error, `[Failed to get playlists of category ${location.state.name} for]: `);
handleSpotifyError(error);
});
}
} catch (error) {
console.error(`Failed to get playlists of category ${name} for: `, error);
}
})();
return () => {
mounted = false;
};
}, [access_token]);
const playlistGenreViewStylesheet = `

View File

@ -1,15 +1,15 @@
import React, { FC, useContext, useEffect, useState } from "react";
import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { FC, useContext } from "react";
import { BoxView, Button, ScrollArea, Text } from "@nodegui/react-nodegui";
import BackButton from "./BackButton";
import { useLocation, useParams } from "react-router";
import { QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
import authContext from "../context/authContext";
import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton";
import { heartRegular, play, stop } from "../icons";
import { audioPlayer } from "./Player";
import useSpotifyApi from "../hooks/useSpotifyApi";
import { QueryCacheKeys } from "../conf";
import useSpotifyQuery from "../hooks/useSpotifyQuery";
export interface PlaylistTrackRes {
name: string;
@ -18,28 +18,18 @@ export interface PlaylistTrackRes {
}
const PlaylistView: FC = () => {
const { isLoggedIn } = useContext(authContext);
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const params = useParams<{ id: string }>();
const location = useLocation<{ name: string; thumbnail: string }>();
const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]);
useEffect(() => {
if (isLoggedIn) {
(async () => {
try {
const tracks = await spotifyApi.getPlaylistTracks(params.id);
setTracks(tracks.body.items);
} catch (error) {
console.error(`Failed to get tracks from ${params.id}: `, error);
}
})();
}
}, []);
const { data: tracks, isSuccess, isError, isLoading, refetch } = useSpotifyQuery<SpotifyApi.PlaylistTrackObject[]>(
[QueryCacheKeys.playlistTracks, params.id],
(spotifyApi) => spotifyApi.getPlaylistTracks(params.id).then((track) => track.body.items),
{ initialData: [] }
);
const handlePlaylistPlayPause = () => {
if (currentPlaylist?.id !== params.id) {
if (currentPlaylist?.id !== params.id && isSuccess && tracks) {
setCurrentPlaylist({ ...params, ...location.state, tracks });
setCurrentTrack(tracks[0].track);
} else {
@ -49,32 +39,38 @@ const PlaylistView: FC = () => {
}
};
const trackClickHandler = async (track: SpotifyApi.TrackObjectFull) => {
try {
const trackClickHandler = (track: SpotifyApi.TrackObjectFull) => {
setCurrentTrack(track);
} catch (error) {
console.error("Failed to resolve track's youtube url: ", error);
}
};
return (
<View style={`flex-direction: 'column'; flex-grow: 1;`}>
<View style={`justify-content: 'space-between'; padding-bottom: 10px; padding-left: 10px;`}>
<BoxView direction={Direction.TopToBottom}>
<BoxView style={`max-width: 150px;`}>
<BackButton />
<View style={`height: 50px; justify-content: 'space-between'; width: 100px; padding-right: 20px;`}>
<IconButton icon={new QIcon(heartRegular)} />
<IconButton style={`background-color: #00be5f; color: white;`} on={{ clicked: handlePlaylistPlayPause }} icon={new QIcon(currentPlaylist?.id === params.id ? stop : play)} />
</View>
</View>
</BoxView>
<Text>{`<center><h2>${location.state.name[0].toUpperCase()}${location.state.name.slice(1)}</h2></center>`}</Text>
<ScrollArea style={`flex-grow: 1; border: none;`}>
<View style={`flex-direction:column;`}>
{isLoggedIn &&
tracks.length > 0 &&
tracks.map(({ track }, index) => {
<BoxView /* style={`flex-direction:column;`} */ direction={Direction.TopToBottom}>
{isLoading && <Text>{`Loading Tracks...`}</Text>}
{isError && (
<>
<Text>{`Failed to load ${location.state.name} tracks`}</Text>
<Button
on={{
clicked() {
refetch();
},
}}
text="Retry"
/>
</>
)}
{tracks?.map(({ track }, index) => {
return (
<TrackButton
key={index * ((Date.now() / Math.random()) * 100)}
key={index+track.id}
active={currentTrack?.id === track.id && currentPlaylist?.id === params.id}
artist={track.artists.map((x) => x.name).join(", ")}
name={track.name}
@ -82,9 +78,9 @@ const PlaylistView: FC = () => {
/>
);
})}
</View>
</BoxView>
</ScrollArea>
</View>
</BoxView>
);
};

View File

@ -3,6 +3,7 @@ import { Image, Text, View } from "@nodegui/react-nodegui";
import { QLabel } from "@nodegui/nodegui";
import { ImageProps } from "@nodegui/react-nodegui/dist/components/Image/RNImage";
import { getCachedImageBuffer } from "../../helpers/getCachedImageBuffer";
import showError from "../../helpers/showError";
interface CachedImageProps extends Omit<ImageProps, "buffer"> {
src: string;
@ -15,18 +16,17 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
const [imageProcessError, setImageProcessError] = useState<boolean>(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
mounted && setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size));
} catch (error) {
mounted && setImageProcessError(false);
console.log("Cached Image Error:", error);
if (imageBuffer===undefined) {
getCachedImageBuffer(src, props.maxSize ?? props.size)
.then((buffer) => setImageBuffer(buffer))
.catch((error) => {
setImageProcessError(false);
showError(error, "[Cached Image Error]: ");
});
}
})();
return () => {
imgRef.current?.close();
mounted = false;
};
}, []);
return !imageProcessError && imageBuffer ? (

View File

@ -3,4 +3,11 @@ import { join } from "path";
const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any
export const clientId = "";
export const trace = process.argv.find(arg => arg === "--trace") ?? false;
export const redirectURI = "http://localhost:4304/auth/spotify/callback"
export enum QueryCacheKeys{
categories="categories",
categoryPlaylists = "categoryPlaylists",
playlistTracks="playlistTracks"
}

8
src/helpers/showError.ts Normal file
View File

@ -0,0 +1,8 @@
import { trace } from "../conf";
import chalk from "chalk";
function showError(error: any, message: any="[Error]: ") {
console.error(chalk.red(message), trace ? error : error.message);
}
export default showError;

View File

@ -1,30 +1,22 @@
import chalk from "chalk";
import { useContext, useEffect } from "react";
import { CredentialKeys } from "../app";
import authContext from "../context/authContext";
import spotifyApi from "../initializations/spotifyApi";
function useSpotifyApi() {
const { access_token, clientId, clientSecret, expires_in, isLoggedIn, setExpires_in, setAccess_token } = useContext(authContext);
const { access_token, clientId, clientSecret, isLoggedIn } = useContext(authContext);
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
useEffect(() => {
if (isLoggedIn && clientId && clientSecret) {
if (isLoggedIn && clientId && clientSecret && refreshToken) {
console.log(chalk.bgCyan.black("Setting up spotify credentials"))
spotifyApi.setClientId(clientId);
spotifyApi.setClientSecret(clientSecret);
spotifyApi.setRefreshToken(refreshToken);
spotifyApi.setAccessToken(access_token);
}
const isExpiredToken = Date.now() > expires_in;
if (isLoggedIn && isExpiredToken && refreshToken) {
spotifyApi.setRefreshToken(refreshToken);
spotifyApi
.refreshAccessToken()
.then(({ body: { access_token, expires_in } }) => {
setAccess_token(access_token);
setExpires_in(expires_in);
})
.catch();
}
}, [access_token, clientId, clientSecret]);
}, [access_token, clientId, clientSecret, isLoggedIn]);
return spotifyApi;
}

View File

@ -0,0 +1,23 @@
import chalk from "chalk";
import { useContext } from "react";
import SpotifyWebApi from "spotify-web-api-node";
import authContext from "../context/authContext";
import showError from "../helpers/showError";
function useSpotifyApiError(spotifyApi: SpotifyWebApi) {
const { setAccess_token, isLoggedIn } = useContext(authContext);
return async (error: any | Error | TypeError) => {
if ((error.message === "Unauthorized" && error.status === 401 && isLoggedIn) || (error.body.error.message === "No token provided" && error.body.error.status===401)) {
try {
console.log(chalk.bgYellow.blackBright("Refreshing Access token"))
const { body:{access_token: refreshedAccessToken}} = await spotifyApi.refreshAccessToken();
setAccess_token(refreshedAccessToken);
} catch (error) {
showError(error, "[Authorization Failure]: ")
}
}
};
}
export default useSpotifyApiError;

View File

@ -0,0 +1,29 @@
import { useEffect } from "react";
import { QueryFunction, QueryKey, useQuery, UseQueryOptions, UseQueryResult } from "react-query";
import SpotifyWebApi from "spotify-web-api-node";
import useSpotifyApi from "./useSpotifyApi";
import useSpotifyApiError from "./useSpotifyApiError";
type SpotifyQueryFn<TQueryData> = (spotifyApi: SpotifyWebApi) => Promise<TQueryData>;
function useSpotifyQuery<TQueryData = unknown>(
queryKey: QueryKey,
queryHandler: SpotifyQueryFn<TQueryData>,
options: UseQueryOptions<TQueryData, SpotifyApi.ErrorObject> = {}
): UseQueryResult<TQueryData, SpotifyApi.ErrorObject> {
const spotifyApi = useSpotifyApi();
const handleSpotifyError = useSpotifyApiError(spotifyApi);
const query = useQuery<TQueryData, SpotifyApi.ErrorObject>(queryKey, ()=>queryHandler(spotifyApi), options);
const { isError, error } = query;
useEffect(() => {
if (isError && error) {
handleSpotifyError(error);
}
}, [isError, error]);
return query;
}
export default useSpotifyQuery;

View File

@ -15,8 +15,8 @@ function Routes() {
<Route exact path="/">
{isLoggedIn ? <Home /> : <Login />}
</Route>
<Route path="/playlist/:id"><PlaylistView/></Route>
<Route path="/genre/playlists/:id"><PlaylistGenreView/></Route>
<Route exact path="/playlist/:id"><PlaylistView/></Route>
<Route exact path="/genre/playlists/:id"><PlaylistGenreView/></Route>
</>
);
}