mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
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:
@@ -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
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
@@ -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; }>;
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user