mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-04-01 06:42:24 -04:00
Add staff page with all their roles (#1399)
This commit is contained in:
commit
045cf0196b
@ -1,4 +1,4 @@
|
||||
import { and, eq, exists, ne, type SQL, sql } from "drizzle-orm";
|
||||
import { and, eq, exists, ne, or, type SQL, sql } from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
@ -364,7 +364,17 @@ export async function getShows({
|
||||
.where(
|
||||
and(
|
||||
filter,
|
||||
query ? sql`${transQ.name} %> ${query}::text` : undefined,
|
||||
query
|
||||
? or(
|
||||
sql`${transQ.name} %> ${query}::text`,
|
||||
exists(
|
||||
db
|
||||
.select()
|
||||
.from(sql`unnest(${transQ.tags}) as tag`)
|
||||
.where(sql`tag %> ${query}::text`),
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
keysetPaginate({ after, sort }),
|
||||
),
|
||||
)
|
||||
|
||||
@ -48,7 +48,16 @@
|
||||
"version": "Version {{number}}",
|
||||
"part": "Part {{number}}",
|
||||
"videos-map": "Edit video mappings",
|
||||
"staff-as":"as {{character}}"
|
||||
"staff-as":"as {{character}}",
|
||||
"staff-kind": {
|
||||
"actor": "Actor",
|
||||
"director": "Director",
|
||||
"writter": "Writer",
|
||||
"producer": "Producer",
|
||||
"music": "Music",
|
||||
"crew": "Crew",
|
||||
"other": "Other"
|
||||
}
|
||||
},
|
||||
"videos-map": {
|
||||
"none": "NONE",
|
||||
|
||||
5
front/src/app/(app)/staff/[slug].tsx
Normal file
5
front/src/app/(app)/staff/[slug].tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { StaffPage } from "~/ui/staff";
|
||||
|
||||
export { ErrorBoundary } from "~/ui/error-boundary";
|
||||
|
||||
export default StaffPage;
|
||||
@ -1,4 +1,5 @@
|
||||
import { z } from "zod/v4";
|
||||
import { Show } from "./show";
|
||||
import { KImage } from "./utils/images";
|
||||
import { Metadata } from "./utils/metadata";
|
||||
import { zdate } from "./utils/utils";
|
||||
@ -22,7 +23,7 @@ export const Staff = z.object({
|
||||
});
|
||||
export type Staff = z.infer<typeof Staff>;
|
||||
|
||||
export const Role = z.object({
|
||||
const BaseRole = z.object({
|
||||
kind: z.enum([
|
||||
"actor",
|
||||
"director",
|
||||
@ -33,6 +34,17 @@ export const Role = z.object({
|
||||
"other",
|
||||
]),
|
||||
character: Character.nullable(),
|
||||
});
|
||||
|
||||
export const Role = BaseRole.extend({
|
||||
staff: Staff,
|
||||
});
|
||||
export type Role = z.infer<typeof Role>;
|
||||
|
||||
export const RoleWithShow = BaseRole.extend({
|
||||
show: Show,
|
||||
}).transform((x) => ({
|
||||
...x,
|
||||
id: `${x.show.id}-${x.kind}-${x.character?.name ?? "none"}`,
|
||||
}));
|
||||
export type RoleWithShow = z.infer<typeof RoleWithShow>;
|
||||
|
||||
@ -90,6 +90,7 @@ export const IconButton = <AsProps = PressableProps>({
|
||||
"outline-0 hover:bg-gray-400/50 focus-visible:bg-gray-400/50",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...(asProps as AsProps)}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@ -1,43 +1,52 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Role } from "~/models";
|
||||
import { Container, H2, P, Poster, Skeleton, SubP } from "~/primitives";
|
||||
import { type KImage, Role } from "~/models";
|
||||
import { Container, H2, Link, P, Poster, Skeleton, SubP } from "~/primitives";
|
||||
import { InfiniteGrid, type QueryIdentifier } from "~/query";
|
||||
import { EmptyView } from "../empty-view";
|
||||
|
||||
const CharacterCard = ({ item }: { item: Role }) => {
|
||||
const { t } = useTranslation();
|
||||
export const CharacterCard = ({
|
||||
href,
|
||||
name,
|
||||
subtitle,
|
||||
image,
|
||||
characterImage,
|
||||
}: {
|
||||
href: string;
|
||||
name: string;
|
||||
subtitle: string;
|
||||
image: KImage | null;
|
||||
characterImage?: KImage | null;
|
||||
}) => {
|
||||
return (
|
||||
<View className="flex-row items-center overflow-hidden rounded-xl bg-card">
|
||||
<Poster src={item.staff.image} quality="low" className="w-28" />
|
||||
<Link
|
||||
href={href}
|
||||
className="flex-row items-center overflow-hidden rounded-xl bg-card"
|
||||
>
|
||||
<Poster src={image} quality="low" className="w-28" />
|
||||
<View className="flex-1 items-center justify-center py-5">
|
||||
<P className="text-center font-semibold" numberOfLines={2}>
|
||||
{item.staff.name}
|
||||
{name}
|
||||
</P>
|
||||
<SubP className="mt-1 text-center" numberOfLines={2}>
|
||||
{item.character
|
||||
? t("show.staff-as", {
|
||||
character: item.character.name,
|
||||
})
|
||||
: item.kind}
|
||||
{subtitle}
|
||||
</SubP>
|
||||
</View>
|
||||
{item.character && (
|
||||
<Poster src={item.character.image} quality="low" className="w-28" />
|
||||
{characterImage && (
|
||||
<Poster src={characterImage} quality="low" className="w-28" />
|
||||
)}
|
||||
</View>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
CharacterCard.Loader = () => (
|
||||
<View className="h-32 flex-row overflow-hidden rounded-xl bg-card">
|
||||
<Skeleton variant="custom" className="h-full w-1/3 rounded-none" />
|
||||
<View className="flex-row items-center overflow-hidden rounded-xl bg-card">
|
||||
<Poster.Loader className="w-28" />
|
||||
<View className="flex-1 items-center justify-center px-3">
|
||||
<Skeleton className="h-5 w-4/5" />
|
||||
<Skeleton className="mt-2 h-4 w-3/5" />
|
||||
<Skeleton className="mt-2 h-3 w-2/5" />
|
||||
</View>
|
||||
<Skeleton variant="custom" className="h-full w-1/3 rounded-none" />
|
||||
<Poster.Loader className="w-28" />
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -66,7 +75,21 @@ export const Staff = ({
|
||||
</View>
|
||||
)}
|
||||
Empty={<EmptyView message={t("show.staff-none")} />}
|
||||
Render={({ item }) => <CharacterCard item={item} />}
|
||||
Render={({ item }) => (
|
||||
<CharacterCard
|
||||
href={`/staff/${item.staff.slug}`}
|
||||
name={item.staff.name}
|
||||
subtitle={
|
||||
item.character
|
||||
? t("show.staff-as", {
|
||||
character: item.character.name,
|
||||
})
|
||||
: t(`show.staff-kind.${item.kind}`)
|
||||
}
|
||||
image={item.staff.image}
|
||||
characterImage={item.character?.image}
|
||||
/>
|
||||
)}
|
||||
Loader={() => <CharacterCard.Loader />}
|
||||
getItemKey={(item) =>
|
||||
`${item.staff.id}-${item.kind}-${item.character?.name ?? "none"}`
|
||||
|
||||
81
front/src/ui/staff/index.tsx
Normal file
81
front/src/ui/staff/index.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { RoleWithShow, Staff as StaffModel } from "~/models";
|
||||
import { Container, H1, Head, Poster, Skeleton, SubP } from "~/primitives";
|
||||
import { Fetch, InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { CharacterCard } from "../details/staff";
|
||||
import { EmptyView } from "../empty-view";
|
||||
|
||||
const StaffHeader = ({ slug }: { slug: string }) => {
|
||||
return (
|
||||
<Fetch
|
||||
query={StaffPage.staffQuery(slug)}
|
||||
Render={(staff) => (
|
||||
<Container className="my-4 flex-row items-center gap-4 rounded-2xl bg-card p-4">
|
||||
<Head title={staff.name} />
|
||||
<Poster src={staff.image} quality="medium" className="w-32" />
|
||||
<View className="flex-1">
|
||||
<H1 className="text-3xl">{staff.name}</H1>
|
||||
{staff.latinName && <SubP>{staff.latinName}</SubP>}
|
||||
</View>
|
||||
</Container>
|
||||
)}
|
||||
Loader={() => (
|
||||
<Container className="my-4 flex-row items-center gap-4 rounded-2xl bg-card p-4">
|
||||
<Skeleton className="h-24 w-24 rounded-xl" />
|
||||
<View className="flex-1">
|
||||
<Skeleton className="h-9 w-2/3" />
|
||||
<Skeleton className="mt-2 h-5 w-1/2" />
|
||||
</View>
|
||||
</Container>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const StaffPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [slug] = useQueryState<string>("slug", undefined!);
|
||||
|
||||
return (
|
||||
<InfiniteFetch
|
||||
query={StaffPage.rolesQuery(slug)}
|
||||
layout={{
|
||||
layout: "grid",
|
||||
numColumns: { xs: 1, md: 2, lg: 3, xl: 4 },
|
||||
gap: { xs: 8, lg: 12 },
|
||||
size: 112,
|
||||
}}
|
||||
Header={<StaffHeader slug={slug} />}
|
||||
Render={({ item }) => (
|
||||
<CharacterCard
|
||||
href={`/${item.show.kind}s/${item.show.slug}`}
|
||||
name={item.show.name}
|
||||
subtitle={
|
||||
item.character
|
||||
? t("show.staff-as", {
|
||||
character: item.character.name,
|
||||
})
|
||||
: t(`show.staff-kind.${item.kind}`)
|
||||
}
|
||||
image={item.show.poster}
|
||||
characterImage={item.character?.image}
|
||||
/>
|
||||
)}
|
||||
Loader={() => <CharacterCard.Loader />}
|
||||
Empty={<EmptyView message={t("show.staff-none")} className="py-8" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
StaffPage.staffQuery = (slug: string): QueryIdentifier<StaffModel> => ({
|
||||
path: ["api", "staff", slug],
|
||||
parser: StaffModel,
|
||||
});
|
||||
|
||||
StaffPage.rolesQuery = (slug: string): QueryIdentifier<RoleWithShow> => ({
|
||||
path: ["api", "staff", slug, "roles"],
|
||||
parser: RoleWithShow,
|
||||
infinite: true,
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user