Add a navbar

This commit is contained in:
Zoe Roux 2022-07-19 00:36:18 +02:00
parent fb436fd0f5
commit 50507a1379
6 changed files with 198 additions and 117 deletions

View File

@ -18,26 +18,22 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { QueryPage, useFetch } from "~/utils/query";
import useTranslation from "next-translate/useTranslation";
import { Alert, Snackbar, SnackbarCloseReason } from "@mui/material";
import { SyntheticEvent, useState } from "react";
import { KyooErrors } from "~/models";
const Toto: QueryPage = ({}) => {
const libraries = useFetch<any>("libraries");
const { t } = useTranslation("common");
if (libraries.error) return <p>oups</p>;
if (!libraries.data) return <p>loading</p>;
export const ErrorSnackbar = ({ error }: { error: KyooErrors }) => {
const [isOpen, setOpen] = useState(true);
const close = (_: Event | SyntheticEvent, reason?: SnackbarCloseReason) => {
if (reason !== "clickaway") setOpen(false);
};
if (!isOpen) return null;
return (
<>
<p>{t("navbar.home")}</p>
{libraries.data.items.map((x: any) => (
<p key={x.id}>{x.name}</p>
))}
</>
<Snackbar open={isOpen} onClose={close} autoHideDuration={6000}>
<Alert severity="error" onClose={close} sx={{ width: "100%" }}>
{error.errors[0]}
</Alert>
</Snackbar>
);
};
Toto.getFetchUrls = () => [["libraries"]];
export default Toto;

View File

@ -0,0 +1,121 @@
/*
* 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 {
AppBar,
Toolbar,
Typography,
SxProps,
Theme,
Avatar,
IconButton,
Tooltip,
Box,
Skeleton,
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import logo from "../../public/icons/icon.svg";
import useTranslation from "next-translate/useTranslation";
import Image from "next/image";
import { ButtonLink } from "~/utils/link";
import { Library, Page } from "~/models";
import { useFetch } from "~/utils/query";
import { ErrorSnackbar } from "./error-snackbar";
export const KyooTitle = (props: { sx: SxProps<Theme> }) => {
const { t } = useTranslation("common");
return (
<Tooltip title={t("navbar.home")}>
<ButtonLink
sx={{
alignItems: "center",
color: "inherit",
textDecoration: "inherit",
display: "flex",
...props.sx,
}}
href="/"
>
<Image src={logo} width={24} height={24} alt="" />
<Typography
variant="h6"
noWrap
sx={{
ml: 1,
mr: 2,
fontFamily: "monospace",
fontWeight: 700,
}}
>
Kyoo
</Typography>
</ButtonLink>
</Tooltip>
);
};
export const Navbar = () => {
const { t } = useTranslation("common");
const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries");
return (
<AppBar position="static">
<Toolbar>
<IconButton
size="large"
aria-label="more"
aria-controls="menu-appbar"
aria-haspopup="true"
color="inherit"
sx={{ display: { sx: "flex", sm: "none" } }}
>
<MenuIcon />
</IconButton>
<Box sx={{ flexGrow: 1, display: { sx: "flex", sm: "none" } }} />
<KyooTitle sx={{ mr: 1 }} />
<Box sx={{ flexGrow: 1, display: { sx: "flex", sm: "none" } }} />
<Box sx={{ flexGrow: 1, display: { xs: "none", sm: "flex" } }}>
{isSuccess
? data.items.map((library) => (
<ButtonLink
href={`/library/${library.slug}`}
key={library.slug}
sx={{ color: "white" }}
>
{library.name}
</ButtonLink>
))
: [...Array(4)].map((_, i) => (
<Typography key={i} variant="button" px=".25rem">
<Skeleton width="5rem" />
</Typography>
))}
</Box>
<Tooltip title={t("navbar.login")}>
<IconButton sx={{ p: 0 }} href="/auth/login">
<Avatar alt="Account" />
</IconButton>
</Tooltip>
</Toolbar>
{isError && <ErrorSnackbar error={error} />}
</AppBar>
);
};

3
front/src/global.css Normal file
View File

@ -0,0 +1,3 @@
body {
margin: 0 !important;
}

View File

@ -26,6 +26,8 @@ import type { AppProps } from "next/app";
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"
const App = ({ Component, pageProps }: AppProps) => {
const [queryClient] = useState(() => createQueryClient());
@ -33,6 +35,9 @@ const App = ({ Component, pageProps }: AppProps) => {
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.queryState}>
<ThemeProvider theme={defaultTheme}>
<Navbar />
{/* 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 */}
<Component {...pageProps} />
</ThemeProvider>
</Hydrate>
@ -44,7 +49,10 @@ App.getInitialProps = async (ctx: AppContext) => {
const appProps = await NextApp.getInitialProps(ctx);
const getUrl = (ctx.Component as any).getFetchUrls;
if (getUrl) appProps.pageProps.queryState = await fetchQuery(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);
return appProps;
};

View File

@ -18,106 +18,30 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
// This file was shamelessly taken from:
// https://github.com/mui/material-ui/tree/master/examples/nextjs
import React, { forwardRef, Ref } from "react";
import NLink, { LinkProps as NLinkProps} from "next/link";
import { Button as MButton, ButtonProps, Link as MLink, LinkProps as MLinkProps} from "@mui/material";
import * as React from "react";
import clsx from "clsx";
import { useRouter } from "next/router";
import NextLink, { LinkProps as NextLinkProps } from "next/link";
import MuiLink, { LinkProps as MuiLinkProps } from "@mui/material/Link";
import { styled } from "@mui/material/styles";
type ButtonRef = HTMLButtonElement;
type ButtonLinkProps = Omit<ButtonProps, "href"> &
Pick<NLinkProps, "href" | "as" | "prefetch" | "locale">;
// Add support for the sx prop for consistency with the other branches.
const Anchor = styled("a")({});
interface NextLinkComposedProps
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">,
Omit<NextLinkProps, "href" | "as" | "onClick" | "onMouseEnter"> {
to: NextLinkProps["href"];
linkAs?: NextLinkProps["as"];
}
export const NextLinkComposed = React.forwardRef<HTMLAnchorElement, NextLinkComposedProps>(
function NextLinkComposed(props, ref) {
const { to, linkAs, replace, scroll, shallow, prefetch, locale, ...other } = props;
return (
<NextLink
href={to}
prefetch={prefetch}
as={linkAs}
replace={replace}
scroll={scroll}
shallow={shallow}
passHref
locale={locale}
>
<Anchor ref={ref} {...other} />
</NextLink>
);
},
const NextButton = ({ href, as, prefetch, locale, ...props }: ButtonLinkProps, ref: Ref<ButtonRef>) => (
<NLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
<MButton ref={ref} {...props} />
</NLink>
);
export type LinkProps = {
activeClassName?: string;
as?: NextLinkProps["as"];
href: NextLinkProps["href"];
linkAs?: NextLinkProps["as"]; // Useful when the as prop is shallow by styled().
noLinkStyle?: boolean;
} & Omit<NextLinkComposedProps, "to" | "linkAs" | "href"> &
Omit<MuiLinkProps, "href">;
export const ButtonLink = forwardRef<ButtonRef, ButtonLinkProps>(NextButton);
// A styled version of the Next.js Link component:
// https://nextjs.org/docs/api-reference/next/link
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) {
const {
activeClassName = "active",
as,
className: classNameProps,
href,
linkAs: linkAsProp,
locale,
noLinkStyle,
prefetch,
replace,
role, // Link don't have roles.
scroll,
shallow,
...other
} = props;
type LinkRef = HTMLAnchorElement;
type LinkProps = Omit<MLinkProps, "href"> &
Pick<NLinkProps, "href" | "as" | "prefetch" | "locale">;
const router = useRouter();
const pathname = typeof href === "string" ? href : href.pathname;
const className = clsx(classNameProps, {
[activeClassName]: router.pathname === pathname && activeClassName,
});
const NextLink = ({ href, as, prefetch, locale, ...props }: LinkProps, ref: Ref<LinkRef>) => (
<NLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
<MLink ref={ref} {...props} />
</NLink>
);
const isExternal =
typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0);
if (isExternal) {
if (noLinkStyle) {
return <Anchor className={className} href={href} ref={ref} {...other} />;
}
return <MuiLink className={className} href={href} ref={ref} {...other} />;
}
const linkAs = linkAsProp || as;
const nextjsProps = { to: href, linkAs, replace, scroll, shallow, prefetch, locale };
if (noLinkStyle) {
return <NextLinkComposed className={className} ref={ref} {...nextjsProps} {...other} />;
}
return (
<MuiLink
component={NextLinkComposed}
className={className}
ref={ref}
{...nextjsProps}
{...other}
/>
);
});
export const Link = forwardRef<LinkRef, LinkProps>(NextLink);

View File

@ -19,7 +19,14 @@
*/
import { ComponentType } from "react";
import { dehydrate, QueryClient, QueryFunctionContext, useQuery } from "react-query";
import {
dehydrate,
QueryClient,
QueryFunctionContext,
useInfiniteQuery,
useQuery,
} from "react-query";
import { imageList, KyooErrors, Page } from "~/models";
const queryFn = async <T>(context: QueryFunctionContext): Promise<T> => {
try {
@ -55,8 +62,30 @@ export type QueryPage<Props = {}> = ComponentType<Props> & {
getFetchUrls?: (route: { [key: string]: string }) => [[string]];
};
const imageSelector = <T>(obj: T): T => {
for (const img of imageList) {
// @ts-ignore
if (img in obj && !obj[img].startWith("/api")) {
// @ts-ignore
obj[img] = `/api/${obj[img]}`;
}
}
return obj;
};
export const useFetch = <Data>(...params: [string]) => {
return useQuery<Data, any>(params);
return useQuery<Data, KyooErrors>(params, {
select: imageSelector,
});
};
export const useInfiniteFetch = <Data>(...params: [string]) => {
return useInfiniteQuery<Page<Data>, KyooErrors>(params, {
select: (pages) => {
pages.pages.map((x) => x.items.map(imageSelector));
return pages;
},
});
};
export const fetchQuery = async (queries: [[string]]) => {