first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import {TITLE_MODEL} from '@app/titles/models/title';
import {ChannelContentModel} from '@app/admin/channels/channel-content-config';
import {NEWS_ARTICLE_MODEL} from '@app/titles/models/news-article';
import {ContentGridProps} from '@app/channels/content-grid/content-grid-layout';
import {Person, PERSON_MODEL} from '@app/titles/models/person';
import {PersonPoster} from '@app/people/person-poster/person-poster';
import {PersonLink} from '@app/people/person-link';
import {PersonAge} from '@app/people/person-age';
import {NewsArticleGridItem} from '@app/news/news-article-grid-item';
import {
TitleLandscapeGridItem,
TitlePortraitGridItem,
} from '@app/channels/content-grid/title-grid-item';
interface Props {
item: ChannelContentModel;
variant?: ContentGridProps['variant'];
}
export function ChannelContentGridItem({item, variant}: Props) {
switch (item.model_type) {
case TITLE_MODEL:
return variant === 'landscape' ? (
<TitleLandscapeGridItem item={item} />
) : (
<TitlePortraitGridItem item={item} />
);
case PERSON_MODEL:
return <PersonGridItem item={item} />;
case NEWS_ARTICLE_MODEL:
return <NewsArticleGridItem article={item} />;
default:
return null;
}
}
interface PersonGridItemProps {
item: Person;
}
function PersonGridItem({item}: PersonGridItemProps) {
return (
<div>
<PersonPoster person={item} srcSize="md" size="w-full" rounded />
<div className="mt-10 text-center text-sm">
<PersonLink person={item} className="block text-base font-medium" />
<div>
<PersonAge person={item} showRange />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,97 @@
import {InfiniteScrollSentinel} from '@common/ui/infinite-scroll/infinite-scroll-sentinel';
import React, {Fragment} from 'react';
import {ChannelContentProps} from '@app/channels/channel-content';
import {useInfiniteChannelContent} from '@common/channels/requests/use-infinite-channel-content';
import {ChannelHeader} from '@app/channels/channel-header/channel-header';
import {
ContentGridLayout,
ContentGridProps,
} from '@app/channels/content-grid/content-grid-layout';
import {ChannelContentGridItem} from '@app/channels/content-grid/channel-content-grid-item';
import {ChannelContentModel} from '@app/admin/channels/channel-content-config';
import {useChannelContent} from '@common/channels/requests/use-channel-content';
import clsx from 'clsx';
import {
PaginationControls,
PaginationControlsType,
} from '@common/ui/navigation/pagination-controls';
interface ChannelContentGridProps extends ChannelContentProps {
variant?: ContentGridProps['variant'];
}
export function ChannelContentGrid(props: ChannelContentGridProps) {
const isInfiniteScroll =
!props.isNested &&
(!props.channel.config.paginationType ||
props.channel.config.paginationType === 'infiniteScroll');
return (
<Fragment>
<ChannelHeader {...props} />
{isInfiniteScroll ? (
<InfiniteScrollGrid {...props} />
) : (
<PaginatedGrid {...props} />
)}
</Fragment>
);
}
function InfiniteScrollGrid({channel, variant}: ChannelContentGridProps) {
const query = useInfiniteChannelContent<ChannelContentModel>(channel);
return (
<div
className={clsx('transition-opacity', query.isReloading && 'opacity-70')}
>
<ContentGrid content={query.items} variant={variant} />
<InfiniteScrollSentinel query={query} />
</div>
);
}
function PaginatedGrid({channel, variant, isNested}: ChannelContentGridProps) {
const shouldPaginate = !isNested;
const query = useChannelContent(channel, null, {paginate: shouldPaginate});
return (
<div
className={clsx(
'transition-opacity',
query.isPlaceholderData && 'opacity-70',
)}
>
{shouldPaginate && (
<PaginationControls
pagination={query.data}
type={channel.config.paginationType as PaginationControlsType}
className="mb-24"
/>
)}
<ContentGrid content={query.data?.data} variant={variant} />
{shouldPaginate && (
<PaginationControls
pagination={query.data}
type={channel.config.paginationType as PaginationControlsType}
className="mt-24"
scrollToTop
/>
)}
</div>
);
}
interface ContentProps {
content: ChannelContentModel[] | undefined;
variant: ContentGridProps['variant'];
}
export function ContentGrid({content = [], variant}: ContentProps) {
return (
<ContentGridLayout variant={variant}>
{content.map(item => (
<ChannelContentGridItem
key={`${item.id}-${item.model_type}`}
item={item}
variant={variant}
/>
))}
</ContentGridLayout>
);
}

View File

@@ -0,0 +1,27 @@
.content-grid-landscape {
--nVisibleItems: 1;
}
@media (min-width: 280px) {
.content-grid-landscape {
--nVisibleItems: 2;
}
}
@media (min-width: 786px) {
.content-grid-landscape {
--nVisibleItems: 3;
}
}
@container (min-width: 600px) {
.content-grid-landscape {
--nVisibleItems: 3;
}
}
@container (min-width: 1200px) {
.content-grid-landscape {
--nVisibleItems: 4;
}
}

View File

@@ -0,0 +1,30 @@
import {ReactNode} from 'react';
import clsx from 'clsx';
export interface ContentGridProps {
className?: string;
children: ReactNode;
variant?: 'portrait' | 'landscape';
gridCols?: string;
}
export function ContentGridLayout({
children,
className,
variant,
gridCols = 'grid-cols-[repeat(var(--nVisibleItems),minmax(0,1fr))]',
}: ContentGridProps) {
return (
<div
className={clsx(
'grid gap-24',
gridCols,
className,
variant === 'landscape'
? 'content-grid-landscape'
: 'content-grid-portrait'
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,52 @@
.content-grid-portrait {
--nVisibleItems: 1;
}
@media (min-width: 280px) {
.content-grid-portrait {
--nVisibleItems: 2;
}
}
@media (min-width: 786px) {
.content-grid-portrait {
--nVisibleItems: 3;
}
}
@media (min-width: 1024px) {
.content-grid-portrait {
--nVisibleItems: 4;
}
}
@media (min-width: 1280px) {
.content-grid-portrait {
--nVisibleItems: 6;
}
}
@container (min-width: 400px) {
.content-grid-portrait {
--nVisibleItems: 3;
}
}
@container (min-width: 600px) {
.content-grid-portrait {
--nVisibleItems: 4;
}
}
@container (min-width: 900px) {
.content-grid-portrait {
--nVisibleItems: 5;
}
}
@container (min-width: 1200px) {
.content-grid-portrait {
--nVisibleItems: 6;
}
}

View File

@@ -0,0 +1,37 @@
import {TitleRating} from '@app/reviews/title-rating';
import React from 'react';
import {Episode} from '@app/titles/models/episode';
import {EpisodePoster} from '@app/episodes/episode-poster/episode-poster';
import {Title} from '@app/titles/models/title';
import {TitleLinkWithEpisodeNumber} from '@app/titles/title-link';
export interface EpisodePortraitGridItemProps {
item: Episode;
title: Title;
rating?: number;
}
export function EpisodePortraitGridItem({
item,
title,
rating,
}: EpisodePortraitGridItemProps) {
return (
<div>
<EpisodePoster
episode={item}
title={title}
srcSize="lg"
aspect="aspect-poster"
showPlayButton
/>
<div className="mt-10 text-sm">
<TitleRating score={rating ?? item.rating} className="mb-4" />
<TitleLinkWithEpisodeNumber
title={title}
episode={item}
className="block font-medium text-base"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,86 @@
import {Title} from '@app/titles/models/title';
import {TitlePoster} from '@app/titles/title-poster/title-poster';
import {TitleRating} from '@app/reviews/title-rating';
import {TitleLink} from '@app/titles/title-link';
import {ReactNode} from 'react';
import {TitleBackdrop} from '@app/titles/title-poster/title-backdrop';
import {BulletSeparatedItems} from '@app/titles/bullet-separated-items';
import {FormattedDate} from '@common/i18n/formatted-date';
export interface TitlePortraitGridItemProps {
item: Title;
rating?: number;
description?: ReactNode;
}
export function TitlePortraitGridItem({
item,
rating,
description,
}: TitlePortraitGridItemProps) {
return (
<div>
<div className="relative">
<TitlePoster title={item} srcSize="md" showPlayButton />
</div>
<div className="mt-10 text-sm">
<RatingOrReleaseDate title={item} rating={rating} className="mb-4" />
<TitleLink title={item} className="block text-base font-medium" />
{description ? <div className="mt-4">{description}</div> : null}
</div>
</div>
);
}
export function TitleLandscapeGridItem({item}: TitlePortraitGridItemProps) {
return (
<div>
<TitleBackdrop
title={item}
srcSize="lg"
size="w-full"
className="rounded"
wrapWithLink
showPlayButton
/>
<div className="mt-10 text-sm">
<TitleLink
title={item}
className="mb-4 block text-base font-semibold"
/>
<BulletSeparatedItems className="mb-4">
{item.release_date && <FormattedDate date={item.release_date} />}
{item.certification && (
<div className="uppercase">{item.certification}</div>
)}
</BulletSeparatedItems>
<TitleRating score={item.rating} className="mb-4" />
</div>
</div>
);
}
interface RatingOrReleaseDateProps {
title: Title;
rating?: number;
className?: string;
}
function RatingOrReleaseDate({
title,
rating,
className,
}: RatingOrReleaseDateProps) {
if (!rating) {
rating = title.rating;
}
if (rating) {
return <TitleRating score={rating} className={className} />;
}
if (title.release_date) {
return (
<div className={className}>
<FormattedDate date={title.release_date} />
</div>
);
}
return null;
}