mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
feat: Enhance support for featured theme (#240)
This commit is contained in:
@@ -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
|
||||
|
||||
Generated
+14138
-14020
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
anime {
|
||||
...extractImagesResourceWithImages
|
||||
}
|
||||
entries {
|
||||
videos {
|
||||
basename
|
||||
fragment FeaturedThemeEntry on Entry {
|
||||
...VideoSummaryCardEntry
|
||||
theme {
|
||||
anime {
|
||||
...extractImagesResourceWithImages
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
video: gql`
|
||||
${VideoSummaryCardFragmentVideo}
|
||||
|
||||
fragment FeaturedThemeVideo on Video {
|
||||
...VideoSummaryCardVideo
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
+19
-20
@@ -813,23 +813,24 @@ export type PlaylistTrackRemoveDialogEntryFragment = {
|
||||
};
|
||||
};
|
||||
|
||||
export type FeaturedThemeThemeFragment = {
|
||||
type: string;
|
||||
sequence: number | null;
|
||||
export type FeaturedThemeEntryFragment = {
|
||||
id: number;
|
||||
anime: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> };
|
||||
entries: Array<{
|
||||
version: number | null;
|
||||
theme: {
|
||||
id: number;
|
||||
version: number | null;
|
||||
videos: Array<{ id: number; basename: string; tags: string; audio: { basename: string } }>;
|
||||
}>;
|
||||
group: { name: string; slug: string } | null;
|
||||
song: {
|
||||
title: string | null;
|
||||
performances: Array<{ alias: string | null; as: string | null; artist: { slug: string; name: string } }>;
|
||||
} | null;
|
||||
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;
|
||||
sequence: number | null;
|
||||
@@ -2495,17 +2496,14 @@ export type HomePageQueryVariables = Exact<{ [key: string]: never }>;
|
||||
export type HomePageQuery = {
|
||||
featuredTheme: {
|
||||
entry: {
|
||||
id: number;
|
||||
version: number | null;
|
||||
theme: {
|
||||
id: number;
|
||||
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 } }>;
|
||||
}>;
|
||||
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 }>;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+10
-8
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user