diff --git a/api/src/controllers/seed/insert/staff.ts b/api/src/controllers/seed/insert/staff.ts index 28e331fb..1e930d9a 100644 --- a/api/src/controllers/seed/insert/staff.ts +++ b/api/src/controllers/seed/insert/staff.ts @@ -43,14 +43,16 @@ export const insertStaff = record( staffPk: ret.find((y) => y.slug === x.staff.slug)!.pk, kind: x.kind, order: i, - character: { - ...x.character, - image: enqueueOptImage(imgQueue, { - url: x.character.image, - table: roles, - column: sql`${roles.character}['image']`, - }), - }, + character: x.character + ? { + ...x.character, + image: enqueueOptImage(imgQueue, { + url: x.character.image, + table: roles, + column: sql`${roles.character}['image']`, + }), + } + : null, })); await flushImageQueue(tx, imgQueue, -200); diff --git a/api/src/models/staff.ts b/api/src/models/staff.ts index c938c810..5121ea6c 100644 --- a/api/src/models/staff.ts +++ b/api/src/models/staff.ts @@ -35,12 +35,14 @@ export type Staff = typeof Staff.static; export const SeedStaff = t.Composite([ t.Omit(Role, ["character"]), t.Object({ - character: t.Composite([ - t.Omit(Character, ["image"]), - t.Object({ - image: t.Nullable(SeedImage), - }), - ]), + character: t.Nullable( + t.Composite([ + t.Omit(Character, ["image"]), + t.Object({ + image: t.Nullable(SeedImage), + }), + ]), + ), staff: t.Composite([ t.Object({ slug: t.String({ format: "slug" }), diff --git a/front/public/translations/en.json b/front/public/translations/en.json index 7167f49d..bece652d 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -47,7 +47,8 @@ "videosCount": "{{number}} videos", "version": "Version {{number}}", "part": "Part {{number}}", - "videos-map": "Edit video mappings" + "videos-map": "Edit video mappings", + "staff-as":"as {{character}}" }, "videos-map": { "none": "NONE", diff --git a/front/src/models/index.ts b/front/src/models/index.ts index 8b3ec060..1a6c9713 100644 --- a/front/src/models/index.ts +++ b/front/src/models/index.ts @@ -7,6 +7,7 @@ export * from "./search"; export * from "./season"; export * from "./serie"; export * from "./show"; +export * from "./staff"; export * from "./studio"; export * from "./user"; export * from "./utils/genre"; diff --git a/front/src/models/staff.ts b/front/src/models/staff.ts new file mode 100644 index 00000000..22be5dac --- /dev/null +++ b/front/src/models/staff.ts @@ -0,0 +1,38 @@ +import { z } from "zod/v4"; +import { KImage } from "./utils/images"; +import { Metadata } from "./utils/metadata"; +import { zdate } from "./utils/utils"; + +export const Character = z.object({ + name: z.string(), + latinName: z.string().nullable(), + image: KImage.nullable(), +}); +export type Character = z.infer; + +export const Staff = z.object({ + id: z.string(), + slug: z.string(), + name: z.string(), + latinName: z.string().nullable(), + image: KImage.nullable(), + externalId: Metadata, + createdAt: zdate(), + updatedAt: zdate(), +}); +export type Staff = z.infer; + +export const Role = z.object({ + kind: z.enum([ + "actor", + "director", + "writter", + "producer", + "music", + "crew", + "other", + ]), + character: Character.nullable(), + staff: Staff, +}); +export type Role = z.infer; diff --git a/front/src/primitives/icons.tsx b/front/src/primitives/icons.tsx index 861b2ec8..f86be613 100644 --- a/front/src/primitives/icons.tsx +++ b/front/src/primitives/icons.tsx @@ -71,12 +71,14 @@ export const IconButton = ({ as, className, iconClassName, + disabled, ...asProps }: { as?: ComponentType; icon: Icon; iconClassName?: string; className?: string; + disabled?: boolean; } & AsProps) => { const Container = as ?? PressableFeedback; @@ -90,7 +92,13 @@ export const IconButton = ({ )} {...(asProps as AsProps)} > - + ); }; diff --git a/front/src/query/fetch-grid.tsx b/front/src/query/fetch-grid.tsx new file mode 100644 index 00000000..8cc0c6dc --- /dev/null +++ b/front/src/query/fetch-grid.tsx @@ -0,0 +1,163 @@ +import ArrowBack from "@material-symbols/svg-400/rounded/arrow_back-fill.svg"; +import ArrowForward from "@material-symbols/svg-400/rounded/arrow_forward-fill.svg"; +import { + type ComponentType, + type ReactElement, + useEffect, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { View } from "react-native"; +import { + type Breakpoint, + IconButton, + tooltip, + useBreakpointMap, +} from "~/primitives"; +import { type QueryIdentifier, useInfiniteFetch } from "./query"; + +export type GridLayout = { + numColumns: Breakpoint; + numLines: Breakpoint; + gap: Breakpoint; +}; + +export const InfiniteGrid = ({ + query, + layout, + Render, + Loader, + Empty, + Header, + Footer, + getItemKey, +}: { + query: QueryIdentifier; + layout: GridLayout; + Render: (props: { item: Data; index: number }) => ReactElement | null; + Loader: (props: { index: number }) => ReactElement | null; + Empty?: ReactElement; + Header?: ComponentType<{ controls: ReactElement }> | ReactElement; + Footer?: ComponentType | ReactElement; + getItemKey?: (item: Data, index: number) => string | number; +}): ReactElement | null => { + const { t } = useTranslation(); + const [pageIndex, setPageIndex] = useState(0); + const { numColumns, numLines, gap } = useBreakpointMap(layout); + + query = { + ...query, + params: { ...query.params, limit: numColumns * numLines }, + }; + const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = + useInfiniteFetch(query); + + if (!query.infinite) + console.warn("A non infinite query was passed to an InfiniteGrid."); + + const queryIdentity = JSON.stringify(query); + useEffect(() => { + queryIdentity; + setPageIndex(0); + }, [queryIdentity]); + + const pages = data?.pages ?? []; + const items = pages[pageIndex]?.items ?? []; + + const controls = ( + + setPageIndex((x) => x - 1)} + disabled={pageIndex <= 0} + {...tooltip(t("misc.prev-page"))} + /> + { + if (pageIndex < pages.length - 1) { + setPageIndex((x) => x + 1); + return; + } + if (!hasNextPage || isFetchingNextPage) return; + const res = await fetchNextPage(); + if (!res.isError) setPageIndex((x) => x + 1); + }} + disabled={ + pageIndex === pages.length - 1 && (isFetchingNextPage || !hasNextPage) + } + {...tooltip(t("misc.next-page"))} + /> + + ); + + const header = + typeof Header === "function" ? ( +
+ ) : ( + (Header ?? controls) + ); + + const footer = typeof Footer === "function" ?