From 49f64509ba3d91737a5373549587069035f79e9b Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sat, 12 Apr 2025 03:08:40 -0300 Subject: [PATCH] feat: Added new artist features (#235) * Added artist information, notes & multiple covers * Added Markdown for artist information, improved design of artist notes and fixed types. * Added Markdown for anime synopsis and better error handling. --- src/components/image/MultiCoverImage.tsx | 56 +++++++++++++- src/generated/graphql-resolvers.ts | 6 ++ src/generated/graphql.ts | 73 ++++++++++++++++-- src/lib/common/animethemes/resolvers.ts | 17 +++-- src/lib/common/animethemes/type-defs.ts | 3 + src/lib/common/animethemes/types.ts | 11 ++- src/pages/[...pageSlug]/index.tsx | 4 +- src/pages/anime/[animeSlug]/index.tsx | 24 +++--- src/pages/artist/[artistSlug]/index.tsx | 95 +++++++++++++++++------- src/pages/index.tsx | 4 +- src/utils/extractMultipleImages.ts | 52 +++++++++++++ src/utils/serializeMarkdown.ts | 13 ++++ 12 files changed, 304 insertions(+), 54 deletions(-) create mode 100644 src/utils/extractMultipleImages.ts diff --git a/src/components/image/MultiCoverImage.tsx b/src/components/image/MultiCoverImage.tsx index 68b855b..e742e47 100644 --- a/src/components/image/MultiCoverImage.tsx +++ b/src/components/image/MultiCoverImage.tsx @@ -5,8 +5,12 @@ import gql from "graphql-tag"; import { LogoPlaceholder } from "@/components/image/LogoPlaceholder"; import { AspectRatio } from "@/components/utils/AspectRatio"; -import type { MultiCoverImageResourceWithImagesFragment } from "@/generated/graphql"; +import type { + MultiCoverImageResourceWithImagesFragment, + MultiLargeCoverImageResourceWithImagesFragment, +} from "@/generated/graphql"; import extractImages from "@/utils/extractImages"; +import extractMultipleImages from "@/utils/extractMultipleImages"; function getTranslationX(item: number, itemCount: number) { switch (itemCount) { @@ -174,3 +178,53 @@ MultiCoverImage.fragments = { } `, }; + +interface MultiLargeCoverImageProps extends ComponentPropsWithoutRef { + resourceWithImages: MultiLargeCoverImageResourceWithImagesFragment; +} + +export function MultiLargeCoverImage({ resourceWithImages, ...props }: MultiLargeCoverImageProps) { + const images = extractMultipleImages(resourceWithImages).slice(0, 4); + + return ( + + + {images.length ? ( + images.map(({ link }) => ( + + + + )) + ) : ( + + )} + + + ); +} + +MultiLargeCoverImage.fragments = { + resourceWithImages: gql` + ${extractMultipleImages.fragments.resourceWithImages} + + fragment MultiLargeCoverImageResourceWithImages on ResourceWithImages { + ... on Anime { + name + } + ... on Artist { + name + } + ... on Studio { + name + } + ...extractMultipleImagesResourceWithImages + } + `, +}; diff --git a/src/generated/graphql-resolvers.ts b/src/generated/graphql-resolvers.ts index 638c0f8..e7b23e8 100644 --- a/src/generated/graphql-resolvers.ts +++ b/src/generated/graphql-resolvers.ts @@ -83,6 +83,7 @@ export type Artist = ResourceWithImages & { groups: Array; id: Scalars["Int"]["output"]; images: Array; + information?: Maybe; members: Array; name: Scalars["String"]["output"]; performances: Array; @@ -96,6 +97,7 @@ export type ArtistMembership = { as?: Maybe; group: Artist; member: Artist; + notes?: Maybe; }; export type ArtistSearchResult = EntitySearchResult & { @@ -204,6 +206,7 @@ export type GlobalSearchResult = { export type Image = { __typename?: "Image"; + depth?: Maybe; facet?: Maybe; id: Scalars["Int"]["output"]; link: Scalars["String"]["output"]; @@ -883,6 +886,7 @@ export type ArtistResolvers< groups?: Resolver, ParentType, ContextType>; id?: Resolver; images?: Resolver, ParentType, ContextType>; + information?: Resolver, ParentType, ContextType>; members?: Resolver, ParentType, ContextType>; name?: Resolver; performances?: Resolver, ParentType, ContextType>; @@ -899,6 +903,7 @@ export type ArtistMembershipResolvers< as?: Resolver, ParentType, ContextType>; group?: Resolver; member?: Resolver; + notes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -1048,6 +1053,7 @@ export type ImageResolvers< ContextType = any, ParentType extends ResolversParentTypes["Image"] = ResolversParentTypes["Image"], > = { + depth?: Resolver, ParentType, ContextType>; facet?: Resolver, ParentType, ContextType>; id?: Resolver; link?: Resolver; diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index ebc8443..7f67fbe 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -44,6 +44,7 @@ export type Artist = ResourceWithImages & { groups: Array; id: Scalars["Int"]["output"]; images: Array; + information: Maybe; members: Array; name: Scalars["String"]["output"]; performances: Array; @@ -56,6 +57,7 @@ export type ArtistMembership = { as: Maybe; group: Artist; member: Artist; + notes: Maybe; }; export type ArtistSearchResult = EntitySearchResult & { @@ -153,6 +155,7 @@ export type GlobalSearchResult = { }; export type Image = { + depth: Maybe; facet: Maybe; id: Scalars["Int"]["output"]; link: Scalars["String"]["output"]; @@ -963,6 +966,26 @@ export type MultiCoverImageResourceWithImagesFragment = | MultiCoverImageResourceWithImages_Artist_Fragment | MultiCoverImageResourceWithImages_Studio_Fragment; +type MultiLargeCoverImageResourceWithImages_Anime_Fragment = { + name: string; + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +type MultiLargeCoverImageResourceWithImages_Artist_Fragment = { + name: string; + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +type MultiLargeCoverImageResourceWithImages_Studio_Fragment = { + name: string; + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +export type MultiLargeCoverImageResourceWithImagesFragment = + | MultiLargeCoverImageResourceWithImages_Anime_Fragment + | MultiLargeCoverImageResourceWithImages_Artist_Fragment + | MultiLargeCoverImageResourceWithImages_Studio_Fragment; + export type ProfileImageUserFragment = { name: string; email: string }; export type StudioCoverImageStudioFragment = { @@ -1807,6 +1830,7 @@ export type RevalidateAnimeQuery = { export type ArtistDetailPageArtistFragment = { slug: string; + information: string | null; name: string; performances: Array<{ alias: string | null; @@ -1858,10 +1882,16 @@ export type ArtistDetailPageArtistFragment = { }>; }; }>; - members: Array<{ alias: string | null; as: string | null; member: { slug: string; name: string } }>; + members: Array<{ + alias: string | null; + as: string | null; + notes: string | null; + member: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> }; + }>; groups: Array<{ alias: string | null; as: string | null; + notes: string | null; group: { slug: string; name: string; @@ -1922,7 +1952,7 @@ export type ArtistDetailPageArtistFragment = { }; }>; resources: Array<{ link: string | null; site: string | null; as: string | null }>; - images: Array<{ facet: string | null; link: string }>; + images: Array<{ link: string; depth: number | null; facet: string | null }>; }; export type ArtistDetailPageQueryVariables = Exact<{ @@ -1932,6 +1962,7 @@ export type ArtistDetailPageQueryVariables = Exact<{ export type ArtistDetailPageQuery = { artist: { slug: string; + information: string | null; name: string; performances: Array<{ alias: string | null; @@ -1987,10 +2018,16 @@ export type ArtistDetailPageQuery = { }>; }; }>; - members: Array<{ alias: string | null; as: string | null; member: { slug: string; name: string } }>; + members: Array<{ + alias: string | null; + as: string | null; + notes: string | null; + member: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> }; + }>; groups: Array<{ alias: string | null; as: string | null; + notes: string | null; group: { slug: string; name: string; @@ -2051,7 +2088,7 @@ export type ArtistDetailPageQuery = { }; }>; resources: Array<{ link: string | null; site: string | null; as: string | null }>; - images: Array<{ facet: string | null; link: string }>; + images: Array<{ link: string; depth: number | null; facet: string | null }>; } | null; }; @@ -2060,6 +2097,7 @@ export type ArtistDetailPageAllQueryVariables = Exact<{ [key: string]: never }>; export type ArtistDetailPageAllQuery = { artistAll: Array<{ slug: string; + information: string | null; name: string; performances: Array<{ alias: string | null; @@ -2115,10 +2153,16 @@ export type ArtistDetailPageAllQuery = { }>; }; }>; - members: Array<{ alias: string | null; as: string | null; member: { slug: string; name: string } }>; + members: Array<{ + alias: string | null; + as: string | null; + notes: string | null; + member: { slug: string; name: string; images: Array<{ link: string; facet: string | null }> }; + }>; groups: Array<{ alias: string | null; as: string | null; + notes: string | null; group: { slug: string; name: string; @@ -2179,7 +2223,7 @@ export type ArtistDetailPageAllQuery = { }; }>; resources: Array<{ link: string | null; site: string | null; as: string | null }>; - images: Array<{ facet: string | null; link: string }>; + images: Array<{ link: string; depth: number | null; facet: string | null }>; }>; }; @@ -3045,3 +3089,20 @@ export type ExtractImagesResourceWithImagesFragment = | ExtractImagesResourceWithImages_Anime_Fragment | ExtractImagesResourceWithImages_Artist_Fragment | ExtractImagesResourceWithImages_Studio_Fragment; + +type ExtractMultipleImagesResourceWithImages_Anime_Fragment = { + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +type ExtractMultipleImagesResourceWithImages_Artist_Fragment = { + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +type ExtractMultipleImagesResourceWithImages_Studio_Fragment = { + images: Array<{ link: string; depth: number | null; facet: string | null }>; +}; + +export type ExtractMultipleImagesResourceWithImagesFragment = + | ExtractMultipleImagesResourceWithImages_Anime_Fragment + | ExtractMultipleImagesResourceWithImages_Artist_Fragment + | ExtractMultipleImagesResourceWithImages_Studio_Fragment; diff --git a/src/lib/common/animethemes/resolvers.ts b/src/lib/common/animethemes/resolvers.ts index 8ffe211..6659708 100644 --- a/src/lib/common/animethemes/resolvers.ts +++ b/src/lib/common/animethemes/resolvers.ts @@ -251,13 +251,16 @@ const resolvers: Resolvers = { }), (resources) => resources.map(({ artistresource, ...resource }) => ({ ...artistresource, ...resource })), ), - images: createApiResolverNotNull>()({ - extractFromParent: (artist) => artist.images, - endpoint: (artist) => `/artist/${artist.slug}`, - extractFromResponse: (response) => response.artist.images, - type: "Artist", - baseInclude: INCLUDES.Artist.images, - }), + images: transformedResolver( + createApiResolverNotNull>()({ + extractFromParent: (artist) => artist.images, + endpoint: (artist) => `/artist/${artist.slug}`, + extractFromResponse: (response) => response.artist.images, + type: "Artist", + baseInclude: INCLUDES.Artist.images, + }), + (images) => images.map(({ artistimage, ...image }) => ({ ...artistimage, ...image })), + ), groups: transformedResolver( createApiResolverNotNull>()({ extractFromParent: (artist) => artist.groups, diff --git a/src/lib/common/animethemes/type-defs.ts b/src/lib/common/animethemes/type-defs.ts index e3144bc..89d522d 100644 --- a/src/lib/common/animethemes/type-defs.ts +++ b/src/lib/common/animethemes/type-defs.ts @@ -165,6 +165,7 @@ const typeDefs = gql` type Artist implements ResourceWithImages { id: Int! + information: String slug: String! name: String! performances: [Performance!]! @@ -179,6 +180,7 @@ const typeDefs = gql` member: Artist! alias: String as: String + notes: String } type Series { @@ -201,6 +203,7 @@ const typeDefs = gql` path: String! size: Int! mimetype: String! + depth: Int facet: String link: String! } diff --git a/src/lib/common/animethemes/types.ts b/src/lib/common/animethemes/types.ts index 22b2ee3..3065d50 100644 --- a/src/lib/common/animethemes/types.ts +++ b/src/lib/common/animethemes/types.ts @@ -54,6 +54,7 @@ export interface ApiAnnouncement { export interface ApiArtist { id: number; + information: string | null; name: string; slug: string; songs?: Array< @@ -66,7 +67,11 @@ export interface ApiArtist { artistresource: ApiArtistResource; } >; - images?: Array; + images?: Array< + ApiImage & { + artistimage: ApiArtistImage; + } + >; groups?: Array< ApiArtist & { artistmember: ApiArtistMember; @@ -221,6 +226,10 @@ export interface ApiAnimeResource { as: string | null; } +export interface ApiArtistImage { + depth: number | null; +} + export interface ApiArtistMember { alias: string | null; as: string | null; diff --git a/src/pages/[...pageSlug]/index.tsx b/src/pages/[...pageSlug]/index.tsx index 3c5ebd9..35e362e 100644 --- a/src/pages/[...pageSlug]/index.tsx +++ b/src/pages/[...pageSlug]/index.tsx @@ -19,7 +19,7 @@ import fetchStaticPaths from "@/utils/fetchStaticPaths"; import type { SharedPageProps } from "@/utils/getSharedPageProps"; import getSharedPageProps from "@/utils/getSharedPageProps"; import type { Heading } from "@/utils/rehypeExtractHeadings"; -import serializeMarkdown from "@/utils/serializeMarkdown"; +import { serializeMarkdownSafe } from "@/utils/serializeMarkdown"; import type { RequiredNonNullable } from "@/utils/types"; const StyledGrid = styled.div` @@ -128,7 +128,7 @@ export const getStaticProps: GetStaticProps {} +interface AnimeDetailPageProps extends SharedPageProps, RequiredNonNullable { + synopsisMarkdownSource: MDXRemoteSerializeResult | null; +} interface AnimeDetailPageParams extends ParsedUrlQuery { animeSlug: string; } -export default function AnimeDetailPage({ anime }: AnimeDetailPageProps) { +export default function AnimeDetailPage({ anime, synopsisMarkdownSource }: AnimeDetailPageProps) { const [collapseSynopsis, setCollapseSynopsis] = useState(true); const { largeCover } = extractImages(anime); @@ -118,16 +123,14 @@ export default function AnimeDetailPage({ anime }: AnimeDetailPageProps) { - {!!anime.synopsis && ( + {!!synopsisMarkdownSource && ( <> Synopsis setCollapseSynopsis(!collapseSynopsis)}> - + + + @@ -214,6 +217,9 @@ export const getStaticProps: GetStaticProps } } -type ArtistDetailPageProps = RequiredNonNullable; +type ArtistDetailPageProps = RequiredNonNullable & { + informationMarkdownSource: MDXRemoteSerializeResult | null; +}; interface ArtistDetailPageParams extends ParsedUrlQuery { artistSlug: string; } -export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) { - const { largeCover } = extractImages(artist); +export default function ArtistDetailPage({ artist, informationMarkdownSource }: ArtistDetailPageProps) { + const images = extractMultipleImages(artist); + const [collapseInformation, setCollapseInformation] = useState(true); const characters = useMemo( () => @@ -183,11 +190,11 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) { return ( <> - + {artist.name} - + {aliases.length > 0 && ( @@ -203,12 +210,19 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) { {!!artist.members?.length && ( - {artist.members.map(({ member, alias, as }) => ( - - {member.name} - {alias ? ` (as ${alias})` : null} - {as ? ` (as ${as})` : null} - + {artist.members.map(({ member, alias, as, notes }) => ( + + + {alias ? alias : member.name} + + {as || notes ? ( + + {[as ? `As ${as}` : null, notes || null] + .filter(Boolean) + .join(" • ")} + + ) : null} + ))} @@ -216,12 +230,23 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) { {!!artist.groups?.length && ( - {artist.groups.map(({ group, alias, as }) => ( - - {group.name} - {alias ? ` (as ${alias})` : null} - {as ? ` (as ${as})` : null} - + {artist.groups.map(({ group, alias, as, notes }) => ( + + + {group.name} + + {alias || as || notes ? ( + + {[ + alias ? `As ${alias}` : null, + as ? `As ${as}` : null, + notes || null, + ] + .filter(Boolean) + .join(" • ")} + + ) : null} + ))} @@ -243,6 +268,18 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) { + {!!informationMarkdownSource && ( + <> + Information + setCollapseInformation(!collapseInformation)}> + + + + + + + + )} Song Performances @@ -386,10 +423,14 @@ ArtistDetailPage.fragements = { ${ThemeSummaryCard.fragments.theme} ${ThemeSummaryCard.fragments.artist} ${ThemeSummaryCard.fragments.expandable} + ${ArtistSummaryCard.fragments.artist} + ${MultiLargeCoverImage.fragments.resourceWithImages} fragment ArtistDetailPageArtist on Artist { ...ThemeSummaryCardArtist + ...MultiLargeCoverImageResourceWithImages slug + information name performances { alias @@ -423,11 +464,13 @@ ArtistDetailPage.fragements = { } members { member { + ...ArtistSummaryCardArtist slug name } alias as + notes } groups { group { @@ -466,16 +509,13 @@ ArtistDetailPage.fragements = { } alias as + notes } resources { link site as } - images { - facet - link - } } `, }; @@ -511,6 +551,9 @@ export const getStaticProps: GetStaticProps = async () => { featuredTheme: data?.featuredTheme?.entry?.theme ?? null, announcementSources: await Promise.all( data?.announcementAll.map( - async (announcement) => (await serializeMarkdown(announcement.content)).source, + async (announcement) => (await serializeMarkdownSafe(announcement.content)).source, ), ), }, diff --git a/src/utils/extractMultipleImages.ts b/src/utils/extractMultipleImages.ts new file mode 100644 index 0000000..dc2ed1b --- /dev/null +++ b/src/utils/extractMultipleImages.ts @@ -0,0 +1,52 @@ +import type { ASTNode } from "graphql/index"; +import gql from "graphql-tag"; + +import type { ExtractMultipleImagesResourceWithImagesFragment } from "@/generated/graphql"; + +interface ExtractMultiplesImagesResult { + link: string; + depth: number | null; + facet: string | null; +} + +interface ExtractMultipleImages { + (resourceWithImages?: ExtractMultipleImagesResourceWithImagesFragment | null): Array; + fragments: { + resourceWithImages: ASTNode; + }; +} + +const extractMultipleImages: ExtractMultipleImages = (resourceWithImages) => { + if (resourceWithImages) { + const largeCovers = resourceWithImages.images.filter((image) => image.facet === "Large Cover"); + + return largeCovers.sort((a, b) => { + if (a.depth && b.depth) { + return a.depth - b.depth; + } + if (a.depth) { + return -1; + } + if (b.depth) { + return 1; + } + return 0; + }); + } + + return []; +}; + +export default extractMultipleImages; + +extractMultipleImages.fragments = { + resourceWithImages: gql` + fragment extractMultipleImagesResourceWithImages on ResourceWithImages { + images { + link + depth + facet + } + } + `, +}; diff --git a/src/utils/serializeMarkdown.ts b/src/utils/serializeMarkdown.ts index 07cbcdd..90e63f3 100644 --- a/src/utils/serializeMarkdown.ts +++ b/src/utils/serializeMarkdown.ts @@ -1,5 +1,6 @@ import { serialize } from "next-mdx-remote/serialize"; +import { isObject } from "lodash-es"; import rehypePrettyCode from "rehype-pretty-code"; import rehypeSlug from "rehype-slug"; import remarkGfm from "remark-gfm"; @@ -28,3 +29,15 @@ export default async function serializeMarkdown(markdown: Compatible) { headings, }; } + +export async function serializeMarkdownSafe(markdown: Compatible) { + try { + return await serializeMarkdown(markdown); + } catch (error) { + if (!isObject(error) || !("message" in error) || typeof error.message !== "string") { + throw error; + } + + return await serializeMarkdown(`*Failed to parse Markdown!*\n\n\`\`\`\n${error.message}\n\`\`\``); + } +}