mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-02-20 10:10:08 -05:00
Create part-of collection card
This commit is contained in:
parent
01f5b44b60
commit
31af752e2e
@ -41,6 +41,8 @@ import {
|
||||
getEntryTransQ,
|
||||
mapProgress,
|
||||
} from "../entries";
|
||||
import { Collection } from "~/models/collections";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
|
||||
export const watchStatusQ = db
|
||||
.select({
|
||||
@ -124,6 +126,42 @@ export const showRelations = {
|
||||
.where(eq(showTranslations.pk, shows.pk))
|
||||
.as("translations");
|
||||
},
|
||||
collection: ({
|
||||
languages,
|
||||
preferOriginal,
|
||||
}: {
|
||||
languages: string[];
|
||||
preferOriginal?: boolean;
|
||||
}) => {
|
||||
const collections = alias(shows, "collections");
|
||||
const colTrans = alias(showTranslations, "col_trans");
|
||||
const transQ = db
|
||||
.selectDistinctOn([colTrans.pk])
|
||||
.from(colTrans)
|
||||
.orderBy(
|
||||
colTrans.pk,
|
||||
sql`array_position(${sqlarr(languages)}, ${colTrans.language})`,
|
||||
)
|
||||
.as("t");
|
||||
|
||||
return db
|
||||
.select({
|
||||
json: jsonbBuildObject<Collection>({
|
||||
...getColumns(collections),
|
||||
...getColumns(transQ),
|
||||
...(preferOriginal && {
|
||||
poster: sql<Image>`coalesce(nullif(${collections.original}->'poster', 'null'::jsonb), ${transQ.poster})`,
|
||||
thumbnail: sql<Image>`coalesce(nullif(${collections.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`,
|
||||
banner: sql<Image>`coalesce(nullif(${collections.original}->'banner', 'null'::jsonb), ${transQ.banner})`,
|
||||
logo: sql<Image>`coalesce(nullif(${collections.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.from(collections)
|
||||
.innerJoin(transQ, eq(collections.pk, transQ.pk))
|
||||
.where(eq(collections.pk, shows.collectionPk))
|
||||
.as("collection");
|
||||
},
|
||||
studios: ({ languages }: { languages: string[] }) => {
|
||||
const studioTransQ = db
|
||||
.selectDistinctOn([studioTranslations.pk])
|
||||
@ -304,7 +342,10 @@ export async function getShows({
|
||||
|
||||
watchStatus: getColumns(watchStatusQ),
|
||||
|
||||
...buildRelations(relations, showRelations, { languages }),
|
||||
...buildRelations(relations, showRelations, {
|
||||
languages,
|
||||
preferOriginal,
|
||||
}),
|
||||
})
|
||||
.from(shows)
|
||||
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
|
||||
|
||||
@ -77,10 +77,13 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
preferOriginal: t.Optional(
|
||||
t.Boolean({ description: desc.preferOriginal }),
|
||||
),
|
||||
with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), {
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
}),
|
||||
with: t.Array(
|
||||
t.UnionEnum(["translations", "collection", "studios", "videos"]),
|
||||
{
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
},
|
||||
),
|
||||
}),
|
||||
headers: t.Object({
|
||||
"accept-language": AcceptLanguage(),
|
||||
@ -119,10 +122,13 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
preferOriginal: t.Optional(
|
||||
t.Boolean({ description: desc.preferOriginal }),
|
||||
),
|
||||
with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), {
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
}),
|
||||
with: t.Array(
|
||||
t.UnionEnum(["translations", "collection", "studios", "videos"]),
|
||||
{
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
},
|
||||
),
|
||||
}),
|
||||
response: {
|
||||
302: t.Void({
|
||||
|
||||
@ -78,7 +78,13 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
||||
t.Boolean({ description: desc.preferOriginal }),
|
||||
),
|
||||
with: t.Array(
|
||||
t.UnionEnum(["translations", "studios", "firstEntry", "nextEntry"]),
|
||||
t.UnionEnum([
|
||||
"translations",
|
||||
"collection",
|
||||
"studios",
|
||||
"firstEntry",
|
||||
"nextEntry",
|
||||
]),
|
||||
{
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
@ -123,7 +129,13 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
||||
t.Boolean({ description: desc.preferOriginal }),
|
||||
),
|
||||
with: t.Array(
|
||||
t.UnionEnum(["translations", "studios", "firstEntry", "nextEntry"]),
|
||||
t.UnionEnum([
|
||||
"translations",
|
||||
"collection",
|
||||
"studios",
|
||||
"firstEntry",
|
||||
"nextEntry",
|
||||
]),
|
||||
{
|
||||
default: [],
|
||||
description: "Include related resources in the response.",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { t } from "elysia";
|
||||
import type { Prettify } from "~/utils";
|
||||
import { SeedCollection } from "./collections";
|
||||
import { Collection, SeedCollection } from "./collections";
|
||||
import { bubble, bubbleImages, registerExamples } from "./examples";
|
||||
import { SeedStaff } from "./staff";
|
||||
import { SeedStudio, Studio } from "./studio";
|
||||
@ -70,6 +70,7 @@ export const FullMovie = t.Intersect([
|
||||
t.Object({
|
||||
translations: t.Optional(TranslationRecord(MovieTranslation)),
|
||||
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||
collection: t.Optional(t.Nullable(Collection)),
|
||||
studios: t.Optional(t.Array(Studio)),
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { t } from "elysia";
|
||||
import type { Prettify } from "~/utils";
|
||||
import { SeedCollection } from "./collections";
|
||||
import { Collection, SeedCollection } from "./collections";
|
||||
import { Entry, SeedEntry, SeedExtra } from "./entry";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
|
||||
import { SeedSeason } from "./season";
|
||||
@ -84,6 +84,7 @@ export const FullSerie = t.Intersect([
|
||||
Serie,
|
||||
t.Object({
|
||||
translations: t.Optional(TranslationRecord(SerieTranslation)),
|
||||
collection: t.Optional(t.Nullable(Collection)),
|
||||
studios: t.Optional(t.Array(Studio)),
|
||||
firstEntry: t.Optional(t.Nullable(Entry)),
|
||||
nextEntry: t.Optional(t.Nullable(Entry)),
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"episode-none": "There is no episodes in this season",
|
||||
"episodeNoMetadata": "No metadata available",
|
||||
"tags": "Tags",
|
||||
"tags-none": "No tags available",
|
||||
"links": "Links",
|
||||
"jumpToSeason": "Jump to season",
|
||||
"partOf": "Part of the",
|
||||
|
||||
@ -13,7 +13,7 @@ export const itemMap = (
|
||||
subtitle: item.kind !== "collection" ? getDisplayDate(item) : null,
|
||||
href: item.href,
|
||||
poster: item.poster,
|
||||
thumbnail: item.thumbnail,
|
||||
banner: item.banner ?? item.thumbnail,
|
||||
watchStatus:
|
||||
item.kind !== "collection" ? (item.watchStatus?.status ?? null) : null,
|
||||
watchPercent:
|
||||
|
||||
@ -22,7 +22,7 @@ export const ItemList = ({
|
||||
kind,
|
||||
name,
|
||||
subtitle,
|
||||
thumbnail,
|
||||
banner,
|
||||
poster,
|
||||
watchStatus,
|
||||
availableCount,
|
||||
@ -36,7 +36,7 @@ export const ItemList = ({
|
||||
name: string;
|
||||
subtitle: string | null;
|
||||
poster: KImage | null;
|
||||
thumbnail: KImage | null;
|
||||
banner: KImage | null;
|
||||
watchStatus: WatchStatusV | null;
|
||||
availableCount?: number | null;
|
||||
seenCount?: number | null;
|
||||
@ -56,7 +56,7 @@ export const ItemList = ({
|
||||
{...props}
|
||||
>
|
||||
<ImageBackground
|
||||
src={thumbnail}
|
||||
src={banner}
|
||||
quality="medium"
|
||||
className="h-full w-full flex-row items-center justify-evenly"
|
||||
>
|
||||
|
||||
@ -4,6 +4,7 @@ import { Genre } from "./utils/genre";
|
||||
import { KImage } from "./utils/images";
|
||||
import { Metadata } from "./utils/metadata";
|
||||
import { zdate } from "./utils/utils";
|
||||
import { Collection } from "./collection";
|
||||
|
||||
export const Movie = z
|
||||
.object({
|
||||
@ -64,6 +65,8 @@ export const Movie = z
|
||||
percent: z.number().int().gte(0).lte(100),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
collection: Collection.optional().nullable(),
|
||||
})
|
||||
.transform((x) => ({
|
||||
...x,
|
||||
|
||||
@ -5,6 +5,7 @@ import { Genre } from "./utils/genre";
|
||||
import { KImage } from "./utils/images";
|
||||
import { Metadata } from "./utils/metadata";
|
||||
import { zdate } from "./utils/utils";
|
||||
import { Collection } from "./collection";
|
||||
|
||||
export const Serie = z
|
||||
.object({
|
||||
@ -62,6 +63,8 @@ export const Serie = z
|
||||
seenCount: z.number().int().gte(0),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
collection: Collection.optional().nullable(),
|
||||
})
|
||||
.transform((x) => {
|
||||
const entry = x.nextEntry ?? x.firstEntry;
|
||||
|
||||
@ -1,24 +1,15 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const Container = <AsProps = ViewProps>({
|
||||
className,
|
||||
as,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
as?: ComponentType<AsProps>;
|
||||
} & AsProps) => {
|
||||
const As = as ?? View;
|
||||
export const Container = ({ className, ...props }: ViewProps) => {
|
||||
return (
|
||||
<As
|
||||
<View
|
||||
className={cn(
|
||||
"flex w-full self-center px-4",
|
||||
"sm:w-xl md:w-3xl lg:w-5xl xl:w-7xl",
|
||||
className,
|
||||
)}
|
||||
{...(props as AsProps)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -21,6 +21,7 @@ export const ImageBackground = ({
|
||||
quality,
|
||||
alt,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
src: KImage | null;
|
||||
@ -32,7 +33,11 @@ export const ImageBackground = ({
|
||||
const { apiUrl, authToken } = useToken();
|
||||
|
||||
if (!src) {
|
||||
return <View className={cn("overflow-hidden bg-gray-300", className)} />;
|
||||
return (
|
||||
<View className={cn("overflow-hidden bg-gray-300", className)}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const uri = `${apiUrl}${src[quality ?? "high"]}`;
|
||||
@ -54,7 +59,9 @@ export const ImageBackground = ({
|
||||
className={cn("overflow-hidden bg-gray-300", className)}
|
||||
imageStyle={{ width: "100%", height: "100%", margin: 0, padding: 0 }}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</ImgBg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
Header,
|
||||
fetchMore = true,
|
||||
contentContainerStyle,
|
||||
margin = true,
|
||||
...props
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
@ -47,6 +48,7 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
contentContainerStyle?: ViewStyle;
|
||||
onScroll?: LegendListProps["onScroll"];
|
||||
scrollEventThrottle?: LegendListProps["scrollEventThrottle"];
|
||||
margin?: boolean;
|
||||
}): JSX.Element | null => {
|
||||
const { numColumns, size, gap } = useBreakpointMap(layout);
|
||||
const oldItems = useRef<Data[] | undefined>(undefined);
|
||||
@ -101,12 +103,15 @@ export const InfiniteFetch = <Data, Type extends string = string>({
|
||||
}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
...contentContainerStyle,
|
||||
gap,
|
||||
marginLeft: numColumns > 1 ? gap : 0,
|
||||
marginRight: numColumns > 1 ? gap : 0,
|
||||
}}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
...contentContainerStyle,
|
||||
gap,
|
||||
},
|
||||
margin
|
||||
? { marginLeft: gap, marginRight: gap }
|
||||
: { marginLeft: 0, marginRight: 0 },
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,30 +1,77 @@
|
||||
import { useState } from "react";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { itemMap } from "~/components/items";
|
||||
import { ItemDetails } from "~/components/items/item-details";
|
||||
import { Show } from "~/models";
|
||||
import { Container } from "~/primitives";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { useQueryState } from "~/utils";
|
||||
import { HeaderBackground, useScrollNavbar } from "../navbar";
|
||||
import { Header } from "./header";
|
||||
import { SvgWave } from "./serie";
|
||||
|
||||
const CollectionHeader = ({
|
||||
slug,
|
||||
onImageLayout,
|
||||
}: {
|
||||
slug: string;
|
||||
onImageLayout?: ViewProps["onLayout"];
|
||||
}) => {
|
||||
return (
|
||||
<View className="bg-background">
|
||||
<Header kind="collection" slug={slug} onImageLayout={onImageLayout} />
|
||||
<SvgWave className="flex-1 shrink-0 fill-card" />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionDetails = () => {
|
||||
const [slug] = useQueryState("slug", undefined!);
|
||||
const insets = useSafeAreaInsets();
|
||||
const [imageHeight, setHeight] = useState(300);
|
||||
const { scrollHandler, headerProps } = useScrollNavbar({ imageHeight });
|
||||
const { scrollHandler, headerProps } = useScrollNavbar({
|
||||
imageHeight,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="flex-1 bg-card">
|
||||
<HeaderBackground {...headerProps} />
|
||||
<Animated.ScrollView
|
||||
<InfiniteFetch
|
||||
query={CollectionDetails.query(slug)}
|
||||
layout={ItemDetails.layout}
|
||||
Render={({ item }) => (
|
||||
<ItemDetails
|
||||
{...itemMap(item)}
|
||||
tagline={item.tagline}
|
||||
description={item.description}
|
||||
genres={item.genres}
|
||||
playHref={item.kind !== "collection" ? item.playHref : null}
|
||||
/>
|
||||
)}
|
||||
Loader={() => (
|
||||
<Container>
|
||||
<ItemDetails.Loader />
|
||||
</Container>
|
||||
)}
|
||||
Header={() => (
|
||||
<CollectionHeader
|
||||
slug={slug}
|
||||
onImageLayout={(e) => setHeight(e.nativeEvent.layout.height)}
|
||||
/>
|
||||
)}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={16}
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom }}
|
||||
>
|
||||
<Header
|
||||
kind="collection"
|
||||
slug={slug}
|
||||
onImageLayout={(e) => setHeight(e.nativeEvent.layout.height)}
|
||||
/>
|
||||
</Animated.ScrollView>
|
||||
</>
|
||||
contentContainerStyle={{
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
margin={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
CollectionDetails.query = (slug: string): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
path: ["api", "collections", slug, "shows"],
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
@ -41,6 +41,7 @@ import {
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { Fetch, type QueryIdentifier } from "~/query";
|
||||
import { cn, displayRuntime, getDisplayDate } from "~/utils";
|
||||
import { PartOf } from "./part-of";
|
||||
|
||||
const ButtonList = ({
|
||||
kind,
|
||||
@ -260,7 +261,7 @@ const Description = ({
|
||||
description: string | null;
|
||||
tags: string[];
|
||||
genres: Genre[];
|
||||
studios: Studio[];
|
||||
studios: Studio[] | null;
|
||||
externalIds: Metadata;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@ -268,7 +269,7 @@ const Description = ({
|
||||
return (
|
||||
<Container className="py-10" {...props}>
|
||||
<View className="flex-1 flex-col-reverse sm:flex-row">
|
||||
<P className="py-5 text-justify">
|
||||
<P className="flex-1 py-5 text-justify">
|
||||
{description ?? t("show.noOverview")}
|
||||
</P>
|
||||
<View className="basis-1/5 flex-row xl:mt-[-100px]">
|
||||
@ -293,27 +294,33 @@ const Description = ({
|
||||
</View>
|
||||
<View className="mt-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.tags")}:</P>
|
||||
{tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag && capitalize(tag)}
|
||||
href={`/search?q=${tag}`}
|
||||
size="small"
|
||||
className="m-1"
|
||||
/>
|
||||
))}
|
||||
{tags.length ? (
|
||||
tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag && capitalize(tag)}
|
||||
href={`/search?q=${tag}`}
|
||||
size="small"
|
||||
className="m-1"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<P>{t("show.tags-none")}</P>
|
||||
)}
|
||||
</View>
|
||||
<P className="my-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.studios")}:</P>
|
||||
{studios.map((x, i) => (
|
||||
<Fragment key={x.id}>
|
||||
{i !== 0 && ","}
|
||||
<A href={x.slug} className="ml-2">
|
||||
{x.name}
|
||||
</A>
|
||||
</Fragment>
|
||||
))}
|
||||
</P>
|
||||
{studios !== null && (
|
||||
<P className="my-5 flex-row flex-wrap items-center">
|
||||
<P className="mr-1">{t("show.studios")}:</P>
|
||||
{studios.map((x, i) => (
|
||||
<Fragment key={x.id}>
|
||||
{i !== 0 && ","}
|
||||
<A href={x.slug} className="ml-2">
|
||||
{x.name}
|
||||
</A>
|
||||
</Fragment>
|
||||
))}
|
||||
</P>
|
||||
)}
|
||||
<View className="flex-row flex-wrap items-center">
|
||||
<P className="mr-1 text-center">{t("show.links")}:</P>
|
||||
{Object.entries(externalIds)
|
||||
@ -425,13 +432,20 @@ export const Header = ({
|
||||
description={data.description}
|
||||
tags={data.tags}
|
||||
genres={data.genres}
|
||||
studios={data.kind !== "collection" ? data.studios! : []}
|
||||
studios={data.kind !== "collection" ? data.studios! : null}
|
||||
externalIds={data.externalId}
|
||||
/>
|
||||
|
||||
{/* {type === "show" && ( */}
|
||||
{/* <ShowWatchStatusCard {...(data?.watchStatus as any)} /> */}
|
||||
{/* )} */}
|
||||
{data.kind !== "collection" && data.collection && (
|
||||
<Container className="mb-4">
|
||||
<PartOf
|
||||
name={data.collection.name}
|
||||
description={data.collection.description}
|
||||
banner={data.collection.banner ?? data.collection.thumbnail}
|
||||
href={data.collection.href}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
Loader={() => (
|
||||
@ -459,7 +473,7 @@ Header.query = (
|
||||
path: ["api", `${kind}s`, slug],
|
||||
params: {
|
||||
with: [
|
||||
...(kind !== "collection" ? ["studios"] : []),
|
||||
...(kind !== "collection" ? ["collection", "studios"] : []),
|
||||
...(kind === "serie" ? ["firstEntry", "nextEntry"] : []),
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,110 +1,47 @@
|
||||
import {
|
||||
type Collection,
|
||||
CollectionP,
|
||||
type KyooImage,
|
||||
type QueryIdentifier,
|
||||
useInfiniteFetch,
|
||||
} from "@kyoo/models";
|
||||
import {
|
||||
Container,
|
||||
focusReset,
|
||||
GradientImageBackground,
|
||||
H2,
|
||||
ImageBackground,
|
||||
Link,
|
||||
P,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Theme, useYoshiki } from "yoshiki/native";
|
||||
import { View } from "react-native";
|
||||
import type { KImage } from "~/models";
|
||||
import { H2, ImageBackground, Link, P } from "~/primitives";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
export const PartOf = ({
|
||||
name,
|
||||
overview,
|
||||
thumbnail,
|
||||
description,
|
||||
banner,
|
||||
href,
|
||||
className,
|
||||
}: {
|
||||
name: string;
|
||||
overview: string | null;
|
||||
thumbnail: KyooImage | null;
|
||||
description: string | null;
|
||||
banner: KImage | null;
|
||||
href: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki("part-of-collection");
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
{...css({
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
borderWidth: ts(0.5),
|
||||
borderStyle: "solid",
|
||||
borderColor: (theme) => theme.background,
|
||||
fover: {
|
||||
self: { ...focusReset, borderColor: (theme: Theme) => theme.accent },
|
||||
title: { textDecorationLine: "underline" },
|
||||
},
|
||||
})}
|
||||
className={cn(
|
||||
"group flex-1 overflow-hidden rounded-xl ring-accent hover:ring-3 focus-visible:ring-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<GradientImageBackground
|
||||
src={thumbnail}
|
||||
alt=""
|
||||
quality="medium"
|
||||
gradient={{
|
||||
colors: [theme.darkOverlay, "transparent"],
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 1, y: 0 },
|
||||
}}
|
||||
{...css({
|
||||
padding: ts(3),
|
||||
})}
|
||||
>
|
||||
<H2 {...css("title")}>
|
||||
<ImageBackground src={banner} quality="high" alt="" className="p-6">
|
||||
<View className="absolute inset-0 bg-linear-to-b from-transparent to-slate-950/70" />
|
||||
<H2
|
||||
className={cn(
|
||||
"py-2",
|
||||
"text-slate-200 dark:text-slate-200",
|
||||
"group-focus-within:underline group-hover:underline",
|
||||
)}
|
||||
>
|
||||
{t("show.partOf")} {name}
|
||||
</H2>
|
||||
<P {...css({ textAlign: "justify" })}>{overview}</P>
|
||||
</GradientImageBackground>
|
||||
<P className="text-justify text-slate-400 dark:text-slate-400">
|
||||
{description}
|
||||
</P>
|
||||
</ImageBackground>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailsCollections = ({
|
||||
type,
|
||||
slug,
|
||||
}: {
|
||||
type: "movie" | "show";
|
||||
slug: string;
|
||||
}) => {
|
||||
const { items } = useInfiniteFetch(DetailsCollections.query(type, slug));
|
||||
const { css } = useYoshiki();
|
||||
|
||||
// Since most items dont have collections, not having a skeleton reduces layout shifts.
|
||||
if (!items) return null;
|
||||
|
||||
return (
|
||||
<Container {...css({ marginY: ts(2) })}>
|
||||
{items.map((x) => (
|
||||
<PartOf
|
||||
key={x.id}
|
||||
name={x.name}
|
||||
overview={x.overview}
|
||||
thumbnail={x.thumbnail}
|
||||
href={x.href}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
DetailsCollections.query = (
|
||||
type: "movie" | "show",
|
||||
slug: string,
|
||||
): QueryIdentifier<Collection> => ({
|
||||
parser: CollectionP,
|
||||
path: [type, slug, "collections"],
|
||||
params: {
|
||||
limit: 0,
|
||||
},
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
@ -123,34 +123,33 @@ export const EntryList = ({
|
||||
.filter((x) => x !== null)
|
||||
}
|
||||
placeholderCount={5}
|
||||
Render={({ item }) =>
|
||||
item.kind === "season" ? (
|
||||
<Container
|
||||
as={SeasonHeader}
|
||||
serieSlug={slug}
|
||||
name={item.name}
|
||||
seasonNumber={item.seasonNumber}
|
||||
seasons={seasons ?? []}
|
||||
/>
|
||||
) : (
|
||||
<Container
|
||||
as={EntryLine}
|
||||
{...item}
|
||||
// Don't display "Go to serie"
|
||||
videosCount={item.videos.length}
|
||||
serieSlug={null}
|
||||
displayNumber={entryDisplayNumber(item)}
|
||||
watchedPercent={item.progress.percent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Loader={({ index }) =>
|
||||
index === 0 ? (
|
||||
<Container as={SeasonHeader.Loader} />
|
||||
) : (
|
||||
<Container as={EntryLine.Loader} />
|
||||
)
|
||||
}
|
||||
Render={({ item }) => (
|
||||
<Container>
|
||||
{item.kind === "season" ? (
|
||||
<SeasonHeader
|
||||
serieSlug={slug}
|
||||
name={item.name}
|
||||
seasonNumber={item.seasonNumber}
|
||||
seasons={seasons ?? []}
|
||||
/>
|
||||
) : (
|
||||
<EntryLine
|
||||
{...item}
|
||||
// Don't display "Go to serie"
|
||||
videosCount={item.videos.length}
|
||||
serieSlug={null}
|
||||
displayNumber={entryDisplayNumber(item)}
|
||||
watchedPercent={item.progress.percent}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)}
|
||||
Loader={({ index }) => (
|
||||
<Container>
|
||||
{index === 0 ? <SeasonHeader.Loader /> : <EntryLine.Loader />}
|
||||
</Container>
|
||||
)}
|
||||
margin={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -74,7 +74,6 @@ const SerieHeader = ({
|
||||
}}
|
||||
Loader={NextUp.Loader}
|
||||
/>
|
||||
{/* <DetailsCollections type="serie" slug={slug} /> */}
|
||||
{/* <Staff slug={slug} /> */}
|
||||
<SvgWave className="flex-1 shrink-0 fill-card" />
|
||||
</View>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user