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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { QueryPage, useFetch } from "~/utils/query";
|
import { Alert, Snackbar, SnackbarCloseReason } from "@mui/material";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import { SyntheticEvent, useState } from "react";
|
||||||
|
import { KyooErrors } from "~/models";
|
||||||
|
|
||||||
const Toto: QueryPage = ({}) => {
|
export const ErrorSnackbar = ({ error }: { error: KyooErrors }) => {
|
||||||
const libraries = useFetch<any>("libraries");
|
const [isOpen, setOpen] = useState(true);
|
||||||
const { t } = useTranslation("common");
|
const close = (_: Event | SyntheticEvent, reason?: SnackbarCloseReason) => {
|
||||||
|
if (reason !== "clickaway") setOpen(false);
|
||||||
if (libraries.error) return <p>oups</p>;
|
};
|
||||||
if (!libraries.data) return <p>loading</p>;
|
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<Snackbar open={isOpen} onClose={close} autoHideDuration={6000}>
|
||||||
<p>{t("navbar.home")}</p>
|
<Alert severity="error" onClose={close} sx={{ width: "100%" }}>
|
||||||
{libraries.data.items.map((x: any) => (
|
{error.errors[0]}
|
||||||
<p key={x.id}>{x.name}</p>
|
</Alert>
|
||||||
))}
|
</Snackbar>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Toto.getFetchUrls = () => [["libraries"]];
|
|
||||||
|
|
||||||
export default Toto;
|
|
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 { 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 "../global.css"
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
@ -33,6 +35,9 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Hydrate state={pageProps.queryState}>
|
<Hydrate state={pageProps.queryState}>
|
||||||
<ThemeProvider theme={defaultTheme}>
|
<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} />
|
<Component {...pageProps} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Hydrate>
|
</Hydrate>
|
||||||
@ -44,7 +49,10 @@ 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;
|
||||||
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;
|
return appProps;
|
||||||
};
|
};
|
||||||
|
@ -18,106 +18,30 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// This file was shamelessly taken from:
|
import React, { forwardRef, Ref } from "react";
|
||||||
// https://github.com/mui/material-ui/tree/master/examples/nextjs
|
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";
|
type ButtonRef = HTMLButtonElement;
|
||||||
import clsx from "clsx";
|
type ButtonLinkProps = Omit<ButtonProps, "href"> &
|
||||||
import { useRouter } from "next/router";
|
Pick<NLinkProps, "href" | "as" | "prefetch" | "locale">;
|
||||||
import NextLink, { LinkProps as NextLinkProps } from "next/link";
|
|
||||||
import MuiLink, { LinkProps as MuiLinkProps } from "@mui/material/Link";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
|
|
||||||
// Add support for the sx prop for consistency with the other branches.
|
const NextButton = ({ href, as, prefetch, locale, ...props }: ButtonLinkProps, ref: Ref<ButtonRef>) => (
|
||||||
const Anchor = styled("a")({});
|
<NLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
|
||||||
|
<MButton ref={ref} {...props} />
|
||||||
interface NextLinkComposedProps
|
</NLink>
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export type LinkProps = {
|
export const ButtonLink = forwardRef<ButtonRef, ButtonLinkProps>(NextButton);
|
||||||
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">;
|
|
||||||
|
|
||||||
// A styled version of the Next.js Link component:
|
type LinkRef = HTMLAnchorElement;
|
||||||
// https://nextjs.org/docs/api-reference/next/link
|
type LinkProps = Omit<MLinkProps, "href"> &
|
||||||
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) {
|
Pick<NLinkProps, "href" | "as" | "prefetch" | "locale">;
|
||||||
const {
|
|
||||||
activeClassName = "active",
|
|
||||||
as,
|
|
||||||
className: classNameProps,
|
|
||||||
href,
|
|
||||||
linkAs: linkAsProp,
|
|
||||||
locale,
|
|
||||||
noLinkStyle,
|
|
||||||
prefetch,
|
|
||||||
replace,
|
|
||||||
role, // Link don't have roles.
|
|
||||||
scroll,
|
|
||||||
shallow,
|
|
||||||
...other
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const router = useRouter();
|
const NextLink = ({ href, as, prefetch, locale, ...props }: LinkProps, ref: Ref<LinkRef>) => (
|
||||||
const pathname = typeof href === "string" ? href : href.pathname;
|
<NLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
|
||||||
const className = clsx(classNameProps, {
|
<MLink ref={ref} {...props} />
|
||||||
[activeClassName]: router.pathname === pathname && activeClassName,
|
</NLink>
|
||||||
});
|
);
|
||||||
|
|
||||||
const isExternal =
|
export const Link = forwardRef<LinkRef, LinkProps>(NextLink);
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
@ -19,7 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentType } from "react";
|
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> => {
|
const queryFn = async <T>(context: QueryFunctionContext): Promise<T> => {
|
||||||
try {
|
try {
|
||||||
@ -55,8 +62,30 @@ export type QueryPage<Props = {}> = ComponentType<Props> & {
|
|||||||
getFetchUrls?: (route: { [key: string]: string }) => [[string]];
|
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]) => {
|
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]]) => {
|
export const fetchQuery = async (queries: [[string]]) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user