feat: Added support for studios (#72)

* Added studio page and included links on anime pages.
* Series and studios now have summary cards.
* Added series and studio cards to the video page.
* New search indices (currently hidden) for series and studios.
* Fixed bug where icons would not show on big buttons.
This commit is contained in:
Manuel S
2021-08-23 02:53:02 +02:00
committed by GitHub
parent 737c7b9473
commit 2d847272bb
22 changed files with 1635 additions and 71 deletions
+14 -1
View File
@@ -36,6 +36,11 @@ export async function createPages({ actions: { createPage }, graphql }) {
slug
}
}
allStudio {
nodes {
slug
}
}
allArtist {
nodes {
slug
@@ -103,6 +108,14 @@ export async function createPages({ actions: { createPage }, graphql }) {
});
}
for (const { slug } of data.allStudio.nodes) {
createPage({
path: `/studio/${slug}`,
component: require.resolve("./src/templates/studio.js"),
context: { slug }
});
}
for (const { slug } of data.allArtist.nodes) {
createPage({
path: `/artist/${slug}`,
@@ -119,7 +132,7 @@ export async function createPages({ actions: { createPage }, graphql }) {
});
}
for (const entity of [ null, "anime", "theme", "artist" ]) {
for (const entity of [ null, "anime", "theme", "artist", "series", "studio" ]) {
createPage({
path: `/search${entity ? `/${entity}` : ``}`,
component: require.resolve("./src/templates/search.js"),
@@ -9,7 +9,8 @@ module.exports = ({ createResolvers }) => {
...pivotResolver({
series: { type: "AnimeSeries", from: "anime", to: "series" },
resources: { type: "AnimeResource", from: "anime", to: "resource" },
images: { type: "AnimeImage", from: "anime", to: "image" }
images: { type: "AnimeImage", from: "anime", to: "image" },
studios: { type: "AnimeStudio", from: "anime", to: "studio" }
})
},
Artist: {
@@ -43,6 +44,11 @@ module.exports = ({ createResolvers }) => {
anime: { type: "AnimeSeries", from: "series", to: "anime" }
})
},
Studio: {
...pivotResolver({
anime: { type: "AnimeStudio", from: "studio", to: "anime" }
})
},
Video: {
...enumResolver({
overlap: videoOverlap,
@@ -17,6 +17,7 @@ module.exports = ({ actions }) => {
series: [Series]
resources: [Resource]
images: [Image]
studios: [Studio]
}
type Synonym implements Node & Entity {
@@ -93,7 +94,7 @@ module.exports = ({ actions }) => {
type Series implements Node & Entity {
idRaw: Int
slug: String!
name: String!
name: String!
anime: [Anime]
}
@@ -109,6 +110,13 @@ module.exports = ({ actions }) => {
link: String
}
type Studio implements Node & Entity {
idRaw: Int
slug: String!
name: String!
anime: [Anime]
}
type AnimeSeries implements Node {
anime: Anime! @link(by: "id")
series: Series! @link(by: "id")
@@ -139,6 +147,11 @@ module.exports = ({ actions }) => {
image: Image! @link(by: "id")
}
type AnimeStudio implements Node {
anime: Anime! @link(by: "id")
studio: Studio! @link(by: "id")
}
type Announcement implements Node & Entity {
idRaw: Int
content: String!
@@ -158,6 +158,15 @@ module.exports = async ({ actions, createNodeId, createContentDigest, reporter }
}, "Image", helpers);
}
for (const studio of await selectAllFrom("studios")) {
createNodeFromData({
id: studio.studio_id,
idRaw: studio.studio_id,
slug: studio.slug,
name: studio.name
}, "Studio", helpers);
}
try {
for (const announcement of await selectAllFrom("announcements")) {
createNodeFromData({
@@ -270,6 +279,18 @@ module.exports = async ({ actions, createNodeId, createContentDigest, reporter }
}, "EntryVideo", helpers);
}
for (const animeStudio of await selectAllFrom(
"anime_studio", true,
"anime", "anime_id", "anime_id",
"studios", "studio_id", "studio_id"
)) {
createNodeFromData({
id: `${animeStudio.anime_id}-${animeStudio.studio_id}`,
anime: createNodeId(`Anime-${animeStudio.anime_id}`),
studio: createNodeId(`Studio-${animeStudio.studio_id}`)
}, "AnimeStudio", helpers);
}
// await cache.set("last-fetched", now);
};
+1231 -14
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -14,10 +14,12 @@ const StyledCover = styled.img.attrs({
export function SummaryCard({ title, description, image, to, children, ...props }) {
return (
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" {...props}>
<Link to={to}>
<StyledCover alt="Cover" src={image}/>
</Link>
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}>
{!!image && (
<Link to={to}>
<StyledCover alt="Cover" src={image} loading="lazy"/>
</Link>
)}
<Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem">
<Text fontWeight="600" maxLines={1}>
{typeof title === "string" ? (
+2 -1
View File
@@ -7,6 +7,7 @@ import {
import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { AnimeSummaryCard } from "components/card";
const sortByFields = new Map([
[ "Relevance", null ],
@@ -53,7 +54,6 @@ export function SearchAnime({ searchQuery, locationState }) {
return (
<SearchEntity
searchQuery={searchQuery}
searchEntity="anime"
filters={
<>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
@@ -68,6 +68,7 @@ export function SearchAnime({ searchQuery, locationState }) {
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isPlaceholderData={isPlaceholderData}
renderSummaryCard={(anime) => <AnimeSummaryCard key={anime.slug} anime={anime}/>}
/>
);
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-f
import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { ArtistSummaryCard } from "components/card";
const sortByFields = new Map([
[ "Relevance", null ],
@@ -42,7 +43,6 @@ export function SearchArtist({ searchQuery, locationState }) {
return (
<SearchEntity
searchQuery={searchQuery}
searchEntity="artist"
filters={
<>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
@@ -55,6 +55,7 @@ export function SearchArtist({ searchQuery, locationState }) {
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isPlaceholderData={isPlaceholderData}
renderSummaryCard={(artist) => <ArtistSummaryCard key={artist.slug} artist={artist}/>}
/>
);
}
+3 -4
View File
@@ -3,19 +3,18 @@ import { Button } from "components/button";
import { Text } from "components/text";
import { Box, Flex } from "components/box";
import { Icon } from "components/icon";
import { SearchResultList } from "components/search";
import { SearchFilterGroup } from "components/search-filter";
export function SearchEntity({
searchQuery,
searchEntity,
filters,
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isPlaceholderData
isPlaceholderData,
renderSummaryCard
}) {
return (
<Box gapsColumn="1.5rem">
@@ -50,7 +49,7 @@ export function SearchEntity({
return (
<>
<Box gapsColumn="1rem">
<SearchResultList results={results} entity={searchEntity}/>
{results.map(renderSummaryCard)}
</Box>
{(hasNextPage || isPlaceholderData) && (
<Flex justifyContent="center">
+21 -6
View File
@@ -5,9 +5,9 @@ import { Text } from "components/text";
import { Box, Flex } from "components/box";
import { Link } from "gatsby";
import { Icon } from "components/icon";
import { SearchResultList } from "components/search";
import { useQuery } from "react-query";
import { fetchGlobalSearchResults } from "lib/search";
import { AnimeSummaryCard, ArtistSummaryCard, ThemeSummaryCard } from "components/card";
export function SearchGlobal({ searchQuery }) {
const fetchSearchResults = () => fetchGlobalSearchResults(
@@ -48,14 +48,29 @@ export function SearchGlobal({ searchQuery }) {
return (
<Box gapsColumn="1.5rem">
<GlobalSearchSection entity="anime" title="Anime" results={animeResults}/>
<GlobalSearchSection entity="theme" title="Themes" results={themeResults}/>
<GlobalSearchSection entity="artist" title="Artist" results={artistResults}/>
<GlobalSearchSection
entity="anime"
title="Anime"
results={animeResults}
renderSummaryCard={(anime) => <AnimeSummaryCard key={anime.slug} anime={anime}/>}
/>
<GlobalSearchSection
entity="theme"
title="Themes"
results={themeResults}
renderSummaryCard={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme}/>}
/>
<GlobalSearchSection
entity="artist"
title="Artist"
results={artistResults}
renderSummaryCard={(artist) => <ArtistSummaryCard key={artist.slug} artist={artist}/>}
/>
</Box>
);
}
function GlobalSearchSection({ entity, title, results }) {
function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
const { search, hash } = useLocation();
const urlSuffix = search + hash;
@@ -70,7 +85,7 @@ function GlobalSearchSection({ entity, title, results }) {
<>
<Text variant="h2">{title}</Text>
<Box gapsColumn="1rem">
<SearchResultList results={resultsPreview} entity={entity}/>
{resultsPreview.map(renderSummaryCard)}
</Box>
{hasMoreResults && (
<Flex justifyContent="center">
-20
View File
@@ -1,20 +0,0 @@
import { AnimeSummaryCard, ArtistSummaryCard, ThemeSummaryCard } from "components/card";
export function SearchResultList({ results, entity }) {
return (
<>
{results.map((result) => {
switch (entity) {
case "anime":
return <AnimeSummaryCard key={result.slug} anime={result}/>;
case "theme":
return <ThemeSummaryCard key={result.anime.slug + result.slug} theme={result}/>;
case "artist":
return <ArtistSummaryCard key={result.slug} artist={result}/>;
default:
return null;
}
})}
</>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { SummaryCard } from "components/card";
const sortByFields = new Map([
[ "Relevance", null ],
[ "A ➜ Z", "name" ],
[ "Z ➜ A", "-name" ],
[ "Last Added", "-created_at" ]
]);
const sortByOptions = [ ...sortByFields.keys() ];
export function SearchSeries({ searchQuery, locationState }) {
const filterFirstLetter = locationState?.filterFirstLetter || null;
const sortBy = locationState?.sortBy || sortByOptions[0];
const updateState = (field) => (newValue) => {
navigate("", {
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isPlaceholderData
} = useEntitySearch("series", searchQuery, {
filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
},
sortBy: sortByFields.get(sortBy)
});
return (
<SearchEntity
searchQuery={searchQuery}
filters={
<>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
<SearchFilterSortBy options={sortByOptions} value={sortBy} setValue={updateState("sortBy")}/>
</>
}
data={data}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isPlaceholderData={isPlaceholderData}
renderSummaryCard={(series) => <SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />}
/>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { SummaryCard } from "components/card";
const sortByFields = new Map([
[ "Relevance", null ],
[ "A ➜ Z", "name" ],
[ "Z ➜ A", "-name" ],
[ "Last Added", "-created_at" ]
]);
const sortByOptions = [ ...sortByFields.keys() ];
export function SearchStudio({ searchQuery, locationState }) {
const filterFirstLetter = locationState?.filterFirstLetter || null;
const sortBy = locationState?.sortBy || sortByOptions[0];
const updateState = (field) => (newValue) => {
navigate("", {
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
isPlaceholderData
} = useEntitySearch("studio", searchQuery, {
filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
},
sortBy: sortByFields.get(sortBy)
});
return (
<SearchEntity
searchQuery={searchQuery}
filters={
<>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
<SearchFilterSortBy options={sortByOptions} value={sortBy} setValue={updateState("sortBy")}/>
</>
}
data={data}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isPlaceholderData={isPlaceholderData}
renderSummaryCard={(studio) => <SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />}
/>
);
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { SearchFilterSortBy, SearchFilterThemeType } from "components/search-fil
import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { ThemeSummaryCard } from "components/card";
const sortByFields = new Map([
[ "Relevance", null ],
@@ -40,7 +41,6 @@ export function SearchTheme({ searchQuery, locationState }) {
return (
<SearchEntity
searchQuery={searchQuery}
searchEntity="theme"
filters={
<>
<SearchFilterThemeType value={filterType} setValue={updateState("filterType")}/>
@@ -53,6 +53,7 @@ export function SearchTheme({ searchQuery, locationState }) {
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isPlaceholderData={isPlaceholderData}
renderSummaryCard={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme}/>}
/>
);
}
+2 -1
View File
@@ -3,4 +3,5 @@ export { SearchEntity } from "./SearchEntity";
export { SearchAnime } from "./SearchAnime";
export { SearchTheme } from "./SearchTheme";
export { SearchArtist } from "./SearchArtist";
export { SearchResultList } from "./SearchResultList";
export { SearchSeries } from "./SearchSeries";
export { SearchStudio } from "./SearchStudio";
+17
View File
@@ -88,6 +88,23 @@ const entityConfigs = {
// TODO: Wait till we can specify deep field property lists
// song: []
}
},
series: {
fields: {
series: [
"name",
"slug"
]
}
},
studio: {
plural: "studios",
fields: {
studio: [
"name",
"slug"
]
}
}
};
+35 -8
View File
@@ -42,7 +42,7 @@ export default function DevelopmentPage({ data: { site, allAnnouncement } }) {
)}
</Text>
<Text as="p">
<span>This page is still activily being worked on. If you are a developer and interested in contributing feel free to contact us on </span>
<span>This page is still actively being worked on. If you are a developer and interested in contributing feel free to contact us on </span>
<ExternalLink href="https://discordapp.com/invite/m9zbVyQ">Discord</ExternalLink>
<span>.</span>
</Text>
@@ -73,21 +73,30 @@ export default function DevelopmentPage({ data: { site, allAnnouncement } }) {
<Box gapsColumn={["2rem", "1.5rem"]}>
<PageGridItem
path="/"
description={
<Text>
<Text variant="small" color="text-primary" letterSpacing="0.1rem">NEW: </Text>
The new home page of AnimeThemes.
</Text>
}
/>
<PageGridItem
path="/dev"
description="This page!"
/>
<PageGridItem
path="/design"
description={
<Text>
<Text variant="small" color="text-primary" letterSpacing="0.1rem">NEW: </Text>
A page listing various design components!
</Text>
}
description="A page listing various design components."
isNew
/>
<PageGridItem
path="/search"
description="Search the AnimeThemes database. You can also use the search bar in the navigation."
description="Search the AnimeThemes database."
otherPaths={{
"/search/anime": "Search only for anime or explore the list of all anime.",
"/search/theme": "Search only for themes or explore the list of all themes.",
"/search/artist": "Search only for artists or explore the list of all artists.",
}}
/>
<PageGridItem
path="/year"
@@ -113,6 +122,15 @@ export default function DevelopmentPage({ data: { site, allAnnouncement } }) {
"/series/fma": "Only two anime."
}}
/>
<PageGridItem
path="/studio/kyoto_animation"
description={
<Text>
<Text variant="small" color="text-primary" letterSpacing="0.1rem">NEW: </Text>
Browse all anime which were produced by the same studio.
</Text>
}
/>
<PageGridItem
path="/artist/kana_hanazawa"
description="Browse all songs an artist has performed."
@@ -142,6 +160,15 @@ export default function DevelopmentPage({ data: { site, allAnnouncement } }) {
"/anime/shingeki_no_kyojin_ova/ED1-NCBD1080": "See above."
}}
/>
<PageGridItem
path="/bracket/best-anime-opening-ix-salty-arrow"
description={
<Text>
<Text variant="small" color="text-primary" letterSpacing="0.1rem">NEW: </Text>
Explore every theme competing in one of the brackets hosted at AnimeBracket.
</Text>
}
/>
</Box>
{!!process.env.GATSBY_CI && (
<>
+4 -3
View File
@@ -50,10 +50,11 @@ const BigButton = styled(Button).attrs({
justify-content: flex-end;
`;
const BigIcon = styled(Icon)`
const BigIcon = styled(Icon).attrs({
size: "4x"
})`
margin: 0 auto -1rem -2rem;
font-size: 4rem;
color: ${theme.colors["text-disabled"]};
`;
@@ -118,7 +119,7 @@ export default function HomePage({ data: { featuredTheme, recentlyAdded } }) {
We provide high quality WebMs of your favorite OPs and EDs for your listening and discussion needs.
</Text>
<Text as="p">
<span>This page is still activily being worked on. If you are a developer and interested in contributing feel free to contact us on </span>
<span>This page is still actively being worked on. If you are a developer and interested in contributing feel free to contact us on </span>
<ExternalLink href="https://discordapp.com/invite/m9zbVyQ">Discord</ExternalLink>
<span>.</span>
</Text>
+19 -2
View File
@@ -49,7 +49,7 @@ export default function AnimeDetailPage({ data: { anime } }) {
</Text>
</Link>
</DescriptionList.Item>
{!!anime.series && !!anime.series.length && (
{!!anime.series?.length && (
<DescriptionList.Item title="Series">
<StyledList>
{anime.series.map((series) =>
@@ -62,7 +62,20 @@ export default function AnimeDetailPage({ data: { anime } }) {
</StyledList>
</DescriptionList.Item>
)}
{!!anime.resources && !!anime.resources.length && (
{!!anime.studios?.length && (
<DescriptionList.Item title="Studios">
<StyledList>
{anime.studios.map((studio) =>
<Link key={studio.slug} to={`/studio/${studio.slug}`}>
<Text link>
{studio.name}
</Text>
</Link>
)}
</StyledList>
</DescriptionList.Item>
)}
{!!anime.resources?.length && (
<DescriptionList.Item title="Links">
<StyledList>
{anime.resources.map((resource) => (
@@ -108,6 +121,10 @@ export const query = graphql`
slug
name
}
studios {
slug
name
}
themes {
group
slug
+5 -1
View File
@@ -3,7 +3,7 @@ import styled from "styled-components";
import { Link, navigate } from "gatsby";
import { useDebounce } from "use-debounce";
import { SearchInput } from "components/input";
import { SearchAnime, SearchArtist, SearchGlobal, SearchTheme } from "components/search";
import { SearchAnime, SearchArtist, SearchGlobal, SearchTheme, SearchSeries, SearchStudio } from "components/search";
import { gapsColumn, gapsRow } from "styles/mixins";
import { Box, Flex } from "components/box";
import { Switcher } from "components/switcher";
@@ -117,6 +117,10 @@ function Search({ searchQuery, searchEntity, locationState }) {
return <SearchTheme searchQuery={searchQuery} locationState={locationState}/>;
case "artist":
return <SearchArtist searchQuery={searchQuery} locationState={locationState}/>;
case "series":
return <SearchSeries searchQuery={searchQuery} locationState={locationState}/>;
case "studio":
return <SearchStudio searchQuery={searchQuery} locationState={locationState}/>;
default:
// Should never happen
return null;
+92
View File
@@ -0,0 +1,92 @@
import { SidebarContainer } from "components/container";
import styled from "styled-components";
import { AnimeSummaryCard } from "components/card";
import { SEO } from "components/seo";
import { graphql } from "gatsby";
import useImage from "hooks/useImage";
import { Box } from "components/box";
import { Text } from "components/text";
import { AspectRatio } from "components/utils";
const StyledCoverContainer = styled.div`
display: flex;
flex-direction: row;
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
`;
const StyledCoverItemContainer = styled.div`
flex: 1;
width: 100%;
height: 100%;
`;
const StyledCover = styled.img`
width: 100%;
height: 100%;
object-fit: cover;
`;
export default function StudioDetailPage({ data: { studio } }) {
const anime = studio.anime.sort((a, b) => a.name.localeCompare(b.name));
const images = [
useImage(anime[0]),
useImage(anime[1]),
useImage(anime[2]),
useImage(anime[3])
].map((images) => images.largeCover).filter((image) => !!image);
return (
<Box gapsColumn="1.5rem">
<SEO title={studio.name} />
<Text variant="h1">{studio.name}</Text>
<SidebarContainer>
<Box gapsColumn="1.5rem">
<AspectRatio ratio={1 / 2}>
<StyledCoverContainer>
{images.map((image) => (
<StyledCoverItemContainer key={image}>
<StyledCover src={image}/>
</StyledCoverItemContainer>
))}
</StyledCoverContainer>
</AspectRatio>
</Box>
<Box gapsColumn="1.5rem">
<Text variant="h2">Anime</Text>
<Box gapsColumn="1rem">
{anime.map((anime) => (
<AnimeSummaryCard key={anime.slug} anime={anime}/>
))}
</Box>
</Box>
</SidebarContainer>
</Box>
);
}
export const query = graphql`
query($slug: String!) {
studio(slug: { eq: $slug }) {
slug
name
anime {
slug
name
year
season
themes {
slug
...VideoSlug
}
resources {
link
site
}
images {
facet
link
}
}
}
}
`;
+16 -2
View File
@@ -6,7 +6,7 @@ import { Text } from "components/text";
import useSiteMeta from "hooks/useSiteMeta";
import { SongTitleWithArtists, ThemeEntryTags, VideoTags } from "components/utils";
import { VideoButton } from "components/button";
import { AnimeSummaryCard, ArtistSummaryCard } from "components/card";
import { AnimeSummaryCard, ArtistSummaryCard, SummaryCard } from "components/card";
import { SEO } from "components/seo";
import useImage from "hooks/useImage";
@@ -105,8 +105,14 @@ export default function VideoPage({ data: { video, entry } }) {
<Box gapsColumn="1rem">
<Text variant="h2">Related entries</Text>
<Flex flexDirection={["column", "row"]} gapsColumn={["1rem", 0]} gapsRow={[0, "1rem"]}>
<Box flex="1">
<Box flex="1" gapsColumn="1rem">
<AnimeSummaryCard anime={anime} hideThemes/>
{!!anime.series?.length && anime.series.map((series) => (
<SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />
))}
{!!anime.studios?.length && anime.studios.map((studio) => (
<SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />
))}
</Box>
<Box flex="1">
<Box gapsColumn="1rem">
@@ -193,6 +199,14 @@ export const query = graphql`
facet
link
}
series {
slug
name
}
studios {
slug
name
}
}
song {
title