feat: Restructured home page (#208)

* Added recently added playlists to home page.
* Added link to blog to home page.
* Added profile picture to "My Profile" button on the home page.
* Changed the order of sections in the "Related" tab on video pages.
* Fixed images not displaying on studio summary cards in the context of video pages.
* Fixed video player not updating correctly when switching between entries with the same video.
* Fixed incorrect watch list creation when clicking on the background of the number one theme in a ranked playlist.
* Fixed playlist summary cards displaying the visibility instead of the owner on video pages.
* Fixed popovers clipping outside the visible area.
This commit is contained in:
Mani
2024-05-03 00:27:51 +02:00
committed by GitHub
parent 9737e2070e
commit 48294f4eaf
13 changed files with 342 additions and 268 deletions
+50
View File
@@ -0,0 +1,50 @@
import { type SyntheticEvent, useState } from "react";
import type { Property } from "csstype";
import gql from "graphql-tag";
import type { StudioSummaryCardStudioFragment } from "generated/graphql";
import extractBackgroundColor from "utils/extractBackgroundColor";
import { SummaryCard } from "components/card/SummaryCard";
import extractImages from "utils/extractImages";
interface StudioSummaryCardProps {
studio: StudioSummaryCardStudioFragment;
}
export function StudioSummaryCard({ studio }: StudioSummaryCardProps) {
const [backgroundColor, setBackgroundColor] = useState<Property.Background>();
function handleLoad(event: SyntheticEvent<HTMLImageElement>) {
const image = event.currentTarget;
const color = extractBackgroundColor(image);
if (color) {
setBackgroundColor(color);
}
}
return (
<SummaryCard
key={studio.slug}
title={studio.name}
description="Studio"
to={`/studio/${studio.slug}`}
image={extractImages(studio).largeCover}
imageProps={{
objectFit: "contain",
backgroundColor,
onLoad: handleLoad
}}
/>
);
}
StudioSummaryCard.fragments = {
studio: gql`
${extractImages.fragments.resourceWithImages}
fragment StudioSummaryCardStudio on Studio {
slug
name
...extractImagesResourceWithImages
}
`,
};
+1
View File
@@ -115,6 +115,7 @@ export const VideoSummaryCardFragmentVideo = gql`
...createVideoSlugVideo
entries {
...createVideoSlugEntry
id
theme {
...createVideoSlugTheme
id
@@ -121,9 +121,10 @@ interface FeaturedThemeProps {
theme: FeaturedThemeThemeFragment;
hasGrill?: boolean;
card?: ReactNode;
onPlay?(): void;
}
export function FeaturedTheme({ theme, hasGrill = true, card }: FeaturedThemeProps) {
export function FeaturedTheme({ theme, hasGrill = true, card, onPlay }: FeaturedThemeProps) {
const [ grill, setGrill ] = useState<string | null>(null);
const [ featuredThemePreview ] = useSetting(FeaturedThemePreview);
@@ -149,7 +150,7 @@ export function FeaturedTheme({ theme, hasGrill = true, card }: FeaturedThemePro
return (
<FeaturedThemeWrapper>
<FeaturedThemeBackground theme={theme}/>
<FeaturedThemeBackground theme={theme} onPlay={onPlay}/>
{featuredThemePreview !== FeaturedThemePreview.DISABLED && grill && (
<StyledGrillContainer>
<StyledGrill src={grill}/>
@@ -160,7 +161,7 @@ export function FeaturedTheme({ theme, hasGrill = true, card }: FeaturedThemePro
);
}
function FeaturedThemeBackground({ theme }: FeaturedThemeProps) {
function FeaturedThemeBackground({ theme, onPlay }: FeaturedThemeProps) {
const [ featuredThemePreview ] = useSetting(FeaturedThemePreview);
const { canPlayVideo } = useCompatability();
const [ fallbackToCover, setFallbackToCover ] = useState(false);
@@ -183,7 +184,7 @@ function FeaturedThemeBackground({ theme }: FeaturedThemeProps) {
if (featuredThemePreview === FeaturedThemePreview.VIDEO && canPlayVideo && !fallbackToCover) {
return (
<StyledOverflowHidden href={href}>
<StyledOverflowHidden href={href} onClick={onPlay}>
<StyledVideo
key={video.basename}
autoPlay
@@ -200,7 +201,7 @@ function FeaturedThemeBackground({ theme }: FeaturedThemeProps) {
);
} else if (featuredThemePreview !== FeaturedThemePreview.DISABLED) {
return (
<StyledOverflowHidden href={href}>
<StyledOverflowHidden href={href} onClick={onPlay}>
<StyledCover src={featuredCover}/>
</StyledOverflowHidden>
);
+1 -1
View File
@@ -139,7 +139,7 @@ export const Listbox = forwardRef<HTMLButtonElement, ListboxProps>(
</StyledListboxButton>
</RadixSelect.Trigger>
<StyledListboxPopover>
<StyledListboxList position="popper" sideOffset={8}>
<StyledListboxList position="popper" sideOffset={8} collisionBoundary={typeof document !== "undefined" ? document.body : []}>
<RadixSelect.Viewport>{children}</RadixSelect.Viewport>
</StyledListboxList>
</StyledListboxPopover>
+8 -1
View File
@@ -26,7 +26,14 @@ export const MenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
function MenuContent({ children, ...props }, forwardedRef) {
return (
<RadixMenu.Portal>
<StyledMenuContent align="start" sideOffset={8} collisionPadding={8} {...props} ref={forwardedRef}>
<StyledMenuContent
align="start"
sideOffset={8}
collisionPadding={8}
collisionBoundary={typeof document !== "undefined" ? document.body : []}
{...props}
ref={forwardedRef}
>
{children}
</StyledMenuContent>
</RadixMenu.Portal>
+1 -36
View File
@@ -1,16 +1,12 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import { SearchEntity } from "components/search";
import { SummaryCard } from "components/card";
import type { SyntheticEvent } from "react";
import { useState } from "react";
import { fetchDataClient } from "lib/client";
import type { SearchStudioQuery, SearchStudioQueryVariables } from "generated/graphql";
import gql from "graphql-tag";
import { StudioCoverImage } from "components/image/StudioCoverImage";
import extractImages from "utils/extractImages";
import extractBackgroundColor from "utils/extractBackgroundColor";
import type { Property } from "csstype";
import useFilterStorage from "hooks/useFilterStorage";
import { StudioSummaryCard } from "components/card/StudioSummaryCard";
const initialFilter = {
firstLetter: null,
@@ -90,34 +86,3 @@ export function SearchStudio({ searchQuery }: SearchStudioProps) {
/>
);
}
interface StudioSummaryCardProps {
studio: SearchStudioQuery["searchStudio"]["data"][number]
}
function StudioSummaryCard({ studio }: StudioSummaryCardProps) {
const [backgroundColor, setBackgroundColor] = useState<Property.Background>();
function handleLoad(event: SyntheticEvent<HTMLImageElement>) {
const image = event.currentTarget;
const color = extractBackgroundColor(image);
if (color) {
setBackgroundColor(color);
}
}
return (
<SummaryCard
key={studio.slug}
title={studio.name}
description="Studio"
to={`/studio/${studio.slug}`}
image={extractImages(studio).largeCover}
imageProps={{
objectFit: "contain",
backgroundColor,
onLoad: handleLoad
}}
/>
);
}
+26 -11
View File
@@ -218,6 +218,7 @@ export type Query = {
page: Maybe<Page>;
pageAll: Array<Page>;
playlist: Maybe<Playlist>;
playlistAll: Array<Playlist>;
search: GlobalSearchResult;
searchAnime: AnimeSearchResult;
searchArtist: ArtistSearchResult;
@@ -284,6 +285,13 @@ export type QueryPlaylistArgs = {
};
export type QueryPlaylistAllArgs = {
limit: InputMaybe<Scalars['Int']>;
orderBy: InputMaybe<Scalars['String']>;
orderDesc: InputMaybe<Scalars['Boolean']>;
};
export type QuerySearchArgs = {
args: SearchArgs;
};
@@ -565,6 +573,8 @@ export type PlaylistSummaryCardPlaylistFragment = { id: string, name: string, vi
export type PlaylistSummaryCardShowOwnerFragment = { user: { name: string } };
export type StudioSummaryCardStudioFragment = { slug: string, name: string, images: Array<{ link: string, facet: string | null }> };
export type ThemeDetailCardThemeFragment = { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> };
export type ThemeSummaryCardThemeFragment = { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> };
@@ -580,13 +590,13 @@ export type ThemeSummaryCardQueryVariables = Exact<{
export type ThemeSummaryCardQuery = { theme: { type: string, sequence: number | null, id: number, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } | null };
export type VideoSummaryCardVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type VideoSummaryCardVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistEditDialogPlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility };
export type PlaylistRemoveDialogPlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number };
export type PlaylistTrackAddDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistTrackAddDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistTrackAddFormPlaylistQueryVariables = Exact<{
filterVideoId: Scalars['Int'];
@@ -597,7 +607,7 @@ export type PlaylistTrackAddFormPlaylistQuery = { me: { playlistAll: Array<{ id:
export type PlaylistTrackRemoveDialogPlaylistFragment = { id: string, name: string };
export type PlaylistTrackRemoveDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type PlaylistTrackRemoveDialogVideoFragment = { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } };
export type FeaturedThemeThemeFragment = { type: string, sequence: number | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, entries: Array<{ version: number | null, videos: Array<{ basename: string, id: number, tags: string, audio: { basename: string } }> }>, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null };
@@ -735,19 +745,19 @@ export type DumpIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
export type DumpIndexPageQuery = { dumpAll: Array<{ path: string, link: string, created_at: string }> };
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> };
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ id: number, episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string, images: Array<{ link: string, facet: string | null }> }> };
export type VideoPageQueryVariables = Exact<{
animeSlug: Scalars['String'];
}>;
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> } | null };
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ id: number, episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string, images: Array<{ link: string, facet: string | null }> }> } | null };
export type VideoPageAllQueryVariables = Exact<{ [key: string]: never; }>;
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> }> };
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, media_format: string | null, themes: Array<{ id: number, type: string, sequence: number | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ id: number, episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ id: number, basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ id: number, basename: string, tags: string, audio: { basename: string } }> }> } }>, tracks: Array<{ playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } } }>, audio: { basename: string }, script: { link: string } | null }> }>, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string, images: Array<{ link: string, facet: string | null }> }> }> };
export type AnimeDetailPageAnimeFragment = { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, media_format: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ type: string, sequence: number | null, id: number, group: { name: string, slug: string } | null, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, id: number, basename: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap, audio: { basename: string } }> }> }>, images: Array<{ link: string, facet: string | null }> };
@@ -835,12 +845,17 @@ export type EventPageQuery = { bracketAll: Array<{ slug: string, name: string }>
export type HomePageRecentlyAddedQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageRecentlyAddedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageRecentlyAddedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageMostViewedQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageMostViewedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageMostViewedQuery = { videoAll: Array<{ id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, 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<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } }> };
export type HomePageRecentlyAddedPlaylistsQueryVariables = Exact<{ [key: string]: never; }>;
export type HomePageRecentlyAddedPlaylistsQuery = { playlistAll: Array<{ id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, user: { name: string } }> };
export type HomePageQueryVariables = Exact<{ [key: string]: never; }>;
@@ -852,14 +867,14 @@ export type PlaylistDetailPagePlaylistQueryVariables = Exact<{
}>;
export type PlaylistDetailPagePlaylistQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null };
export type PlaylistDetailPagePlaylistQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null };
export type PlaylistDetailPageMeQueryVariables = Exact<{ [key: string]: never; }>;
export type PlaylistDetailPageMeQuery = { me: { user: { name: string } | null } };
export type PlaylistDetailPagePlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } };
export type PlaylistDetailPagePlaylistFragment = { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } };
type PlaylistDetailPageUser_UserAuth_Fragment = { name: string };
@@ -872,7 +887,7 @@ export type PlaylistDetailPageQueryVariables = Exact<{
}>;
export type PlaylistDetailPageQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null, me: { user: { name: string } | null } };
export type PlaylistDetailPageQuery = { playlist: { id: string, name: string, visibility: PlaylistVisibility, tracks_count: number, forward: Array<{ id: string, video: { id: number, basename: string, tags: string, entries: Array<{ id: number, version: number | null, theme: { id: number, type: string, sequence: number | null, anime: { year: number | null, season: string | null, slug: string, name: string, images: Array<{ link: string, facet: string | null }> }, group: { name: string, slug: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } }>, audio: { basename: string } } }>, user: { name: string } } | null, me: { user: { name: string } | null } };
export type GalleryPageQueryVariables = Exact<{ [key: string]: never; }>;
+5
View File
@@ -96,6 +96,11 @@ const resolvers: IResolvers = {
endpoint: (_, { id }) => `/playlist/${id}?fields[playlist]=id,name,visibility,tracks_count`,
extractor: (result) => result.playlist,
}),
playlistAll: apiResolver({
endpoint: (_, { limit, orderBy, orderDesc }) =>
`/playlist?sort=${orderDesc ? "-" : ""}${orderBy}&page[size]=${limit}&fields[playlist]=id,name,visibility,tracks_count`,
extractor: (result) => result.playlists
}),
me: () => ({}),
},
UserScopedQuery: {
+1
View File
@@ -23,6 +23,7 @@ const typeDefs = `
featuredTheme: FeaturedTheme
dumpAll: [Dump!]!
playlist(id: String!): Playlist
playlistAll(limit: Int, orderBy: String, orderDesc: Boolean): [Playlist!]!
announcementAll: [Announcement!]!
me: UserScopedQuery!
}
+8 -7
View File
@@ -91,8 +91,8 @@ export default function MyApp({ Component, pageProps }: AppProps) {
const [isLocalAutoPlay, setLocalAutoPlay] = useState(true);
const [isWatchListUsingLocalAutoPlay, setIsWatchListUsingLocalAutoPlay] = useState(false);
const currentBasename = getBasename(pageProps);
const [previousBasename, setPreviousBasename] = useState<string | null>(() => getBasename(pageProps));
const currentVideoIdentifier = getVideoIdentifier(pageProps);
const [previousVideoIdentifier, setPreviousVideoIdentifier] = useState<string | null>(() => getVideoIdentifier(pageProps));
const [isFullscreen, { toggleFullscreen }] = useFullscreen(
() => document.documentElement,
@@ -138,9 +138,10 @@ export default function MyApp({ Component, pageProps }: AppProps) {
return null;
}
if (isVideoPage && currentBasename !== previousBasename) {
setPreviousBasename(currentBasename);
if (currentBasename !== currentWatchListItem?.basename) {
if (isVideoPage && currentVideoIdentifier !== previousVideoIdentifier) {
setPreviousVideoIdentifier(currentVideoIdentifier);
const watchListVideoIdentifier = currentWatchListItem ? `${currentWatchListItem.entries[0].id}-${currentWatchListItem.id}` : null;
if (currentVideoIdentifier !== watchListVideoIdentifier) {
const { anime, themeIndex, entryIndex, videoIndex }: VideoPageProps = pageProps;
const video = anime.themes[themeIndex].entries[entryIndex].videos[videoIndex];
@@ -294,7 +295,7 @@ function MultiContextProvider({ providers = [], children }: MultiContextProvider
return <>{stack}</>;
}
function getBasename(pageProps: any): string | null {
function getVideoIdentifier(pageProps: any): string | null {
if (pageProps.isVideoPage) {
const { anime, themeIndex, entryIndex, videoIndex }: VideoPageProps = pageProps;
@@ -302,7 +303,7 @@ function getBasename(pageProps: any): string | null {
const entry = theme.entries[entryIndex];
const video = entry.videos[videoIndex];
return video.basename;
return `${entry.id}-${video.id}`;
}
return null;
}
@@ -24,7 +24,8 @@ import extractImages from "utils/extractImages";
import { VIDEO_URL } from "utils/config.mjs";
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
import Switch from "components/form/Switch";
import PlaylistSummaryCard from "../../../../components/card/PlaylistSummaryCard";
import PlaylistSummaryCard from "components/card/PlaylistSummaryCard";
import { StudioSummaryCard } from "components/card/StudioSummaryCard";
export interface VideoPageProps extends SharedPageProps, RequiredNonNullable<VideoPageQuery> {
themeIndex: number
@@ -177,7 +178,7 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l
<SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />
))}
{anime.studios.map((studio) => (
<SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />
<StudioSummaryCard key={studio.slug} studio={studio} />
))}
{!!theme.song?.performances?.length && (
<>
@@ -201,32 +202,6 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l
{selectedTab === "related" ? (
<StyledScrollArea>
<Column style={{ "--gap": "16px" }}>
{!!relatedPlaylists.length && (
<>
<Text variant="h2">Part of these Playlists</Text>
{relatedPlaylists.slice(0, showMoreRelatedPlaylists ? undefined : 3).map((playlist) => (
<PlaylistSummaryCard key={playlist.id} playlist={playlist} />
))}
{relatedPlaylists.length > 3 ? (
<Row style={{ "--justify-content": "center" }}>
<IconTextButton
icon={showMoreRelatedPlaylists ? faChevronUp : faChevronDown}
variant="silent"
isCircle
onClick={() => setShowMoreRelatedPlaylists(!showMoreRelatedPlaylists)}
/>
</Row>
) : null}
</>
)}
{!!usedAlsoAs.length && (
<>
<Text variant="h2">Also Used As</Text>
{usedAlsoAs.map((theme) => theme?.anime ? (
<ThemeSummaryCard key={theme.anime.slug} theme={theme}/>
) : null)}
</>
)}
{!!relatedThemes.length && (
<>
<Text variant="h2">Related themes</Text>
@@ -245,6 +220,32 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l
) : null}
</>
)}
{!!usedAlsoAs.length && (
<>
<Text variant="h2">Also Used As</Text>
{usedAlsoAs.map((theme) => theme?.anime ? (
<ThemeSummaryCard key={theme.anime.slug} theme={theme}/>
) : null)}
</>
)}
{!!relatedPlaylists.length && (
<>
<Text variant="h2">Part of these Playlists</Text>
{relatedPlaylists.slice(0, showMoreRelatedPlaylists ? undefined : 3).map((playlist) => (
<PlaylistSummaryCard key={playlist.id} playlist={playlist} showOwner />
))}
{relatedPlaylists.length > 3 ? (
<Row style={{ "--justify-content": "center" }}>
<IconTextButton
icon={showMoreRelatedPlaylists ? faChevronUp : faChevronDown}
variant="silent"
isCircle
onClick={() => setShowMoreRelatedPlaylists(!showMoreRelatedPlaylists)}
/>
</Row>
) : null}
</>
)}
</Column>
</StyledScrollArea>
) : null}
@@ -258,6 +259,9 @@ VideoPage.fragments = {
${ThemeSummaryCard.fragments.theme}
${ArtistSummaryCard.fragments.artist}
${VideoScript.fragments.video}
${PlaylistSummaryCard.fragments.playlist}
${PlaylistSummaryCard.fragments.showOwner}
${StudioSummaryCard.fragments.studio}
fragment VideoPageAnime on Anime {
...AnimeSummaryCardAnime
@@ -278,6 +282,7 @@ VideoPage.fragments = {
}
}
entries {
id
episodes
nsfw
spoiler
@@ -302,10 +307,8 @@ VideoPage.fragments = {
}
tracks {
playlist {
id
name
visibility
tracks_count
...PlaylistSummaryCardPlaylist
...PlaylistSummaryCardShowOwner
}
}
}
@@ -320,8 +323,7 @@ VideoPage.fragments = {
name
}
studios {
slug
name
...StudioSummaryCardStudio
}
}
`,
+198 -173
View File
@@ -4,7 +4,15 @@ import { Text } from "components/text";
import styled from "styled-components";
import { Button } from "components/button";
import { Icon } from "components/icon";
import { faArrowRight, faAward, faRandom, faSearch, faTv, faUser } from "@fortawesome/pro-solid-svg-icons";
import {
faArrowRight,
faAward,
faBookOpenCover, faMegaphone,
faRandom,
faSearch,
faTv,
faUser
} from "@fortawesome/pro-solid-svg-icons";
import theme from "theme";
import { ExternalLink } from "components/external-link";
import useCurrentSeason from "hooks/useCurrentSeason";
@@ -14,7 +22,12 @@ import { FeaturedTheme } from "components/featured-theme";
import getSharedPageProps from "utils/getSharedPageProps";
import { range } from "lodash-es";
import { Skeleton } from "components/skeleton";
import type { HomePageMostViewedQuery, HomePageQuery, HomePageRecentlyAddedQuery } from "generated/graphql";
import type {
HomePageMostViewedQuery,
HomePageQuery,
HomePageRecentlyAddedPlaylistsQuery,
HomePageRecentlyAddedQuery
} from "generated/graphql";
import type { GetStaticProps } from "next";
import { AnnouncementCard } from "components/card/AnnouncementCard";
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
@@ -24,6 +37,9 @@ import useSWR from "swr";
import { fetchDataClient } from "lib/client";
import gql from "graphql-tag";
import { ShuffleDialog } from "components/dialog/ShuffleDialog";
import useAuth from "hooks/useAuth";
import { ProfileImage } from "components/image/ProfileImage";
import PlaylistSummaryCard from "components/card/PlaylistSummaryCard";
const BigButton = styled(Button)`
justify-content: flex-end;
@@ -34,66 +50,34 @@ const BigButton = styled(Button)`
`;
const BigIcon = styled(Icon)`
margin: 0 auto -1rem -2rem;
margin: 0 auto -16px -32px;
font-size: 56px;
color: ${theme.colors["text-disabled"]};
`;
const MainGrid = styled.div`
const BigProfileImage = styled(ProfileImage)`
margin: 0 auto 0 -16px;
border-radius: 0 9999px 9999px 0;
`;
const Grid = styled.div<{ $columns: number }>`
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-areas: "a . b"
"c d e"
"f f g"
"h i g"
"j j g"
". . g";
grid-template-columns: repeat(${(props) => props.$columns}, 1fr);
grid-gap: 24px;
@media (max-width: ${theme.breakpoints.mobileMax}) {
grid-template-columns: 1fr;
grid-template-areas: "a" "c" "d" "e" "h" "i" "f" "b" "g" "j";
}
`;
const MainGridArea = styled.div<{ area: string }>`
grid-area: ${(props) => props.area};
display: flex;
flex-direction: column;
`;
const SmallButtonGrid = styled.div`
grid-area: f;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 16px;
@media (max-width: ${theme.breakpoints.mobileMax}) {
grid-template-columns: 1fr 1fr;
}
`;
const RecentlyAdded = styled(Column)`
grid-area: g;
gap: 16px;
`;
const About = styled(Column)`
grid-area: j;
gap: 24px;
`;
interface HomePageProps {
featuredTheme: NonNullable<NonNullable<HomePageQuery["featuredTheme"]>["entry"]>["theme"] | null;
announcementSources: MDXRemoteSerializeResult[]
}
export default function HomePage({ featuredTheme, announcementSources }: HomePageProps) {
const { me } = useAuth();
const { currentYear, currentSeason } = useCurrentSeason();
const { data: recentlyAdded } = useSWR<HomePageRecentlyAddedQuery["videoAll"] | null[]>(
@@ -132,6 +116,26 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
{ fallbackData: range(10).map(() => null) }
);
const { data: recentlyAddedPlaylists } = useSWR<HomePageRecentlyAddedPlaylistsQuery["playlistAll"] | null[]>(
["HomePageRecentlyAddedPlaylists"],
async () => {
const { data } = await fetchDataClient<HomePageRecentlyAddedPlaylistsQuery>(gql`
${PlaylistSummaryCard.fragments.playlist}
${PlaylistSummaryCard.fragments.showOwner}
query HomePageRecentlyAddedPlaylists {
playlistAll(orderBy: "created_at", orderDesc: true, limit: 10) {
...PlaylistSummaryCardPlaylist
...PlaylistSummaryCardShowOwner
}
}
`);
return data.playlistAll;
},
{ fallbackData: range(10).map(() => null) }
);
return <>
<SEO/>
<Text variant="h1">Welcome, to AnimeThemes.moe!</Text>
@@ -147,140 +151,161 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
</>
) : null}
<MainGrid>
<MainGridArea area="a">
<Text variant="h2">Explore The Database</Text>
</MainGridArea>
<MainGridArea area="b">
<Text variant="h2">Recently Added</Text>
</MainGridArea>
<Text variant="h2">Explore The Database</Text>
<RecentlyAdded>
{recentlyAdded?.map((video, index) => (
<Skeleton key={index} variant="summary-card" delay={index * 100}>
{video ? (
<VideoSummaryCard video={video}/>
) : null}
</Skeleton>
))}
<Text variant="h2">Most Viewed</Text>
{mostViewed?.map((video, index) => (
<Skeleton key={index} variant="summary-card" delay={index * 100}>
{video ? (
<VideoSummaryCard video={video}/>
) : null}
</Skeleton>
))}
</RecentlyAdded>
<Grid $columns={3}>
<Link href="/search" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faSearch} className="fa-flip-horizontal"/>
<Text>Search</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<ShuffleDialog trigger={
<BigButton>
<BigIcon icon={faRandom}/>
<Text>Shuffle</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
} />
<Link
href={(currentYear && currentSeason) ? `/year/${currentYear}/${currentSeason}` : "/"}
passHref
legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faTv}/>
<Text>Current Season</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</Grid>
<MainGridArea area="c">
<Link href="/search" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faSearch} className="fa-flip-horizontal"/>
<Text>Search</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</MainGridArea>
<MainGridArea area="d">
<ShuffleDialog trigger={
<BigButton>
<BigIcon icon={faRandom}/>
<Text>Shuffle</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
} />
</MainGridArea>
<MainGridArea area="e">
<Link
href={(currentYear && currentSeason) ? `/year/${currentYear}/${currentSeason}` : "/"}
passHref
legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faTv}/>
<Text>Current Season</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</MainGridArea>
<MainGridArea area="h">
<Link href="/event" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faAward}/>
<Text>Events</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</MainGridArea>
<MainGridArea area="i">
<Link href="/profile" passHref legacyBehavior>
<BigButton forwardedAs="a">
<Grid $columns={4}>
<Link href="/event" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faAward}/>
<Text>Events</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/wiki" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faBookOpenCover}/>
<Text>Wiki</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/blog" passHref legacyBehavior>
<BigButton forwardedAs="a">
<BigIcon icon={faMegaphone}/>
<Text>Blog</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/profile" passHref legacyBehavior>
<BigButton forwardedAs="a">
{me.user ? (
<BigProfileImage user={me.user} size={96} />
) : (
<BigIcon icon={faUser}/>
<Text>My Profile</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</MainGridArea>
)}
<Text>My Profile</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</Grid>
<SmallButtonGrid>
<Link href="/anime" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Anime Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/artist" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Artist Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/year" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Year Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/series" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Series Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/studio" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Studio Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/wiki" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Wiki</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</SmallButtonGrid>
<Grid $columns={3}>
<Column style={{ "--gap": "24px" }}>
<Text variant="h2">Recently Added</Text>
<Column style={{ "--gap": "16px" }}>
{recentlyAdded?.map((video, index) => (
<Skeleton key={index} variant="summary-card" delay={index * 100}>
{video ? (
<VideoSummaryCard video={video}/>
) : null}
</Skeleton>
))}
</Column>
</Column>
<Column style={{ "--gap": "24px" }}>
<Text variant="h2">Most Viewed</Text>
<Column style={{ "--gap": "16px" }}>
{mostViewed?.map((video, index) => (
<Skeleton key={index} variant="summary-card" delay={index * 100}>
{video ? (
<VideoSummaryCard video={video}/>
) : null}
</Skeleton>
))}
</Column>
</Column>
<Column style={{ "--gap": "24px" }}>
<Text variant="h2">New Playlists</Text>
<Column style={{ "--gap": "16px" }}>
{recentlyAddedPlaylists?.map((playlist, index) => (
<Skeleton key={index} variant="summary-card" delay={index * 100}>
{playlist ? (
<PlaylistSummaryCard playlist={playlist} showOwner />
) : null}
</Skeleton>
))}
</Column>
</Column>
</Grid>
<About>
<Text variant="h2">About The Project</Text>
<Text as="p">
A simple and consistent repository of anime opening and ending themes.
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 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>
<Text as="p">
<span>The source code for this page can be found on </span>
<ExternalLink href="https://github.com/AnimeThemes/animethemes-web">GitHub</ExternalLink>
<span>. For our other open source projects we also have a </span>
<ExternalLink href="https://github.com/AnimeThemes">GitHub organization</ExternalLink>
<span>.</span>
</Text>
</About>
</MainGrid>
<Text variant="h2">About The Project</Text>
<Text as="p">
A simple and consistent repository of anime opening and ending themes.
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 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>
<Text as="p">
<span>The source code for this page can be found on </span>
<ExternalLink href="https://github.com/AnimeThemes/animethemes-web">GitHub</ExternalLink>
<span>. For our other open source projects we also have a </span>
<ExternalLink href="https://github.com/AnimeThemes">GitHub organization</ExternalLink>
<span>.</span>
</Text>
<Text variant="h2">Dive Deeper</Text>
<Grid $columns={5}>
<Link href="/anime" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Anime Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/artist" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Artist Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/year" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Year Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/series" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Series Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
<Link href="/studio" passHref legacyBehavior>
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
<Text>Studio Index</Text>
<Icon icon={faArrowRight} color="text-primary"/>
</BigButton>
</Link>
</Grid>
</>;
}
@@ -336,6 +336,7 @@ export default function PlaylistDetailPage({ playlist: initialPlaylist, me: init
}}
hasGrill={false}
card={trackElements.find(([track]) => track.rank === 1)?.[1]}
onPlay={() => playAll(0)}
/>
) : null}
<StyledHeader>