mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add zod to validate types
This commit is contained in:
parent
090d613266
commit
dfe6fa7cda
@ -24,6 +24,16 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
swcMinify: true,
|
||||||
|
experimental: {
|
||||||
|
swcPlugins: [
|
||||||
|
[
|
||||||
|
"next-superjson-plugin",
|
||||||
|
{
|
||||||
|
excluded: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
KYOO_URL: process.env.KYOO_URL ?? "http://localhost:5000",
|
KYOO_URL: process.env.KYOO_URL ?? "http://localhost:5000",
|
||||||
},
|
},
|
||||||
|
@ -26,10 +26,12 @@
|
|||||||
"@mui/icons-material": "^5.8.4",
|
"@mui/icons-material": "^5.8.4",
|
||||||
"@mui/material": "^5.8.7",
|
"@mui/material": "^5.8.7",
|
||||||
"next": "12.2.2",
|
"next": "12.2.2",
|
||||||
|
"next-superjson-plugin": "^0.3.0",
|
||||||
"next-translate": "^1.5.0",
|
"next-translate": "^1.5.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-query": "^4.0.0-beta.23"
|
"react-query": "^4.0.0-beta.23",
|
||||||
|
"zod": "^3.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.0.3",
|
"@types/node": "18.0.3",
|
||||||
|
@ -29,13 +29,14 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Box,
|
Box,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
|
AppBarProps,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import logo from "../../public/icons/icon.svg";
|
import logo from "../../public/icons/icon.svg";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import useTranslation from "next-translate/useTranslation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ButtonLink } from "~/utils/link";
|
import { ButtonLink } from "~/utils/link";
|
||||||
import { Library, Page } from "~/models";
|
import { LibraryP, Paged } from "~/models";
|
||||||
import { useFetch } from "~/utils/query";
|
import { useFetch } from "~/utils/query";
|
||||||
import { ErrorSnackbar } from "./error-snackbar";
|
import { ErrorSnackbar } from "./error-snackbar";
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ export const KyooTitle = (props: { sx: SxProps<Theme> }) => {
|
|||||||
mr: 2,
|
mr: 2,
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
|
color: "white",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Kyoo
|
Kyoo
|
||||||
@ -72,9 +74,14 @@ export const KyooTitle = (props: { sx: SxProps<Theme> }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const NavbarQuery = {
|
||||||
|
parser: Paged(LibraryP),
|
||||||
|
path: ["libraries"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Navbar = (barProps: AppBarProps) => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries");
|
const { data, error, isSuccess, isError } = useFetch(NavbarQuery);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="sticky">
|
<AppBar position="sticky">
|
||||||
@ -111,7 +118,7 @@ export const Navbar = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Tooltip title={t("navbar.login")}>
|
<Tooltip title={t("navbar.login")}>
|
||||||
<IconButton sx={{ p: 0 }} href="/auth/login">
|
<IconButton sx={{ p: 0 }} href="/auth/login">
|
||||||
<Avatar alt="Account" />
|
<Avatar alt={t("navbar.login")} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
@ -18,9 +18,8 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Box, NoSsr, Skeleton, styled } from "@mui/material";
|
import { Box, Skeleton, styled } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
MutableRefObject,
|
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type { Page } from "./page";
|
export * from "./page";
|
||||||
export type { KyooErrors } from "./kyoo-errors";
|
export * from "./kyoo-errors";
|
||||||
export * from "./traits";
|
export * from "./traits";
|
||||||
export * from "./resources";
|
export * from "./resources";
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A page of resource that contains information about the pagination of resources.
|
* A page of resource that contains information about the pagination of resources.
|
||||||
*/
|
*/
|
||||||
@ -53,3 +55,12 @@ export interface Page<T> {
|
|||||||
*/
|
*/
|
||||||
items: T[];
|
items: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Paged = <Item>(item: z.ZodType<Item>): z.ZodSchema<Page<Item>> =>
|
||||||
|
z.object({
|
||||||
|
this: z.string(),
|
||||||
|
first: z.string(),
|
||||||
|
next: z.string().optional(),
|
||||||
|
count: z.number(),
|
||||||
|
items: z.array(item),
|
||||||
|
});
|
||||||
|
34
front/src/models/resources/genre.ts
Normal file
34
front/src/models/resources/genre.ts
Normal 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 { z } from "zod";
|
||||||
|
import { ResourceP } from "../traits/resource";
|
||||||
|
|
||||||
|
export const GenreP = ResourceP.extend({
|
||||||
|
/**
|
||||||
|
* The name of this genre.
|
||||||
|
*/
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A genre that allow one to specify categories for shows.
|
||||||
|
*/
|
||||||
|
export type Genre = z.infer<typeof GenreP>;
|
@ -18,19 +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 { Resource } from "../traits/resource";
|
import { z } from "zod";
|
||||||
|
import { ResourceP } from "../traits/resource";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The library that will contain Shows, Collections...
|
* The library that will contain Shows, Collections...
|
||||||
*/
|
*/
|
||||||
export interface Library extends Resource {
|
export const LibraryP = ResourceP.extend({
|
||||||
/**
|
/**
|
||||||
* The name of this library.
|
* The name of this library.
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: z.string(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of paths that this library is responsible for. This is mainly used by the Scan task.
|
* The list of paths that this library is responsible for. This is mainly used by the Scan task.
|
||||||
*/
|
*/
|
||||||
paths: string[];
|
paths: z.array(z.string()),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export type Library = z.infer<typeof LibraryP>;
|
||||||
|
@ -18,42 +18,10 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Resource, Images } from "../traits";
|
import { z } from "zod";
|
||||||
|
import { zdate } from "~/utils/zod";
|
||||||
/**
|
import { ImagesP, ResourceP } from "../traits";
|
||||||
* A series or a movie.
|
import { GenreP } from "./genre";
|
||||||
*/
|
|
||||||
export interface Show extends Resource, Images {
|
|
||||||
/**
|
|
||||||
* The title of this show.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of alternative titles of this show.
|
|
||||||
*/
|
|
||||||
aliases: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The summary of this show.
|
|
||||||
*/
|
|
||||||
overview: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this show airing, not aired yet or finished?
|
|
||||||
*/
|
|
||||||
status: Status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date this show started airing. It can be null if this is unknown.
|
|
||||||
*/
|
|
||||||
startAir: Date | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The date this show finished airing. It can also be null if this is unknown.
|
|
||||||
*/
|
|
||||||
endAir: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The enum containing show's status.
|
* The enum containing show's status.
|
||||||
@ -64,3 +32,45 @@ export enum Status {
|
|||||||
Airing = 2,
|
Airing = 2,
|
||||||
Planned = 3,
|
Planned = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ShowP = z.preprocess(
|
||||||
|
(x: any) => {
|
||||||
|
x.name = x.title;
|
||||||
|
return x;
|
||||||
|
},
|
||||||
|
ResourceP.merge(ImagesP).extend({
|
||||||
|
/**
|
||||||
|
* The title of this show.
|
||||||
|
*/
|
||||||
|
name: z.string(),
|
||||||
|
/**
|
||||||
|
* The list of alternative titles of this show.
|
||||||
|
*/
|
||||||
|
aliases: z.array(z.string()),
|
||||||
|
/**
|
||||||
|
* The summary of this show.
|
||||||
|
*/
|
||||||
|
overview: z.string(),
|
||||||
|
/**
|
||||||
|
* Is this show airing, not aired yet or finished?
|
||||||
|
*/
|
||||||
|
status: z.nativeEnum(Status),
|
||||||
|
/**
|
||||||
|
* The date this show started airing. It can be null if this is unknown.
|
||||||
|
*/
|
||||||
|
startAir: zdate().optional(),
|
||||||
|
/**
|
||||||
|
* The date this show finished airing. It can also be null if this is unknown.
|
||||||
|
*/
|
||||||
|
endAir: zdate().nullable(),
|
||||||
|
/**
|
||||||
|
* The list of genres (themes) this show has.
|
||||||
|
*/
|
||||||
|
genres: z.array(GenreP).optional(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A tv serie or an anime.
|
||||||
|
*/
|
||||||
|
export type Show = z.infer<typeof ShowP>;
|
||||||
|
@ -18,38 +18,43 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
import { z } from "zod";
|
||||||
* Base traits for items that has image resources.
|
|
||||||
*/
|
const imageFn = (url: string) => (url.startsWith("/api") ? url : `/api${url}`);
|
||||||
export interface Images {
|
|
||||||
|
export const ImagesP = z.object({
|
||||||
/**
|
/**
|
||||||
* An url to the poster of this resource. If this resource does not have an image, the link will
|
* An url to the poster of this resource. If this resource does not have an image, the link will
|
||||||
* be null. If the kyoo's instance is not capable of handling this kind of image for the specific
|
* be null. If the kyoo's instance is not capable of handling this kind of image for the specific
|
||||||
* resource, this field won't be present.
|
* resource, this field won't be present.
|
||||||
*/
|
*/
|
||||||
poster?: string;
|
poster: z.string().transform(imageFn).optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An url to the thumbnail of this resource. If this resource does not have an image, the link
|
* An url to the thumbnail of this resource. If this resource does not have an image, the link
|
||||||
* will be null. If the kyoo's instance is not capable of handling this kind of image for the
|
* will be null. If the kyoo's instance is not capable of handling this kind of image for the
|
||||||
* specific resource, this field won't be present.
|
* specific resource, this field won't be present.
|
||||||
*/
|
*/
|
||||||
thumbnail?: string;
|
thumbnail: z.string().transform(imageFn).optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An url to the logo of this resource. If this resource does not have an image, the link will be
|
* An url to the logo of this resource. If this resource does not have an image, the link will be
|
||||||
* null. If the kyoo's instance is not capable of handling this kind of image for the specific
|
* null. If the kyoo's instance is not capable of handling this kind of image for the specific
|
||||||
* resource, this field won't be present.
|
* resource, this field won't be present.
|
||||||
*/
|
*/
|
||||||
logo?: string;
|
logo: z.string().transform(imageFn).optional(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An url to the thumbnail of this resource. If this resource does not have an image, the link
|
* An url to the thumbnail of this resource. If this resource does not have an image, the link
|
||||||
* will be null. If the kyoo's instance is not capable of handling this kind of image for the
|
* will be null. If the kyoo's instance is not capable of handling this kind of image for the
|
||||||
* specific resource, this field won't be present.
|
* specific resource, this field won't be present.
|
||||||
*/
|
*/
|
||||||
|
trailer: z.string().transform(imageFn).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
trailer?: string;
|
/**
|
||||||
};
|
* Base traits for items that has image resources.
|
||||||
|
*/
|
||||||
|
export type Images = z.infer<typeof ImagesP>;
|
||||||
|
|
||||||
export const imageList = ["poster", "thumbnail", "logo", "trailer"];
|
export const imageList = ["poster", "thumbnail", "logo", "trailer"];
|
||||||
|
@ -18,18 +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 { z } from "zod";
|
||||||
* The base trait used to represent identifiable resources.
|
|
||||||
*/
|
export const ResourceP = z.object({
|
||||||
export interface Resource {
|
|
||||||
/**
|
/**
|
||||||
* A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
* A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: z.number(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A human-readable identifier that can be used instead of an ID. A slug must be unique for a type
|
* A human-readable identifier that can be used instead of an ID. A slug must be unique for a type
|
||||||
* of resource but it can be changed.
|
* of resource but it can be changed.
|
||||||
*/
|
*/
|
||||||
slug: string;
|
slug: z.string(),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base trait used to represent identifiable resources.
|
||||||
|
*/
|
||||||
|
export type Resource = z.infer<typeof ResourceP>;
|
||||||
|
@ -24,9 +24,9 @@ import { ThemeProvider } from "@mui/material";
|
|||||||
import NextApp, { AppContext } from "next/app";
|
import NextApp, { AppContext } from "next/app";
|
||||||
import type { AppProps } from "next/app";
|
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, QueryIdentifier, QueryPage } from "~/utils/query";
|
||||||
import { defaultTheme } from "~/utils/themes/default-theme";
|
import { defaultTheme } from "~/utils/themes/default-theme";
|
||||||
import { Navbar } from "~/components/navbar";
|
import { Navbar, NavbarQuery } from "~/components/navbar";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
import { Box } from "@mui/system";
|
import { Box } from "@mui/system";
|
||||||
|
|
||||||
@ -35,9 +35,7 @@ const AppWithNavbar = ({ children }: { children: JSX.Element }) => {
|
|||||||
<>
|
<>
|
||||||
<Navbar/>
|
<Navbar/>
|
||||||
{/* TODO: add an option to disable the navbar in the component */}
|
{/* TODO: add an option to disable the navbar in the component */}
|
||||||
<Box >
|
<Box>{children}</Box>
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -45,6 +43,8 @@ const AppWithNavbar = ({ children }: { children: JSX.Element }) => {
|
|||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
const { queryState, ...props } = pageProps;
|
const { queryState, ...props } = pageProps;
|
||||||
|
|
||||||
|
// TODO: tranform date string to date instances in the queryState
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Hydrate state={queryState}>
|
<Hydrate state={queryState}>
|
||||||
@ -61,10 +61,10 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
App.getInitialProps = async (ctx: AppContext) => {
|
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 QueryPage).getFetchUrls;
|
||||||
const urls: string[][] = getUrl ? getUrl(ctx.router.query) : [];
|
const urls: QueryIdentifier[] = getUrl ? getUrl(ctx.router.query as any) : [];
|
||||||
// TODO: check if the navbar is needed for this
|
// TODO: check if the navbar is needed for this
|
||||||
urls.push(["libraries"]);
|
urls.push(NavbarQuery);
|
||||||
appProps.pageProps.queryState = await fetchQuery(urls);
|
appProps.pageProps.queryState = await fetchQuery(urls);
|
||||||
|
|
||||||
return appProps;
|
return appProps;
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
import { Image, Poster } from "~/components/poster";
|
import { Image, Poster } from "~/components/poster";
|
||||||
import { Show } from "~/models";
|
import { Show, ShowP } from "~/models";
|
||||||
import { QueryPage, useFetch } from "~/utils/query";
|
import { QueryIdentifier, QueryPage, useFetch } from "~/utils/query";
|
||||||
import { withRoute } from "~/utils/router";
|
import { withRoute } from "~/utils/router";
|
||||||
|
|
||||||
const ShowHeader = (data: Show) => {
|
const ShowHeader = (data: Show) => {
|
||||||
@ -36,8 +36,17 @@ const ShowHeader = (data: Show) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const query = (slug: string): QueryIdentifier<Show> => ({
|
||||||
|
parser: ShowP,
|
||||||
|
path: ["shows", slug],
|
||||||
|
params: {
|
||||||
|
fields: ["genres"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||||
const { data } = useFetch<Show>("shows", slug);
|
const { data, error } = useFetch(query(slug));
|
||||||
|
console.log("error", data);
|
||||||
|
|
||||||
if (!data) return <p>oups</p>;
|
if (!data) return <p>oups</p>;
|
||||||
|
|
||||||
@ -48,6 +57,6 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ShowDetails.getFetchUrls = ({ slug }) => [["shows", slug]];
|
ShowDetails.getFetchUrls = ({ slug }) => [query(slug)];
|
||||||
|
|
||||||
export default withRoute(ShowDetails);
|
export default withRoute(ShowDetails);
|
||||||
|
@ -26,22 +26,37 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "react-query";
|
} from "react-query";
|
||||||
|
import { z } from "zod";
|
||||||
import { imageList, KyooErrors, Page } from "~/models";
|
import { imageList, KyooErrors, Page } from "~/models";
|
||||||
|
import { Paged } from "~/models/page";
|
||||||
|
|
||||||
const queryFn = async <T>(context: QueryFunctionContext): Promise<T> => {
|
const queryFn = async <Data>(
|
||||||
|
type: z.ZodType<Data>,
|
||||||
|
context: QueryFunctionContext,
|
||||||
|
): Promise<Data> => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
[typeof window === "undefined" ? process.env.KYOO_URL : "/api"]
|
[typeof window === "undefined" ? process.env.KYOO_URL : "/api"]
|
||||||
.concat(context.pageParam ? [context.pageParam] : (context.queryKey as string[]))
|
.concat(
|
||||||
.join("/"),
|
context.pageParam ? [context.pageParam] : (context.queryKey.filter((x) => x) as string[]),
|
||||||
|
)
|
||||||
|
.join("/")
|
||||||
|
.replace("/?", "?"),
|
||||||
);
|
);
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw await resp.json();
|
throw await resp.json();
|
||||||
}
|
}
|
||||||
return await resp.json();
|
|
||||||
|
const data = await resp.json();
|
||||||
|
const parsed = await type.safeParseAsync(data);
|
||||||
|
if (!parsed.success) {
|
||||||
|
console.log("Parse error: ", parsed.error);
|
||||||
|
throw { errors: parsed.error.errors.map((x) => x.message) } as KyooErrors;
|
||||||
|
}
|
||||||
|
return parsed.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error("Fetch error: ", e);
|
||||||
throw { errors: ["Could not reach Kyoo's server."] }; // as KyooErrors;
|
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,52 +68,57 @@ export const createQueryClient = () =>
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
refetchOnReconnect: false,
|
refetchOnReconnect: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
queryFn: queryFn,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type QueryIdentifier<T = unknown> = {
|
||||||
|
parser: z.ZodType<T>;
|
||||||
|
path: string[];
|
||||||
|
params?: { [query: string]: boolean | number | string | string[] };
|
||||||
|
};
|
||||||
|
|
||||||
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||||
getFetchUrls?: (route: { [key: string]: string }) => string[][];
|
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageSelector = <T>(obj: T): T => {
|
const toQuery = (params?: { [query: string]: boolean | number | string | string[] }) => {
|
||||||
// TODO: remove this
|
if (!params) return undefined;
|
||||||
// @ts-ignore
|
return (
|
||||||
if ("title" in obj) obj.name = obj.title;
|
"?" +
|
||||||
|
Object.entries(params)
|
||||||
for (const img of imageList) {
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
||||||
// @ts-ignore
|
.join("&")
|
||||||
if (img in obj && obj[img] && !obj[img].startsWith("/api")) {
|
);
|
||||||
// @ts-ignore
|
|
||||||
obj[img] = `/api${obj[img]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetch = <Data>(...params: string[]) => {
|
export const useFetch = <Data>(query: QueryIdentifier<Data>) => {
|
||||||
return useQuery<Data, KyooErrors>(params, {
|
return useQuery<Data, KyooErrors>({
|
||||||
select: imageSelector,
|
queryKey: [...query.path, toQuery(query.params)],
|
||||||
|
queryFn: (ctx) => queryFn(query.parser, ctx),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteFetch = <Data>(...params: string[]) => {
|
export const useInfiniteFetch = <Data>(query: QueryIdentifier<Data>) => {
|
||||||
return useInfiniteQuery<Page<Data>, KyooErrors>(params, {
|
return useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||||
select: (pages) => {
|
queryKey: [...query.path, toQuery(query.params)],
|
||||||
pages.pages.map((x) => x.items.map(imageSelector));
|
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
||||||
return pages;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchQuery = async (queries: string[][]) => {
|
export const fetchQuery = async (queries: QueryIdentifier[]) => {
|
||||||
// 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((query) =>
|
||||||
|
client.prefetchQuery({
|
||||||
|
queryKey: [...query.path, toQuery(query.params)],
|
||||||
|
queryFn: (ctx) => queryFn(query.parser, ctx),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
return dehydrate(client);
|
return dehydrate(client);
|
||||||
};
|
};
|
||||||
|
33
front/src/utils/zod.ts
Normal file
33
front/src/utils/zod.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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 { z } from "zod";
|
||||||
|
|
||||||
|
export const zdate = () => {
|
||||||
|
return z.preprocess((arg) => {
|
||||||
|
if (arg instanceof Date) return arg;
|
||||||
|
|
||||||
|
if (typeof arg === "string" && /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/.test(arg)) {
|
||||||
|
return new Date(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, z.date());
|
||||||
|
};
|
@ -1385,7 +1385,7 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.1:
|
hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
@ -1938,6 +1938,13 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
|
next-superjson-plugin@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/next-superjson-plugin/-/next-superjson-plugin-0.3.0.tgz#81145f275c1e555be68867c104cc21113f96c675"
|
||||||
|
integrity sha512-M0Soj1P2t9peCyzNndEJiS48O2m88X9UGsCXDy8WHyGwWw1S7eCOEg9MiMqR+X1GD5C3hsdtHKMsKqrZOzr+SQ==
|
||||||
|
dependencies:
|
||||||
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
|
||||||
next-translate@^1.5.0:
|
next-translate@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/next-translate/-/next-translate-1.5.0.tgz#b1e5c4a8e55e31b3ed1b9428529f27c289c6b7bc"
|
resolved "https://registry.yarnpkg.com/next-translate/-/next-translate-1.5.0.tgz#b1e5c4a8e55e31b3ed1b9428529f27c289c6b7bc"
|
||||||
@ -2583,3 +2590,8 @@ yaml@^1.7.2:
|
|||||||
version "1.10.2"
|
version "1.10.2"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||||
|
|
||||||
|
zod@^3.18.0:
|
||||||
|
version "3.18.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.18.0.tgz#2eed58b3cafb8d9a67aa2fee69279702f584f3bc"
|
||||||
|
integrity sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user