Add image and poster component

This commit is contained in:
Zoe Roux 2022-07-24 23:17:35 +02:00
parent 50507a1379
commit 090d613266
7 changed files with 303 additions and 14 deletions

View File

@ -77,7 +77,7 @@ export const Navbar = () => {
const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries"); const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries");
return ( return (
<AppBar position="static"> <AppBar position="sticky">
<Toolbar> <Toolbar>
<IconButton <IconButton
size="large" size="large"

View File

@ -0,0 +1,128 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Box, NoSsr, Skeleton, styled } from "@mui/material";
import {
MutableRefObject,
SyntheticEvent,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { ComponentsOverrides, ComponentsProps, ComponentsVariants } from "@mui/material";
import { withThemeProps } from "~/utils/with-theme";
type ImageOptions = {
radius?: string;
fallback?: string;
};
type ImageProps = {
img?: string;
alt: string;
} & ImageOptions;
type ImagePropsWithLoading =
| (ImageProps & { loading?: false })
| (Partial<ImageProps> & { loading: true });
const _Image = ({
img,
alt,
radius,
fallback,
loading = false,
aspectRatio = undefined,
width = undefined,
height = undefined,
...others
}: ImagePropsWithLoading &
(
| { aspectRatio?: string; width: string | number; height: string | number }
| { aspectRatio: string; width?: string | number; height?: string | number }
)) => {
const [showLoading, setLoading] = useState<boolean>(loading);
const imgRef = useRef<HTMLImageElement>(null);
// This allow the loading bool to be false with SSR but still be on client-side
useLayoutEffect(() => {
if (!imgRef.current?.complete) setLoading(true);
}, []);
return (
<Box
borderRadius={radius}
overflow={"hidden"}
sx={{
aspectRatio,
width,
height,
backgroundColor: "primary.dark",
"& > *": { width: "100%", height: "100%" },
}}
{...others}
>
{showLoading && <Skeleton variant="rectangular" height="100%" />}
{!loading && img && (
<Box
component="img"
ref={imgRef}
src={img}
alt={alt}
onLoad={() => setLoading(false)}
onError={({ currentTarget }: SyntheticEvent<HTMLImageElement>) => {
if (fallback && currentTarget.src !== fallback) currentTarget.src = fallback;
else setLoading(false);
}}
sx={{ objectFit: "cover", display: showLoading ? "hidden" : undefined }}
/>
)}
</Box>
);
};
export const Image = styled(_Image)({});
// eslint-disable-next-line jsx-a11y/alt-text
const _Poster = (props: ImagePropsWithLoading) => <_Image aspectRatio="2 / 3" {...props} />;
declare module "@mui/material/styles" {
interface ComponentsPropsList {
Poster: ImageOptions;
}
interface ComponentNameToClassKey {
Poster: Record<string, never>;
}
interface Components<Theme = unknown> {
Poster?: {
defaultProps?: ComponentsProps["Poster"];
styleOverrides?: ComponentsOverrides<Theme>["Poster"];
variants?: ComponentsVariants["Poster"];
};
}
}
export const Poster = withThemeProps(_Poster, {
name: "Poster",
slot: "Root",
});

View File

@ -27,18 +27,31 @@ import { Hydrate, QueryClientProvider } from "react-query";
import { createQueryClient, fetchQuery } from "~/utils/query"; import { createQueryClient, fetchQuery } from "~/utils/query";
import { defaultTheme } from "~/utils/themes/default-theme"; import { defaultTheme } from "~/utils/themes/default-theme";
import { Navbar } from "~/components/navbar"; import { Navbar } from "~/components/navbar";
import "../global.css" import "../global.css";
import { Box } from "@mui/system";
const AppWithNavbar = ({ children }: { children: JSX.Element }) => {
return (
<>
<Navbar/>
{/* TODO: add an option to disable the navbar in the component */}
<Box >
{children}
</Box>
</>
);
};
const App = ({ Component, pageProps }: AppProps) => { const App = ({ Component, pageProps }: AppProps) => {
const [queryClient] = useState(() => createQueryClient()); const [queryClient] = useState(() => createQueryClient());
const { queryState, ...props } = pageProps;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.queryState}> <Hydrate state={queryState}>
<ThemeProvider theme={defaultTheme}> <ThemeProvider theme={defaultTheme}>
<Navbar /> <AppWithNavbar>
{/* TODO: add a container to allow the component to be scrolled without the navbar */} <Component {...props} />
{/* TODO: add an option to disable the navbar in the component */} </AppWithNavbar>
<Component {...pageProps} />
</ThemeProvider> </ThemeProvider>
</Hydrate> </Hydrate>
</QueryClientProvider> </QueryClientProvider>
@ -49,7 +62,7 @@ App.getInitialProps = async (ctx: AppContext) => {
const appProps = await NextApp.getInitialProps(ctx); const appProps = await NextApp.getInitialProps(ctx);
const getUrl = (ctx.Component as any).getFetchUrls; const getUrl = (ctx.Component as any).getFetchUrls;
const urls: [[string]] = getUrl ? getUrl(ctx.router.query) : []; const urls: string[][] = getUrl ? getUrl(ctx.router.query) : [];
// TODO: check if the navbar is needed for this // TODO: check if the navbar is needed for this
urls.push(["libraries"]); urls.push(["libraries"]);
appProps.pageProps.queryState = await fetchQuery(urls); appProps.pageProps.queryState = await fetchQuery(urls);

View File

@ -0,0 +1,53 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Box, Typography } from "@mui/material";
import { Image, Poster } from "~/components/poster";
import { Show } from "~/models";
import { QueryPage, useFetch } from "~/utils/query";
import { withRoute } from "~/utils/router";
const ShowHeader = (data: Show) => {
return (
<>
<Image img={data.thumbnail} alt="" height="60vh" width="100%" sx={{ positon: "relative" }} />
<Poster img={data.poster} alt={`${data.name}`} />
<Typography variant="h1" component="h1">
{data.name}
</Typography>
</>
);
};
const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
const { data } = useFetch<Show>("shows", slug);
if (!data) return <p>oups</p>;
return (
<>
<ShowHeader {...data} />
</>
);
};
ShowDetails.getFetchUrls = ({ slug }) => [["shows", slug]];
export default withRoute(ShowDetails);

View File

@ -59,27 +59,31 @@ export const createQueryClient = () =>
}); });
export type QueryPage<Props = {}> = ComponentType<Props> & { export type QueryPage<Props = {}> = ComponentType<Props> & {
getFetchUrls?: (route: { [key: string]: string }) => [[string]]; getFetchUrls?: (route: { [key: string]: string }) => string[][];
}; };
const imageSelector = <T>(obj: T): T => { const imageSelector = <T>(obj: T): T => {
// TODO: remove this
// @ts-ignore
if ("title" in obj) obj.name = obj.title;
for (const img of imageList) { for (const img of imageList) {
// @ts-ignore // @ts-ignore
if (img in obj && !obj[img].startWith("/api")) { if (img in obj && obj[img] && !obj[img].startsWith("/api")) {
// @ts-ignore // @ts-ignore
obj[img] = `/api/${obj[img]}`; obj[img] = `/api${obj[img]}`;
} }
} }
return obj; return obj;
}; };
export const useFetch = <Data>(...params: [string]) => { export const useFetch = <Data>(...params: string[]) => {
return useQuery<Data, KyooErrors>(params, { return useQuery<Data, KyooErrors>(params, {
select: imageSelector, select: imageSelector,
}); });
}; };
export const useInfiniteFetch = <Data>(...params: [string]) => { export const useInfiniteFetch = <Data>(...params: string[]) => {
return useInfiniteQuery<Page<Data>, KyooErrors>(params, { return useInfiniteQuery<Page<Data>, KyooErrors>(params, {
select: (pages) => { select: (pages) => {
pages.pages.map((x) => x.items.map(imageSelector)); pages.pages.map((x) => x.items.map(imageSelector));
@ -88,10 +92,11 @@ export const useInfiniteFetch = <Data>(...params: [string]) => {
}); });
}; };
export const fetchQuery = async (queries: [[string]]) => { export const fetchQuery = async (queries: string[][]) => {
// we can't put this check in a function because we want build time optimizations // we can't put this check in a function because we want build time optimizations
// see https://github.com/vercel/next.js/issues/5354 for details // see https://github.com/vercel/next.js/issues/5354 for details
if (typeof window !== "undefined") return {}; if (typeof window !== "undefined") return {};
console.log(queries)
const client = createQueryClient(); const client = createQueryClient();
await Promise.all(queries.map((x) => client.prefetchQuery(x))); await Promise.all(queries.map((x) => client.prefetchQuery(x)));

View File

@ -0,0 +1,34 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { useRouter } from "next/router";
import { ComponentType } from "react";
export const withRoute = <Props,>(Component: ComponentType<Props>) => {
const WithUseRoute = (props: Props) => {
const router = useRouter();
return <Component {...router.query} {...props} />;
};
const { ...all } = Component;
Object.assign(WithUseRoute, { ...all });
return WithUseRoute;
};

View File

@ -0,0 +1,56 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { Theme, useThemeProps, styled } from "@mui/material";
import { MUIStyledCommonProps, MuiStyledOptions } from "@mui/system";
import { FilteringStyledOptions } from "@mui/styled-engine";
import { WithConditionalCSSProp } from "@emotion/react/types/jsx-namespace";
import clsx from "clsx";
export interface ClassNameProps {
className?: string;
}
export const withThemeProps = <P,>(
component: React.ComponentType<P>,
options?: FilteringStyledOptions<P> & MuiStyledOptions,
) => {
const name = options?.name || component.displayName;
const Component = styled(component, options)<P>(() => ({}));
const WithTheme = (
inProps: P &
WithConditionalCSSProp<P & MUIStyledCommonProps<Theme>> &
ClassNameProps &
MUIStyledCommonProps<Theme>,
) => {
if (!name) {
console.error(
"withTheme could not be defined because the underlining component does not have a display name and the name option was not specified.",
);
return <Component {...inProps} />;
}
const props = useThemeProps({ props: inProps, name: name });
const className = clsx(props.className, `${name}-${options?.slot ?? "Root"}`);
return <Component {...props} className={className} />;
};
WithTheme.displayName = `WithThemeProps(${name || "Component"})`;
return WithTheme;
};