Started work on #9, many small changes

main
Guus van Meerveld 3 years ago
parent 0973689fb7
commit 99d1e1dc90
Signed by: Guusvanmeerveld
GPG Key ID: 2BA7D7912771966E

@ -9,6 +9,7 @@
"^next.*",
"^react$",
"^react.*",
"^axios.*",
"^@emotion/.*",
"^@mui/material/.*",
"^@mui/.*",

@ -12,7 +12,8 @@ module.exports = {
env: {
NEXT_PUBLIC_GITHUB_URL: packageInfo.repository.url,
NEXT_PUBLIC_APP_NAME: process.env.APP_NAME ?? packageInfo.displayName,
NEXT_PUBLIC_DEFAULT_SERVER: process.env.DEFAULT_SERVER ?? "vid.puffyan.us"
NEXT_PUBLIC_DEFAULT_SERVER:
process.env.DEFAULT_SERVER ?? "invidious.privacy.gd"
},
basePath: process.env.BASE_PATH ?? "",
trailingSlash: !(process.env.CI == "true")

@ -0,0 +1,71 @@
import Link from "next/link";
import { FC } from "react";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { abbreviateNumber, formatNumber } from "@src/utils/";
import { Channel as ChannelModel } from "@interfaces/api/search";
const Channel: FC<{ channel: ChannelModel }> = ({ channel }) => {
const theme = useTheme();
const thumbnail = channel.authorThumbnails.find(
(thumbnail) => thumbnail.height == 512
)?.url as string;
return (
<Link
passHref
href={{ pathname: "/channel", query: { c: channel.authorId } }}
>
<a>
<Paper sx={{ my: 2 }}>
<Box
sx={{
p: 3,
display: { md: "flex", xs: "block" },
alignItems: "center"
}}
>
<Avatar
sx={{
width: 96,
height: 96,
mx: { md: 3, xs: "auto" },
mb: { md: 0, xs: 2 }
}}
src={thumbnail}
alt={channel.author}
/>
<Box sx={{ textAlign: { md: "left", xs: "center" } }}>
<Typography variant="h5">{channel.author}</Typography>
<Typography
variant="subtitle1"
color={theme.palette.text.secondary}
>
{abbreviateNumber(channel.subCount)} subscribers {" "}
{formatNumber(channel.videoCount)} videos
</Typography>
<Typography
variant="subtitle1"
color={theme.palette.text.secondary}
>
{channel.description}
</Typography>
</Box>
</Box>
</Paper>
</a>
</Link>
);
};
export default Channel;

@ -34,6 +34,8 @@ const Navbar: FC = () => {
const [drawerIsOpen, setDrawerState] = useState(false);
const router = useRouter();
const query = router.query["search_query"];
const toggleDrawer =
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
@ -141,6 +143,7 @@ const Navbar: FC = () => {
</SearchIconWrapper>
<form action={`${router.basePath}/results`} method="get">
<StyledInputBase
defaultValue={query ?? ""}
name="search_query"
placeholder="Search…"
inputProps={{ "aria-label": "search" }}
@ -150,12 +153,22 @@ const Navbar: FC = () => {
</Box>
<Link href="/settings" passHref>
<IconButton
sx={{ display: { md: "flex", xs: "none" } }}
size="large"
<a
onClick={(e) => {
if (router.pathname == "/settings") {
e.preventDefault();
router.back();
}
}}
>
<Settings />
</IconButton>
<IconButton
sx={{ display: { md: "flex", xs: "none" } }}
size="large"
>
<Settings />
</IconButton>
</a>
</Link>
</Toolbar>
</AppBar>

@ -0,0 +1,94 @@
import Link from "next/link";
import { FC } from "react";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import Grid from "@mui/material/Grid";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { abbreviateNumber } from "@src/utils/";
import VideoModel from "@interfaces/video";
import { useAuthorThumbnail } from "@utils/requests";
const Video: FC<{ video: VideoModel }> = ({ video }) => {
const theme = useTheme();
const {
ref,
isLoading,
thumbnail: authorThumbnail
} = useAuthorThumbnail(video.author.id, 176);
return (
<Paper sx={{ my: 2 }}>
<Grid container spacing={0}>
<Grid item md={4}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
style={{
width: "100%",
height: "100%",
borderRadius: "4px"
}}
src={video.thumbnail}
alt="thumbnail"
loading="lazy"
/>
</Grid>
<Grid item md={8} sx={{ padding: 3, width: "100%" }}>
<Link href={{ pathname: "/watch", query: { v: video.id } }}>
<a>
<Typography gutterBottom noWrap variant="h5">
{video.title}
</Typography>
</a>
</Link>
<Typography
gutterBottom
variant="subtitle1"
color={theme.palette.text.secondary}
>
{abbreviateNumber(video.views)} Views Published{" "}
{video.published.text}
</Typography>
<Typography
gutterBottom
variant="subtitle1"
color={theme.palette.text.secondary}
>
{video.description.text}
</Typography>
<Link
passHref
href={{ pathname: "/channel", query: { c: video.author.id } }}
>
<a>
<Box ref={ref} sx={{ display: "flex", alignItems: "center" }}>
{isLoading && <CircularProgress />}
{!isLoading && (
<Avatar src={authorThumbnail} alt={video.author.name} />
)}
<Typography
sx={{ ml: 2 }}
variant="subtitle1"
color={theme.palette.text.secondary}
>
{video.author.name}
</Typography>
</Box>
</a>
</Link>
</Grid>
</Grid>
</Paper>
);
};
export default Video;

@ -1,12 +1,7 @@
import axios, { AxiosError } from "axios";
import Link from "next/link";
import { useRouter } from "next/router";
import { FC, useEffect } from "react";
import { useInView } from "react-intersection-observer/";
import { useQuery } from "react-query";
import { FC } from "react";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
@ -17,94 +12,71 @@ import CardMedia from "@mui/material/CardMedia";
import CircularProgress from "@mui/material/CircularProgress";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { abbreviateNumber } from "@src/utils";
import { Video as VideoModel } from "@interfaces/video";
import { useSettings } from "@utils/hooks";
import VideoModel from "@interfaces/video";
interface Channel {
authorThumbnails: { url: string; width: number; height: number }[];
}
import { useAuthorThumbnail } from "@utils/requests";
const Video: FC<VideoModel> = ({
thumbnail,
title,
id,
author,
views,
published
}) => {
const [settings] = useSettings();
const Video: FC<VideoModel> = (video) => {
const theme = useTheme();
const { isLoading, error, data, refetch, isFetched } = useQuery<
Channel,
AxiosError<Error>
>(
["channelData", author.id],
() =>
axios
.get(
`https://${settings.invidiousServer}/api/v1/channels/${author.id}`,
{
params: {
fields: ["authorThumbnails"].join(",")
}
}
)
.then((res) => res.data),
{ enabled: false }
);
const { ref, inView } = useInView({
threshold: 0
});
const {
ref,
isLoading,
thumbnail: authorThumbnail
} = useAuthorThumbnail(video.author.id, 100);
const router = useRouter();
useEffect(() => {
if (inView && !isFetched) refetch();
}, [inView, isFetched, refetch]);
return (
<Card sx={{ width: "100%" }}>
<CardActionArea
onClick={() => router.push({ pathname: "/watch", query: { v: id } })}
onClick={() =>
router.push({ pathname: "/watch", query: { v: video.id } })
}
>
<CardMedia
height="270"
component="img"
image={thumbnail}
image={video.thumbnail}
alt="video thumbnail"
/>
<CardContent>
<Tooltip title={title}>
<Tooltip title={video.title}>
<Typography noWrap gutterBottom variant="h6" component="div">
{title}
{video.title}
</Typography>
</Tooltip>
<Link passHref href={`/channel/${author.id}`}>
<Box ref={ref} sx={{ display: "flex", alignItems: "center" }}>
{isLoading && <CircularProgress sx={{ mr: 2 }} />}
{data && (
<Avatar
sx={{ mr: 2 }}
alt={author.name}
src={
data.authorThumbnails.find(
(thumbnail) => thumbnail.width == 100
)?.url as string
}
/>
)}
<Typography color="text.secondary" variant="subtitle1">
{author.name}
</Typography>
</Box>
<Link passHref href={`/channel/${video.author.id}`}>
<a>
<Box ref={ref} sx={{ display: "flex", alignItems: "center" }}>
{isLoading && <CircularProgress sx={{ mr: 2 }} />}
{!isLoading && (
<Avatar
sx={{ mr: 2 }}
alt={video.author.name}
src={authorThumbnail}
/>
)}
<Typography
color={theme.palette.text.secondary}
variant="subtitle1"
>
{video.author.name}
</Typography>
</Box>
</a>
</Link>
<Typography sx={{ mt: 2 }} color="text.secondary" variant="body2">
{abbreviateNumber(views)} Views Published {published.text}
<Typography
sx={{ mt: 2 }}
color={theme.palette.text.secondary}
variant="body2"
>
{abbreviateNumber(video.views)} Views Published{" "}
{video.published.text}
</Typography>
</CardContent>
</CardActionArea>

@ -0,0 +1,4 @@
a {
text-decoration: none;
color: unset;
}

@ -1,3 +1,22 @@
export interface Error {
error: string;
}
export interface Thumbnail {
url: string;
width: number;
height: number;
quality?: Quality;
}
export enum Quality {
Default = "default",
End = "end",
High = "high",
Maxres = "maxres",
Maxresdefault = "maxresdefault",
Medium = "medium",
Middle = "middle",
Sddefault = "sddefault",
Start = "start"
}

@ -0,0 +1,83 @@
import { Thumbnail } from "@interfaces/api";
import VideoTrending from "@interfaces/api/trending";
interface Results {
type: Type;
}
export interface ChannelResult extends Results {
type: "channel";
author: string;
authorId: string;
authorUrl: string;
authorThumbnails: Thumbnail[];
subCount: number;
videoCount: number;
description: string;
descriptionHtml: string;
}
export interface VideoResult extends Results {
type: "video";
title: string;
author: string;
authorId: string;
authorUrl: string;
description: string;
descriptionHtml: string;
videoId: string;
videoThumbnails: Thumbnail[];
viewCount: number;
published: number;
publishedText: string;
lengthSeconds: number;
liveNow: boolean;
premium: boolean;
}
export interface PlaylistResult extends Results {
type: "playlist";
title: string;
playlistId: string;
author: string;
authorId: string;
authorUrl: string;
videoCount: number;
videos: Video[];
}
export interface CategoryResult extends Results {
type: "category";
title: string;
contents: VideoTrending[];
}
export interface Content {
type: Type;
title: string;
videoId: string;
author: string;
authorId: string;
authorUrl: string;
videoThumbnails: Thumbnail[];
description: string;
descriptionHtml: string;
viewCount: number;
published: number;
publishedText: string;
lengthSeconds: number;
liveNow: boolean;
premium: boolean;
isUpcoming: boolean;
}
export type Type = "category" | "channel" | "playlist" | "video";
export interface Video {
title: string;
videoId: string;
lengthSeconds: number;
videoThumbnails: Thumbnail[];
}
export default Results;

@ -1,3 +1,5 @@
import { Thumbnail } from "@interfaces/api";
interface Trending {
type: string;
title: string;
@ -5,7 +7,7 @@ interface Trending {
author: string;
authorId: string;
authorUrl: string;
videoThumbnails: VideoThumbnail[];
videoThumbnails: Thumbnail[];
description: string;
descriptionHtml: string;
viewCount: number;
@ -17,11 +19,4 @@ interface Trending {
isUpcoming: boolean;
}
interface VideoThumbnail {
quality: string;
url: string;
width: number;
height: number;
}
export default Trending;

@ -1,3 +1,11 @@
import { Thumbnail } from "@interfaces/api";
import {
AdaptiveFormat,
Caption,
FormatStream,
RecommendedVideo
} from "@interfaces/video";
export interface Video {
type: string;
title: string;
@ -37,83 +45,6 @@ export interface Video {
recommendedVideos: RecommendedVideo[];
}
export interface AdaptiveFormat {
index: string;
bitrate: string;
init: string;
url: string;
itag: string;
type: string;
clen: string;
lmt: string;
projectionType: ProjectionType;
fps?: number;
container?: Container;
encoding?: string;
resolution?: string;
qualityLabel?: string;
}
export interface FormatStream {
url: string;
itag: string;
type: string;
quality: string;
fps: number;
container: string;
encoding: string;
resolution: string;
qualityLabel: string;
size: string;
}
enum Container {
M4A = "m4a",
Mp4 = "mp4",
Webm = "webm"
}
enum ProjectionType {
Rectangular = "RECTANGULAR"
}
interface Thumbnail {
url: string;
width: number;
height: number;
quality?: Quality;
}
enum Quality {
Default = "default",
End = "end",
High = "high",
Maxres = "maxres",
Maxresdefault = "maxresdefault",
Medium = "medium",
Middle = "middle",
Sddefault = "sddefault",
Start = "start"
}
export interface Caption {
label: string;
language_code: string;
url: string;
}
export interface RecommendedVideo {
videoId: string;
title: string;
videoThumbnails: Thumbnail[];
author: string;
authorUrl: string;
authorId: string;
lengthSeconds: number;
viewCountText: string;
viewCount: number;
}
interface Storyboard {
url: string;
templateUrl: string;

@ -1,11 +1,6 @@
import {
AdaptiveFormat,
Caption,
FormatStream,
RecommendedVideo
} from "@interfaces/api/video";
export interface Video {
import { Thumbnail } from "@interfaces/api";
interface Video {
thumbnail: string;
title: string;
description: {
@ -17,6 +12,7 @@ export interface Video {
name: string;
id: string;
url: string;
thumbnail?: string;
};
views: number;
published: {
@ -26,30 +22,79 @@ export interface Video {
length: number;
live: boolean;
premium: boolean;
}
export interface FullVideo extends Video {
keywords: string[];
likes: number;
dislikes: number;
familyFriendly: boolean;
genre: {
keywords?: string[];
likes?: number;
dislikes?: number;
familyFriendly?: boolean;
genre?: {
type: string;
url: string;
};
author: {
thumbnail: string;
name: string;
id: string;
url: string;
};
subscriptions: string;
rating: number;
premiered: Date | undefined;
recommendedVideos: RecommendedVideo[];
adaptiveFormats: AdaptiveFormat[];
formatStreams: FormatStream[];
captions: Caption[];
subscriptions?: string;
rating?: number;
premiered?: Date;
recommendedVideos?: RecommendedVideo[];
adaptiveFormats?: AdaptiveFormat[];
formatStreams?: FormatStream[];
captions?: Caption[];
}
export interface RecommendedVideo {
videoId: string;
title: string;
videoThumbnails: Thumbnail[];
author: string;
authorUrl: string;
authorId: string;
lengthSeconds: number;
viewCountText: string;
viewCount: number;
}
export interface Caption {
label: string;
language_code: string;
url: string;
}
enum ProjectionType {
Rectangular = "RECTANGULAR"
}
export interface AdaptiveFormat {
index: string;
bitrate: string;
init: string;
url: string;
itag: string;
type: string;
clen: string;
lmt: string;
projectionType: ProjectionType;
fps?: number;
container?: Container;
encoding?: string;
resolution?: string;
qualityLabel?: string;
}
export interface FormatStream {
url: string;
itag: string;
type: string;
quality: string;
fps: number;
container: string;
encoding: string;
resolution: string;
qualityLabel: string;
size: string;
}
enum Container {
M4A = "m4a",
Mp4 = "mp4",
Webm = "webm"
}
export default Video;

@ -5,7 +5,7 @@ import type { DefaultSeoProps } from "next-seo";
const name = process.env.NEXT_PUBLIC_APP_NAME;
const SEO: DefaultSeoProps = {
titleTemplate: `%s | ${name}`,
titleTemplate: `%s - ${name}`,
defaultTitle: name,
description: packageInfo.description,
openGraph: {

@ -10,6 +10,7 @@ import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import "@src/globals.css";
import SEO from "@src/next-seo.config";
import createTheme from "@src/theme";

@ -0,0 +1,103 @@
import NotFound from "./404";
import { NextPage } from "next";
import { NextSeo } from "next-seo";
import { useRouter } from "next/router";
import { useQuery } from "react-query";
import axios from "axios";
import Container from "@mui/material/Container";
import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography";
import Result, {
CategoryResult,
ChannelResult,
PlaylistResult,
VideoResult
} from "@interfaces/api/search";
import { apiToVideo } from "@utils/conversions";
import { useSettings } from "@utils/hooks";
import Channel from "@components/Channel/Inline";
import Layout from "@components/Layout";
import Loading from "@components/Loading";
import Video from "@components/Video/Inline";
const Results: NextPage = () => {
const router = useRouter();
const [settings] = useSettings();
const query = router.query["search_query"];
const { data, isLoading } = useQuery<Result[] | undefined>(
["searchResultsFor", query],
() =>
query
? axios
.get(`https://${settings.invidiousServer}/api/v1/search`, {
params: {
q: query,
type: "all"
}
})
.then((res) => res.data)
: undefined
);
if (!router.isReady || isLoading)
return (
<>
<NextSeo title="Searching..." />
<Layout>
<Loading />
</Layout>
</>
);
if (!query) return <NotFound />;
const channels = data?.filter((result) => result.type == "channel") as
| ChannelResult[]
| undefined;
const videos = data?.filter((result) => result.type == "video") as
| VideoResult[]
| undefined;
// const categories = data?.filter()
return (
<>
<NextSeo title={query as string} />
<Layout>
<Container sx={{ py: 2 }}>
{channels && channels.length != 0 && (
<>
<Typography variant="h5">Channels</Typography>
{channels.map((channel, i) => (
<Channel key={i} channel={channel} />
))}
<Divider sx={{ my: 4 }} />
</>
)}
{videos && videos.length != 0 && (
<>
<Typography variant="h5">Videos</Typography>
{videos.map((video, i) => (
<Video key={i} video={apiToVideo(video)} />
))}
<Divider sx={{ my: 4 }} />
</>
)}
</Container>
</Layout>
</>
);
};
export default Results;

@ -1,5 +1,3 @@
import axios, { AxiosError } from "axios";
import { NextPage } from "next";
import { NextSeo } from "next-seo";
@ -7,14 +5,16 @@ import { useState } from "react";
import { useQuery } from "react-query";
import axios, { AxiosError } from "axios";
import Box from "@mui/material/Box";
import Chip from "@mui/material/Chip";
import Typography from "@mui/material/Typography";
import { Error } from "@interfaces/api";
import TrendingModel from "@interfaces/api/trending";
import VideoTrending from "@interfaces/api/trending";
import { trendingToVideo } from "@utils/conversions";
import { apiToVideo } from "@utils/conversions";
import { useSettings } from "@utils/hooks";
import Layout from "@components/Layout";
@ -26,10 +26,10 @@ const Trending: NextPage = () => {
const [settings] = useSettings();
const { isLoading, error, data, refetch } = useQuery<
TrendingModel[],
const { isLoading, error, data } = useQuery<
VideoTrending[],
AxiosError<Error>
>("trendingData", (context) =>
>("trendingData", () =>
axios
.get(`https://${settings.invidiousServer}/api/v1/trending`, {
params: {
@ -84,7 +84,7 @@ const Trending: NextPage = () => {
);
})}
</Box>
<Grid videos={data.map(trendingToVideo)} />
<Grid videos={data.map(apiToVideo)} />
</>
)}
</Box>

@ -1,5 +1,4 @@
import NotFound from "./404";
import axios, { AxiosError } from "axios";
import { NextPage } from "next";
import { NextSeo } from "next-seo";
@ -9,8 +8,10 @@ import { useEffect } from "react";
import { useQuery } from "react-query";
import axios, { AxiosError } from "axios";
import { Error } from "@interfaces/api";
import Video from "@interfaces/api/video";
import VideoAPI from "@interfaces/api/video";
import { videoToVideo } from "@utils/conversions";
import { useSettings } from "@utils/hooks";
@ -19,29 +20,35 @@ import Layout from "@components/Layout";
import Loading from "@components/Loading";
const Watch: NextPage = () => {
const router = useRouter();
const { query, isReady } = useRouter();
const videoId = router.query["v"];
const videoId = query["v"];
const [settings] = useSettings();
const { isLoading, error, data, refetch } = useQuery<
Video,
const { isLoading, error, data } = useQuery<
VideoAPI | null,
AxiosError<Error>
>(
["videoData", videoId],
() =>
axios
.get(`https://${settings.invidiousServer}/api/v1/videos/${videoId}`, {
params: {}
})
.then((res) => res.data),
{ enabled: false }
>(["videoData", videoId], () =>
videoId
? axios
.get(`https://${settings.invidiousServer}/api/v1/videos/${videoId}`, {
params: {}
})
.then((res) => res.data)
: null
);
useEffect(() => {
if (videoId) refetch();
}, [videoId, refetch]);
if (!isReady || isLoading) {
return (
<>
<NextSeo title="Loading video..." />
<Layout>
<Loading />
</Layout>
</>
);
}
if (!videoId) {
return <NotFound />;
@ -49,14 +56,9 @@ const Watch: NextPage = () => {
return (
<>
<NextSeo
title={data ? data.title : isLoading ? "Loading video..." : "Not Found"}
/>
<Layout>
{isLoading && <Loading />}
{}
</Layout>
<NextSeo title={data ? data.title : "Not Found"} />
<Layout>{}</Layout>
</>
);
};

@ -1,8 +1,10 @@
import TrendingAPI from "@interfaces/api/trending";
import { Quality } from "@interfaces/api";
import { VideoResult } from "@interfaces/api/search";
import VideoTrending from "@interfaces/api/trending";
import VideoAPI from "@interfaces/api/video";
import Video, { FullVideo } from "@interfaces/video";
import Video from "@interfaces/video";
export const trendingToVideo = (item: TrendingAPI): Video => {
export const apiToVideo = (item: VideoTrending | VideoResult): Video => {
return {
title: item.title,
description: {
@ -24,12 +26,12 @@ export const trendingToVideo = (item: TrendingAPI): Video => {
live: item.liveNow,
premium: item.premium,
thumbnail: item.videoThumbnails.find(
(thumbnail) => thumbnail.quality == "maxresdefault"
(thumbnail) => thumbnail.quality == Quality.Maxresdefault
)?.url as string
};
};
export const videoToVideo = (item: VideoAPI): FullVideo => {
export const videoToVideo = (item: VideoAPI): Video => {
return {
title: item.title,
views: item.viewCount,

@ -8,11 +8,12 @@ export const abbreviateNumber = (value: number): string => {
suffixNum++;
}
value = parseInt(value.toPrecision(3));
return `${value}${suffixes[suffixNum]}`;
return `${value.toPrecision(4)}${suffixes[suffixNum]}`;
};
export const formatNumber = (number: number) =>
number.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
export const toCamelCase = (string: string): string =>
string
.replace(/(?:^\w|[A-Z]|\b\w)/g, (leftTrim: string, index: number) =>

@ -0,0 +1,57 @@
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { useQuery } from "react-query";
import axios, { AxiosError } from "axios";
import { useSettings } from "@utils/hooks";
interface Channel {
authorThumbnails: { url: string; width: number; height: number }[];
}
export const useAuthorThumbnail = (
authorId: string,
quality: 32 | 48 | 76 | 100 | 176 | 512
): {
isLoading: boolean;
error: AxiosError<Error> | null;
ref: (node?: Element) => void;
thumbnail?: string;
} => {
const [settings] = useSettings();
const { ref, inView } = useInView({
threshold: 0
});
const { isLoading, error, data, isFetched, refetch } = useQuery<
Channel,
AxiosError<Error>
>(
["channelData", authorId],
() =>
axios
.get(
`https://${settings.invidiousServer}/api/v1/channels/${authorId}`,
{
params: {
fields: ["authorThumbnails"].join(",")
}
}
)
.then((res) => res.data),
{ enabled: false }
);
useEffect(() => {
if (!isFetched && inView) refetch();
}, [refetch, isFetched, inView]);
const thumbnail = data?.authorThumbnails.find(
(thumbnail) => thumbnail.width == quality
)?.url;
return { isLoading, error, ref, thumbnail };
};
Loading…
Cancel
Save