mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
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.
This commit is contained in:
@@ -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<typeof StyledCover> {
|
||||
resourceWithImages: MultiLargeCoverImageResourceWithImagesFragment;
|
||||
}
|
||||
|
||||
export function MultiLargeCoverImage({ resourceWithImages, ...props }: MultiLargeCoverImageProps) {
|
||||
const images = extractMultipleImages(resourceWithImages).slice(0, 4);
|
||||
|
||||
return (
|
||||
<AspectRatio $ratio={2 / 3}>
|
||||
<StyledCoverContainer>
|
||||
{images.length ? (
|
||||
images.map(({ link }) => (
|
||||
<StyledCoverItemContainer key={link} $itemCount={images.length}>
|
||||
<StyledCover
|
||||
loading="lazy"
|
||||
src={link}
|
||||
alt={`Cover image of ${resourceWithImages.name}`}
|
||||
title={resourceWithImages.name}
|
||||
style={{ backgroundImage: `url(${link})` }}
|
||||
{...props}
|
||||
/>
|
||||
</StyledCoverItemContainer>
|
||||
))
|
||||
) : (
|
||||
<LogoPlaceholder {...props} />
|
||||
)}
|
||||
</StyledCoverContainer>
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
||||
|
||||
MultiLargeCoverImage.fragments = {
|
||||
resourceWithImages: gql`
|
||||
${extractMultipleImages.fragments.resourceWithImages}
|
||||
|
||||
fragment MultiLargeCoverImageResourceWithImages on ResourceWithImages {
|
||||
... on Anime {
|
||||
name
|
||||
}
|
||||
... on Artist {
|
||||
name
|
||||
}
|
||||
... on Studio {
|
||||
name
|
||||
}
|
||||
...extractMultipleImagesResourceWithImages
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -83,6 +83,7 @@ export type Artist = ResourceWithImages & {
|
||||
groups: Array<ArtistMembership>;
|
||||
id: Scalars["Int"]["output"];
|
||||
images: Array<Image>;
|
||||
information?: Maybe<Scalars["String"]["output"]>;
|
||||
members: Array<ArtistMembership>;
|
||||
name: Scalars["String"]["output"];
|
||||
performances: Array<Performance>;
|
||||
@@ -96,6 +97,7 @@ export type ArtistMembership = {
|
||||
as?: Maybe<Scalars["String"]["output"]>;
|
||||
group: Artist;
|
||||
member: Artist;
|
||||
notes?: Maybe<Scalars["String"]["output"]>;
|
||||
};
|
||||
|
||||
export type ArtistSearchResult = EntitySearchResult & {
|
||||
@@ -204,6 +206,7 @@ export type GlobalSearchResult = {
|
||||
|
||||
export type Image = {
|
||||
__typename?: "Image";
|
||||
depth?: Maybe<Scalars["Int"]["output"]>;
|
||||
facet?: Maybe<Scalars["String"]["output"]>;
|
||||
id: Scalars["Int"]["output"];
|
||||
link: Scalars["String"]["output"];
|
||||
@@ -883,6 +886,7 @@ export type ArtistResolvers<
|
||||
groups?: Resolver<Array<ResolversTypes["ArtistMembership"]>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes["Int"], ParentType, ContextType>;
|
||||
images?: Resolver<Array<ResolversTypes["Image"]>, ParentType, ContextType>;
|
||||
information?: Resolver<Maybe<ResolversTypes["String"]>, ParentType, ContextType>;
|
||||
members?: Resolver<Array<ResolversTypes["ArtistMembership"]>, ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||
performances?: Resolver<Array<ResolversTypes["Performance"]>, ParentType, ContextType>;
|
||||
@@ -899,6 +903,7 @@ export type ArtistMembershipResolvers<
|
||||
as?: Resolver<Maybe<ResolversTypes["String"]>, ParentType, ContextType>;
|
||||
group?: Resolver<ResolversTypes["Artist"], ParentType, ContextType>;
|
||||
member?: Resolver<ResolversTypes["Artist"], ParentType, ContextType>;
|
||||
notes?: Resolver<Maybe<ResolversTypes["String"]>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
@@ -1048,6 +1053,7 @@ export type ImageResolvers<
|
||||
ContextType = any,
|
||||
ParentType extends ResolversParentTypes["Image"] = ResolversParentTypes["Image"],
|
||||
> = {
|
||||
depth?: Resolver<Maybe<ResolversTypes["Int"]>, ParentType, ContextType>;
|
||||
facet?: Resolver<Maybe<ResolversTypes["String"]>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes["Int"], ParentType, ContextType>;
|
||||
link?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||
|
||||
@@ -44,6 +44,7 @@ export type Artist = ResourceWithImages & {
|
||||
groups: Array<ArtistMembership>;
|
||||
id: Scalars["Int"]["output"];
|
||||
images: Array<Image>;
|
||||
information: Maybe<Scalars["String"]["output"]>;
|
||||
members: Array<ArtistMembership>;
|
||||
name: Scalars["String"]["output"];
|
||||
performances: Array<Performance>;
|
||||
@@ -56,6 +57,7 @@ export type ArtistMembership = {
|
||||
as: Maybe<Scalars["String"]["output"]>;
|
||||
group: Artist;
|
||||
member: Artist;
|
||||
notes: Maybe<Scalars["String"]["output"]>;
|
||||
};
|
||||
|
||||
export type ArtistSearchResult = EntitySearchResult & {
|
||||
@@ -153,6 +155,7 @@ export type GlobalSearchResult = {
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
depth: Maybe<Scalars["Int"]["output"]>;
|
||||
facet: Maybe<Scalars["String"]["output"]>;
|
||||
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;
|
||||
|
||||
@@ -251,13 +251,16 @@ const resolvers: Resolvers = {
|
||||
}),
|
||||
(resources) => resources.map(({ artistresource, ...resource }) => ({ ...artistresource, ...resource })),
|
||||
),
|
||||
images: createApiResolverNotNull<ApiArtistShow<"images">>()({
|
||||
extractFromParent: (artist) => artist.images,
|
||||
endpoint: (artist) => `/artist/${artist.slug}`,
|
||||
extractFromResponse: (response) => response.artist.images,
|
||||
type: "Artist",
|
||||
baseInclude: INCLUDES.Artist.images,
|
||||
}),
|
||||
images: transformedResolver(
|
||||
createApiResolverNotNull<ApiArtistShow<"images">>()({
|
||||
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<ApiArtistShow<"groups">>()({
|
||||
extractFromParent: (artist) => artist.groups,
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
|
||||
@@ -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<ApiImage>;
|
||||
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;
|
||||
|
||||
@@ -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<DocumentPageProps, DocumentPageParam
|
||||
...getSharedPageProps(apiRequests),
|
||||
page: {
|
||||
...data.page,
|
||||
...(await serializeMarkdown(data.page.body)),
|
||||
...(await serializeMarkdownSafe(data.page.body)),
|
||||
},
|
||||
},
|
||||
// Revalidate after 1 hour (= 3600 seconds).
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import Link from "next/link";
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
|
||||
import gql from "graphql-tag";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
@@ -14,6 +15,7 @@ import { DescriptionList } from "@/components/description-list/DescriptionList";
|
||||
import { ExternalLink } from "@/components/external-link/ExternalLink";
|
||||
import { AnimeThemeFilter } from "@/components/filter/AnimeThemeFilter";
|
||||
import { CoverImage } from "@/components/image/CoverImage";
|
||||
import { Markdown } from "@/components/markdown/Markdown";
|
||||
import { SEO } from "@/components/seo/SEO";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import { HeightTransition } from "@/components/utils/HeightTransition";
|
||||
@@ -30,6 +32,7 @@ import extractImages from "@/utils/extractImages";
|
||||
import fetchStaticPaths from "@/utils/fetchStaticPaths";
|
||||
import type { SharedPageProps } from "@/utils/getSharedPageProps";
|
||||
import getSharedPageProps from "@/utils/getSharedPageProps";
|
||||
import { serializeMarkdownSafe } from "@/utils/serializeMarkdown";
|
||||
import type { RequiredNonNullable } from "@/utils/types";
|
||||
|
||||
const StyledList = styled.div`
|
||||
@@ -40,13 +43,15 @@ const StyledList = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
interface AnimeDetailPageProps extends SharedPageProps, RequiredNonNullable<AnimeDetailPageQuery> {}
|
||||
interface AnimeDetailPageProps extends SharedPageProps, RequiredNonNullable<AnimeDetailPageQuery> {
|
||||
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) {
|
||||
</DescriptionList>
|
||||
</Column>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
{!!anime.synopsis && (
|
||||
{!!synopsisMarkdownSource && (
|
||||
<>
|
||||
<Text variant="h2">Synopsis</Text>
|
||||
<Card $hoverable onClick={() => setCollapseSynopsis(!collapseSynopsis)}>
|
||||
<HeightTransition>
|
||||
<Text
|
||||
as="p"
|
||||
maxLines={collapseSynopsis ? 2 : undefined}
|
||||
dangerouslySetInnerHTML={{ __html: anime.synopsis }}
|
||||
/>
|
||||
<Text as="div" maxLines={collapseSynopsis ? 2 : undefined}>
|
||||
<Markdown source={synopsisMarkdownSource} />
|
||||
</Text>
|
||||
</HeightTransition>
|
||||
</Card>
|
||||
</>
|
||||
@@ -214,6 +217,9 @@ export const getStaticProps: GetStaticProps<AnimeDetailPageProps, AnimeDetailPag
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
anime: data.anime,
|
||||
synopsisMarkdownSource: data.anime.synopsis
|
||||
? (await serializeMarkdownSafe(data.anime.synopsis)).source
|
||||
: null,
|
||||
},
|
||||
// Revalidate after 1 hour (= 3600 seconds).
|
||||
revalidate: 3600,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { useContext } from "react";
|
||||
import React, { memo, useCallback, useContext, useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import Link from "next/link";
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
|
||||
import gql from "graphql-tag";
|
||||
import { uniq } from "lodash-es";
|
||||
@@ -11,18 +10,22 @@ import type { ParsedUrlQuery } from "querystring";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import { FilterToggleButton } from "@/components/button/FilterToggleButton";
|
||||
import { ArtistSummaryCard } from "@/components/card/ArtistSummaryCard";
|
||||
import { Card } from "@/components/card/Card";
|
||||
import { ThemeSummaryCard } from "@/components/card/ThemeSummaryCard";
|
||||
import { SidebarContainer } from "@/components/container/SidebarContainer";
|
||||
import { DescriptionList } from "@/components/description-list/DescriptionList";
|
||||
import { ExternalLink } from "@/components/external-link/ExternalLink";
|
||||
import { CoverImage } from "@/components/image/CoverImage";
|
||||
import { MultiLargeCoverImage } from "@/components/image/MultiCoverImage";
|
||||
import { Listbox, ListboxOption } from "@/components/listbox/Listbox";
|
||||
import { Markdown } from "@/components/markdown/Markdown";
|
||||
import { SearchFilter } from "@/components/search-filter/SearchFilter";
|
||||
import { SearchFilterGroup } from "@/components/search-filter/SearchFilterGroup";
|
||||
import { SearchFilterSortBy } from "@/components/search-filter/SearchFilterSortBy";
|
||||
import { SEO } from "@/components/seo/SEO";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import { Collapse } from "@/components/utils/Collapse";
|
||||
import { HeightTransition } from "@/components/utils/HeightTransition";
|
||||
import PlayerContext, { createWatchListItem } from "@/context/playerContext";
|
||||
import type {
|
||||
ArtistDetailPageAllQuery,
|
||||
@@ -44,9 +47,10 @@ import {
|
||||
SONG_Z_A,
|
||||
SONG_Z_A_ANIME,
|
||||
} from "@/utils/comparators";
|
||||
import extractImages from "@/utils/extractImages";
|
||||
import extractMultipleImages from "@/utils/extractMultipleImages";
|
||||
import fetchStaticPaths from "@/utils/fetchStaticPaths";
|
||||
import getSharedPageProps from "@/utils/getSharedPageProps";
|
||||
import { serializeMarkdownSafe } from "@/utils/serializeMarkdown";
|
||||
import type { RequiredNonNullable } from "@/utils/types";
|
||||
|
||||
const StyledList = styled.div`
|
||||
@@ -76,14 +80,17 @@ function getPerformanceFilter(key: string | null): (performance: Performance) =>
|
||||
}
|
||||
}
|
||||
|
||||
type ArtistDetailPageProps = RequiredNonNullable<ArtistDetailPageQuery>;
|
||||
type ArtistDetailPageProps = RequiredNonNullable<ArtistDetailPageQuery> & {
|
||||
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 (
|
||||
<>
|
||||
<SEO title={artist.name} image={largeCover} />
|
||||
<SEO title={artist.name} image={images[0]?.link} />
|
||||
<Text variant="h1">{artist.name}</Text>
|
||||
<SidebarContainer>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
<CoverImage resourceWithImages={artist} alt={`Picture of ${artist.name}`} />
|
||||
<MultiLargeCoverImage resourceWithImages={artist} />
|
||||
<DescriptionList>
|
||||
{aliases.length > 0 && (
|
||||
<DescriptionList.Item title="Aliases">
|
||||
@@ -203,12 +210,19 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) {
|
||||
{!!artist.members?.length && (
|
||||
<DescriptionList.Item title="Members">
|
||||
<StyledList>
|
||||
{artist.members.map(({ member, alias, as }) => (
|
||||
<Text key={member.slug} as={Link} href={`/artist/${member.slug}`} link>
|
||||
{member.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
{artist.members.map(({ member, alias, as, notes }) => (
|
||||
<Column key={member.slug}>
|
||||
<Text as={Link} href={`/artist/${member.slug}`} link>
|
||||
{alias ? alias : member.name}
|
||||
</Text>
|
||||
{as || notes ? (
|
||||
<Text variant="small" color="text-muted">
|
||||
{[as ? `As ${as}` : null, notes || null]
|
||||
.filter(Boolean)
|
||||
.join(" • ")}
|
||||
</Text>
|
||||
) : null}
|
||||
</Column>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
@@ -216,12 +230,23 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) {
|
||||
{!!artist.groups?.length && (
|
||||
<DescriptionList.Item title="Member of">
|
||||
<StyledList>
|
||||
{artist.groups.map(({ group, alias, as }) => (
|
||||
<Text key={group.slug} as={Link} href={`/artist/${group.slug}`} link>
|
||||
{group.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
{artist.groups.map(({ group, alias, as, notes }) => (
|
||||
<Column key={group.slug}>
|
||||
<Text as={Link} href={`/artist/${group.slug}`} link>
|
||||
{group.name}
|
||||
</Text>
|
||||
{alias || as || notes ? (
|
||||
<Text variant="small" color="text-muted">
|
||||
{[
|
||||
alias ? `As ${alias}` : null,
|
||||
as ? `As ${as}` : null,
|
||||
notes || null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" • ")}
|
||||
</Text>
|
||||
) : null}
|
||||
</Column>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
@@ -243,6 +268,18 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) {
|
||||
</DescriptionList>
|
||||
</Column>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
{!!informationMarkdownSource && (
|
||||
<>
|
||||
<Text variant="h2">Information</Text>
|
||||
<Card $hoverable onClick={() => setCollapseInformation(!collapseInformation)}>
|
||||
<HeightTransition>
|
||||
<Text as="div" maxLines={collapseInformation ? 2 : undefined}>
|
||||
<Markdown source={informationMarkdownSource} />
|
||||
</Text>
|
||||
</HeightTransition>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
<StyledHeader>
|
||||
<Text variant="h2">
|
||||
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<ArtistDetailPageProps, ArtistDetailP
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
artist: data.artist,
|
||||
informationMarkdownSource: data.artist.information
|
||||
? (await serializeMarkdownSafe(data.artist.information)).source
|
||||
: null,
|
||||
},
|
||||
// Revalidate after 1 hour (= 3600 seconds).
|
||||
revalidate: 3600,
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ import useCurrentSeason from "@/hooks/useCurrentSeason";
|
||||
import { fetchData } from "@/lib/server";
|
||||
import theme from "@/theme";
|
||||
import getSharedPageProps from "@/utils/getSharedPageProps";
|
||||
import serializeMarkdown from "@/utils/serializeMarkdown";
|
||||
import { serializeMarkdownSafe } from "@/utils/serializeMarkdown";
|
||||
|
||||
const BigButton = styled(Button)`
|
||||
justify-content: flex-end;
|
||||
@@ -246,7 +246,7 @@ export const getStaticProps: GetStaticProps<HomePageProps> = 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,
|
||||
),
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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<ExtractMultiplesImagesResult>;
|
||||
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
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -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\`\`\``);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user