feat: Enhance support for featured theme (#240)

This commit is contained in:
Kyrch
2025-12-10 18:22:16 -03:00
committed by GitHub
parent 735704377c
commit 937d05053a
9 changed files with 14206 additions and 14105 deletions
-4
View File
@@ -30,10 +30,6 @@ This is a list of all available options:
; The URL to the AnimeThemes API which will be used on the server.
ANIMETHEMES_API_URL=http://localhost
; If specified, this API key will be used to make requests to the AnimeThemes API.
; This is used to by-pass rate limiting.
ANIMETHEMES_API_KEY=...
; Set to any truthy value to activate minimal build mode.
; In minimal build mode, only a small subset of pages get prerendered at build time.
MINIMAL_BUILD=true
+588 -470
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -44,7 +44,7 @@
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"motion": "^11.15.0",
"next": "15.3.0",
"next": "^15.3.0",
"next-mdx-remote": "^5.0.0",
"p-limit": "^3.1.0",
"picocolors": "^1.0.0",
@@ -71,7 +71,7 @@
"@types/node": "^22.10.5",
"@types/react": "19.1.1",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"esbuild": "^0.24.2",
"esbuild": "^0.27.1",
"eslint": "^8.57.0",
"eslint-config-next": "15.3.0",
"eslint-config-prettier": "^9.1.0",
+28 -30
View File
@@ -5,8 +5,12 @@ import Link from "next/link";
import gql from "graphql-tag";
import { ThemeSummaryCard } from "@/components/card/ThemeSummaryCard";
import type { FeaturedThemeThemeFragment } from "@/generated/graphql";
import {
VideoSummaryCard,
VideoSummaryCardFragmentEntry,
VideoSummaryCardFragmentVideo,
} from "@/components/card/VideoSummaryCard";
import type { FeaturedThemeEntryFragment, FeaturedThemeVideoFragment } from "@/generated/graphql";
import useCompatability from "@/hooks/useCompatability";
import useSetting from "@/hooks/useSetting";
import { fetchRandomGrill } from "@/lib/client/randomGrill";
@@ -120,13 +124,14 @@ const StyledGrill = styled.img`
const Box = styled.div``;
interface FeaturedThemeProps {
theme: FeaturedThemeThemeFragment;
entry: FeaturedThemeEntryFragment;
video: FeaturedThemeVideoFragment;
hasGrill?: boolean;
card?: ReactNode;
onPlay?(): void;
}
export function FeaturedTheme({ theme, hasGrill = true, card, onPlay }: FeaturedThemeProps) {
export function FeaturedTheme({ entry, video, hasGrill = true, card, onPlay }: FeaturedThemeProps) {
const [grill, setGrill] = useState<string | null>(null);
const [featuredThemePreview] = useSetting(FeaturedThemePreview);
@@ -140,14 +145,14 @@ export function FeaturedTheme({ theme, hasGrill = true, card, onPlay }: Featured
const featuredThemeSummaryCard =
featuredThemePreview !== FeaturedThemePreview.DISABLED ? (
<StyledCenter>{card ?? <ThemeSummaryCard theme={theme} />}</StyledCenter>
<StyledCenter>{card ?? <VideoSummaryCard entry={entry} video={video} />}</StyledCenter>
) : (
card ?? <ThemeSummaryCard theme={theme} />
(card ?? <VideoSummaryCard entry={entry} video={video} />)
);
return (
<FeaturedThemeWrapper>
<FeaturedThemeBackground theme={theme} onPlay={onPlay} />
<FeaturedThemeBackground entry={entry} video={video} onPlay={onPlay} />
{featuredThemePreview !== FeaturedThemePreview.DISABLED && grill && (
<StyledGrillContainer>
<StyledGrill src={grill} />
@@ -158,26 +163,15 @@ export function FeaturedTheme({ theme, hasGrill = true, card, onPlay }: Featured
);
}
function FeaturedThemeBackground({ theme, onPlay }: FeaturedThemeProps) {
function FeaturedThemeBackground({ entry, video, onPlay }: FeaturedThemeProps) {
const [featuredThemePreview] = useSetting(FeaturedThemePreview);
const { canPlayVideo } = useCompatability();
const [fallbackToCover, setFallbackToCover] = useState(false);
const { smallCover: featuredCover } = extractImages(theme.anime);
const { smallCover: featuredCover } = extractImages(entry.theme.anime);
if (!theme.anime || !theme.entries.length) {
return null;
}
const videoSlug = createVideoSlug(entry.theme, entry, video);
const entry = theme.entries[0];
if (!entry.videos.length) {
return null;
}
const video = entry.videos[0];
const videoSlug = createVideoSlug(theme, entry, video);
const href = `/anime/${theme.anime.slug}/${videoSlug}`;
const href = `/anime/${entry.theme.anime.slug}/${videoSlug}`;
if (featuredThemePreview === FeaturedThemePreview.VIDEO && canPlayVideo && !fallbackToCover) {
return (
@@ -199,20 +193,24 @@ function FeaturedThemeBackground({ theme, onPlay }: FeaturedThemeProps) {
}
FeaturedTheme.fragments = {
theme: gql`
${ThemeSummaryCard.fragments.theme}
entry: gql`
${VideoSummaryCardFragmentEntry}
${extractImages.fragments.resourceWithImages}
fragment FeaturedThemeTheme on Theme {
...ThemeSummaryCardTheme
fragment FeaturedThemeEntry on Entry {
...VideoSummaryCardEntry
theme {
anime {
...extractImagesResourceWithImages
}
entries {
videos {
basename
}
}
}
`,
video: gql`
${VideoSummaryCardFragmentVideo}
fragment FeaturedThemeVideo on Video {
...VideoSummaryCardVideo
}
`,
};
+15 -16
View File
@@ -813,22 +813,23 @@ export type PlaylistTrackRemoveDialogEntryFragment = {
};
};
export type FeaturedThemeThemeFragment = {
type: string;
sequence: number | null;
id: number;
anime: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> };
entries: Array<{
export type FeaturedThemeEntryFragment = {
id: number;
version: number | null;
videos: Array<{ id: number; basename: string; tags: string; audio: { basename: string } }>;
}>;
theme: {
id: number;
type: string;
sequence: number | null;
group: { name: string; slug: string } | null;
anime: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> };
song: {
title: string | null;
performances: Array<{ alias: string | null; as: string | null; artist: { slug: string; name: string } }>;
} | null;
};
};
export type FeaturedThemeVideoFragment = { id: number; basename: string; tags: string; audio: { basename: string } };
export type AnimeThemeFilterThemeFragment = {
type: string;
@@ -2495,17 +2496,14 @@ export type HomePageQueryVariables = Exact<{ [key: string]: never }>;
export type HomePageQuery = {
featuredTheme: {
entry: {
theme: {
type: string;
sequence: number | null;
id: number;
anime: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> };
entries: Array<{
id: number;
version: number | null;
videos: Array<{ id: number; basename: string; tags: string; audio: { basename: string } }>;
}>;
theme: {
id: number;
type: string;
sequence: number | null;
group: { name: string; slug: string } | null;
anime: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> };
song: {
title: string | null;
performances: Array<{
@@ -2516,6 +2514,7 @@ export type HomePageQuery = {
} | null;
};
} | null;
video: { id: number; basename: string; tags: string; audio: { basename: string } } | null;
} | null;
announcementAll: Array<{ content: string }>;
};
+1 -8
View File
@@ -7,7 +7,7 @@ import pLimit from "p-limit";
import type { InputMaybe, ResolverFn } from "@/generated/graphql-resolvers";
import type { ApiIndex } from "@/lib/common/animethemes/types";
import { AUTH_REFERER, CLIENT_API_URL, PAGINATION_PAGE_SIZE, SERVER_API_KEY, SERVER_API_URL } from "@/utils/config";
import { AUTH_REFERER, CLIENT_API_URL, PAGINATION_PAGE_SIZE, SERVER_API_URL } from "@/utils/config";
import devLog from "@/utils/devLog";
const limit = pLimit(5);
@@ -468,13 +468,6 @@ export async function fetchJson<T = Record<string, unknown>>(
config.credentials = "include";
if (SERVER_API_KEY && !("cookie" in (config.headers ?? {}))) {
config.headers = {
Authorization: `Bearer ${SERVER_API_KEY}`,
...config.headers,
};
}
const response = await fetch(url, config);
if (!response.ok) {
+9 -7
View File
@@ -67,7 +67,7 @@ const Grid = styled.div<{ $columns: number }>`
`;
interface HomePageProps {
featuredTheme: NonNullable<NonNullable<HomePageQuery["featuredTheme"]>["entry"]>["theme"] | null;
featuredTheme: NonNullable<NonNullable<HomePageQuery["featuredTheme"]>> | null;
announcementSources: MDXRemoteSerializeResult[];
}
@@ -82,10 +82,10 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
{announcementSources.length > 0 ? <AnnouncementCard announcementSource={announcementSources[0]} /> : null}
{featuredTheme ? (
{featuredTheme && featuredTheme.entry && featuredTheme.video ? (
<>
<Text variant="h2">Featured Theme</Text>
<FeaturedTheme theme={featuredTheme} />
<FeaturedTheme entry={featuredTheme.entry} video={featuredTheme.video} />
</>
) : null}
@@ -224,14 +224,16 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
const { data, apiRequests } = await fetchData<HomePageQuery>(gql`
${FeaturedTheme.fragments.theme}
${FeaturedTheme.fragments.entry}
${FeaturedTheme.fragments.video}
query HomePage {
featuredTheme {
entry {
theme {
...FeaturedThemeTheme
...FeaturedThemeEntry
}
video {
...FeaturedThemeVideo
}
}
announcementAll {
@@ -243,7 +245,7 @@ export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
return {
props: {
...getSharedPageProps(apiRequests),
featuredTheme: data?.featuredTheme?.entry?.theme ?? null,
featuredTheme: data?.featuredTheme,
announcementSources: await Promise.all(
data?.announcementAll.map(
async (announcement) => (await serializeMarkdownSafe(announcement.content)).source,
+6 -9
View File
@@ -369,15 +369,8 @@ export default function PlaylistDetailPage({ playlist: initialPlaylist, me: init
) : null}
{isRanking && topRankedTrack ? (
<FeaturedTheme
theme={{
...tracks[0].entry.theme,
entries: [
{
...tracks[0].entry,
videos: [tracks[0].video],
},
],
}}
entry={tracks[0].entry}
video={tracks[0].video}
hasGrill={false}
card={
<PlaylistTrack
@@ -713,6 +706,8 @@ PlaylistDetailPage.fragments = {
${VideoSummaryCardFragmentEntry}
${PlaylistEditDialog.fragments.playlist}
${PlaylistTrackRemoveDialog.fragments.playlist}
${FeaturedTheme.fragments.entry}
${FeaturedTheme.fragments.video}
fragment PlaylistDetailPagePlaylist on Playlist {
...PlaylistEditDialogPlaylist
@@ -726,10 +721,12 @@ PlaylistDetailPage.fragments = {
id
video {
...VideoSummaryCardVideo
...FeaturedThemeVideo
id
}
entry {
...VideoSummaryCardEntry
...FeaturedThemeEntry
theme {
anime {
year
-2
View File
@@ -5,7 +5,6 @@ import { error, warn } from "@/utils/log";
// Server-side
const SERVER_API_URL = process.env.ANIMETHEMES_API_URL;
const SERVER_API_KEY = process.env.ANIMETHEMES_API_KEY;
const ANALYZE = !!process.env.ANALYZE;
const MINIMAL_BUILD = !!process.env.MINIMAL_BUILD;
@@ -62,7 +61,6 @@ export {
CLIENT_API_URL,
MINIMAL_BUILD,
PAGINATION_PAGE_SIZE,
SERVER_API_KEY,
SERVER_API_URL,
STAGING,
validateConfig,