diff --git a/front/src/components/navbar.tsx b/front/src/components/navbar.tsx index 60d8e242..f26a273c 100644 --- a/front/src/components/navbar.tsx +++ b/front/src/components/navbar.tsx @@ -77,7 +77,7 @@ export const Navbar = () => { const { data, error, isSuccess, isError } = useFetch>("libraries"); return ( - + . + */ + +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 & { 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(loading); + const imgRef = useRef(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 ( + *": { width: "100%", height: "100%" }, + }} + {...others} + > + {showLoading && } + {!loading && img && ( + setLoading(false)} + onError={({ currentTarget }: SyntheticEvent) => { + if (fallback && currentTarget.src !== fallback) currentTarget.src = fallback; + else setLoading(false); + }} + sx={{ objectFit: "cover", display: showLoading ? "hidden" : undefined }} + /> + )} + + ); +}; + +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; + } + + interface Components { + Poster?: { + defaultProps?: ComponentsProps["Poster"]; + styleOverrides?: ComponentsOverrides["Poster"]; + variants?: ComponentsVariants["Poster"]; + }; + } +} + +export const Poster = withThemeProps(_Poster, { + name: "Poster", + slot: "Root", +}); diff --git a/front/src/pages/_app.tsx b/front/src/pages/_app.tsx index 37a52654..0f5a78ed 100755 --- a/front/src/pages/_app.tsx +++ b/front/src/pages/_app.tsx @@ -27,18 +27,31 @@ import { Hydrate, QueryClientProvider } from "react-query"; import { createQueryClient, fetchQuery } from "~/utils/query"; import { defaultTheme } from "~/utils/themes/default-theme"; import { Navbar } from "~/components/navbar"; -import "../global.css" +import "../global.css"; +import { Box } from "@mui/system"; + +const AppWithNavbar = ({ children }: { children: JSX.Element }) => { + return ( + <> + + {/* TODO: add an option to disable the navbar in the component */} + + {children} + + + ); +}; const App = ({ Component, pageProps }: AppProps) => { const [queryClient] = useState(() => createQueryClient()); + const { queryState, ...props } = pageProps; return ( - + - - {/* TODO: add a container to allow the component to be scrolled without the navbar */} - {/* TODO: add an option to disable the navbar in the component */} - + + + @@ -49,7 +62,7 @@ App.getInitialProps = async (ctx: AppContext) => { const appProps = await NextApp.getInitialProps(ctx); 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 urls.push(["libraries"]); appProps.pageProps.queryState = await fetchQuery(urls); diff --git a/front/src/pages/show/[slug].tsx b/front/src/pages/show/[slug].tsx new file mode 100644 index 00000000..56c026fa --- /dev/null +++ b/front/src/pages/show/[slug].tsx @@ -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 . + */ + +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 ( + <> + + + + {data.name} + + + ); +}; + +const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => { + const { data } = useFetch("shows", slug); + + if (!data) return

oups

; + + return ( + <> + + + ); +}; + +ShowDetails.getFetchUrls = ({ slug }) => [["shows", slug]]; + +export default withRoute(ShowDetails); diff --git a/front/src/utils/query.ts b/front/src/utils/query.ts index 1c4def36..fb21217e 100644 --- a/front/src/utils/query.ts +++ b/front/src/utils/query.ts @@ -59,27 +59,31 @@ export const createQueryClient = () => }); export type QueryPage = ComponentType & { - getFetchUrls?: (route: { [key: string]: string }) => [[string]]; + getFetchUrls?: (route: { [key: string]: string }) => string[][]; }; const imageSelector = (obj: T): T => { + // TODO: remove this + // @ts-ignore + if ("title" in obj) obj.name = obj.title; + for (const img of imageList) { // @ts-ignore - if (img in obj && !obj[img].startWith("/api")) { + if (img in obj && obj[img] && !obj[img].startsWith("/api")) { // @ts-ignore - obj[img] = `/api/${obj[img]}`; + obj[img] = `/api${obj[img]}`; } } return obj; }; -export const useFetch = (...params: [string]) => { +export const useFetch = (...params: string[]) => { return useQuery(params, { select: imageSelector, }); }; -export const useInfiniteFetch = (...params: [string]) => { +export const useInfiniteFetch = (...params: string[]) => { return useInfiniteQuery, KyooErrors>(params, { select: (pages) => { pages.pages.map((x) => x.items.map(imageSelector)); @@ -88,10 +92,11 @@ export const useInfiniteFetch = (...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 // see https://github.com/vercel/next.js/issues/5354 for details if (typeof window !== "undefined") return {}; + console.log(queries) const client = createQueryClient(); await Promise.all(queries.map((x) => client.prefetchQuery(x))); diff --git a/front/src/utils/router.tsx b/front/src/utils/router.tsx new file mode 100644 index 00000000..3574b0d2 --- /dev/null +++ b/front/src/utils/router.tsx @@ -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 . + */ + +import { useRouter } from "next/router"; +import { ComponentType } from "react"; + +export const withRoute = (Component: ComponentType) => { + const WithUseRoute = (props: Props) => { + const router = useRouter(); + + return ; + }; + + const { ...all } = Component; + Object.assign(WithUseRoute, { ...all }); + return WithUseRoute; +}; diff --git a/front/src/utils/with-theme.tsx b/front/src/utils/with-theme.tsx new file mode 100644 index 00000000..3dcf0b82 --- /dev/null +++ b/front/src/utils/with-theme.tsx @@ -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 . + */ + +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 = ( + component: React.ComponentType

, + options?: FilteringStyledOptions

& MuiStyledOptions, +) => { + const name = options?.name || component.displayName; + const Component = styled(component, options)

(() => ({})); + + const WithTheme = ( + inProps: P & + WithConditionalCSSProp

> & + ClassNameProps & + MUIStyledCommonProps, + ) => { + 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 ; + } + const props = useThemeProps({ props: inProps, name: name }); + const className = clsx(props.className, `${name}-${options?.slot ?? "Root"}`); + return ; + }; + WithTheme.displayName = `WithThemeProps(${name || "Component"})`; + return WithTheme; +};