mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add a navbar
This commit is contained in:
parent
fb436fd0f5
commit
50507a1379
@ -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>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{t("navbar.home")}</p>
|
||||
{libraries.data.items.map((x: any) => (
|
||||
<p key={x.id}>{x.name}</p>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
export const ErrorSnackbar = ({ error }: { error: KyooErrors }) => {
|
||||
const [isOpen, setOpen] = useState(true);
|
||||
const close = (_: Event | SyntheticEvent, reason?: SnackbarCloseReason) => {
|
||||
if (reason !== "clickaway") setOpen(false);
|
||||
};
|
||||
|
||||
Toto.getFetchUrls = () => [["libraries"]];
|
||||
|
||||
export default Toto;
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<Snackbar open={isOpen} onClose={close} autoHideDuration={6000}>
|
||||
<Alert severity="error" onClose={close} sx={{ width: "100%" }}>
|
||||
{error.errors[0]}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
121
front/src/components/navbar.tsx
Normal file
121
front/src/components/navbar.tsx
Normal 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
3
front/src/global.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
margin: 0 !important;
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
@ -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 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}
|
||||
/>
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
export const Link = forwardRef<LinkRef, LinkProps>(NextLink);
|
||||
|
@ -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]]) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user