From 48294f4eafe13f094820c1fa456b896567d98686 Mon Sep 17 00:00:00 2001 From: Mani <12898705+Maniload@users.noreply.github.com> Date: Fri, 3 May 2024 00:27:51 +0200 Subject: [PATCH] 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. --- src/components/card/StudioSummaryCard.tsx | 50 +++ src/components/card/VideoSummaryCard.tsx | 1 + .../featured-theme/FeaturedTheme.tsx | 11 +- src/components/listbox/Listbox.tsx | 2 +- src/components/menu/Menu.tsx | 9 +- src/components/search/SearchStudio.tsx | 37 +- src/generated/graphql.ts | 37 +- src/lib/common/animethemes/resolvers.ts | 5 + src/lib/common/animethemes/type-defs.ts | 1 + src/pages/_app.tsx | 15 +- .../anime/[animeSlug]/[videoSlug]/index.tsx | 70 ++-- src/pages/index.tsx | 371 ++++++++++-------- src/pages/playlist/[playlistId]/index.tsx | 1 + 13 files changed, 342 insertions(+), 268 deletions(-) create mode 100644 src/components/card/StudioSummaryCard.tsx diff --git a/src/components/card/StudioSummaryCard.tsx b/src/components/card/StudioSummaryCard.tsx new file mode 100644 index 0000000..56d5031 --- /dev/null +++ b/src/components/card/StudioSummaryCard.tsx @@ -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(); + + function handleLoad(event: SyntheticEvent) { + const image = event.currentTarget; + const color = extractBackgroundColor(image); + if (color) { + setBackgroundColor(color); + } + } + + return ( + + ); +} + +StudioSummaryCard.fragments = { + studio: gql` + ${extractImages.fragments.resourceWithImages} + + fragment StudioSummaryCardStudio on Studio { + slug + name + ...extractImagesResourceWithImages + } + `, +}; \ No newline at end of file diff --git a/src/components/card/VideoSummaryCard.tsx b/src/components/card/VideoSummaryCard.tsx index ed102f1..f8849a9 100644 --- a/src/components/card/VideoSummaryCard.tsx +++ b/src/components/card/VideoSummaryCard.tsx @@ -115,6 +115,7 @@ export const VideoSummaryCardFragmentVideo = gql` ...createVideoSlugVideo entries { ...createVideoSlugEntry + id theme { ...createVideoSlugTheme id diff --git a/src/components/featured-theme/FeaturedTheme.tsx b/src/components/featured-theme/FeaturedTheme.tsx index fabc8d5..0a849fb 100644 --- a/src/components/featured-theme/FeaturedTheme.tsx +++ b/src/components/featured-theme/FeaturedTheme.tsx @@ -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(null); const [ featuredThemePreview ] = useSetting(FeaturedThemePreview); @@ -149,7 +150,7 @@ export function FeaturedTheme({ theme, hasGrill = true, card }: FeaturedThemePro return ( - + {featuredThemePreview !== FeaturedThemePreview.DISABLED && 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 ( - + + ); diff --git a/src/components/listbox/Listbox.tsx b/src/components/listbox/Listbox.tsx index 2565be3..0b690ec 100644 --- a/src/components/listbox/Listbox.tsx +++ b/src/components/listbox/Listbox.tsx @@ -139,7 +139,7 @@ export const Listbox = forwardRef( - + {children} diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 036b595..5a2c930 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -26,7 +26,14 @@ export const MenuContent = forwardRef( function MenuContent({ children, ...props }, forwardedRef) { return ( - + {children} diff --git a/src/components/search/SearchStudio.tsx b/src/components/search/SearchStudio.tsx index 4e2175f..c97f026 100644 --- a/src/components/search/SearchStudio.tsx +++ b/src/components/search/SearchStudio.tsx @@ -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(); - - function handleLoad(event: SyntheticEvent) { - const image = event.currentTarget; - const color = extractBackgroundColor(image); - if (color) { - setBackgroundColor(color); - } - } - - return ( - - ); -} diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index 1bb5315..a50f5fb 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -218,6 +218,7 @@ export type Query = { page: Maybe; pageAll: Array; playlist: Maybe; + playlistAll: Array; search: GlobalSearchResult; searchAnime: AnimeSearchResult; searchArtist: ArtistSearchResult; @@ -284,6 +285,13 @@ export type QueryPlaylistArgs = { }; +export type QueryPlaylistAllArgs = { + limit: InputMaybe; + orderBy: InputMaybe; + orderDesc: InputMaybe; +}; + + 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; }>; diff --git a/src/lib/common/animethemes/resolvers.ts b/src/lib/common/animethemes/resolvers.ts index d3c5522..92c09a8 100644 --- a/src/lib/common/animethemes/resolvers.ts +++ b/src/lib/common/animethemes/resolvers.ts @@ -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: { diff --git a/src/lib/common/animethemes/type-defs.ts b/src/lib/common/animethemes/type-defs.ts index 7414e22..60ecf89 100644 --- a/src/lib/common/animethemes/type-defs.ts +++ b/src/lib/common/animethemes/type-defs.ts @@ -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! } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a7d5be8..ef5de4a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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(() => getBasename(pageProps)); + const currentVideoIdentifier = getVideoIdentifier(pageProps); + const [previousVideoIdentifier, setPreviousVideoIdentifier] = useState(() => 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; } diff --git a/src/pages/anime/[animeSlug]/[videoSlug]/index.tsx b/src/pages/anime/[animeSlug]/[videoSlug]/index.tsx index 9bf1a5a..27289ba 100644 --- a/src/pages/anime/[animeSlug]/[videoSlug]/index.tsx +++ b/src/pages/anime/[animeSlug]/[videoSlug]/index.tsx @@ -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 { themeIndex: number @@ -177,7 +178,7 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l ))} {anime.studios.map((studio) => ( - + ))} {!!theme.song?.performances?.length && ( <> @@ -201,32 +202,6 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l {selectedTab === "related" ? ( - {!!relatedPlaylists.length && ( - <> - Part of these Playlists - {relatedPlaylists.slice(0, showMoreRelatedPlaylists ? undefined : 3).map((playlist) => ( - - ))} - {relatedPlaylists.length > 3 ? ( - - setShowMoreRelatedPlaylists(!showMoreRelatedPlaylists)} - /> - - ) : null} - - )} - {!!usedAlsoAs.length && ( - <> - Also Used As - {usedAlsoAs.map((theme) => theme?.anime ? ( - - ) : null)} - - )} {!!relatedThemes.length && ( <> Related themes @@ -245,6 +220,32 @@ export default function VideoPage({ anime, themeIndex, entryIndex, videoIndex, l ) : null} )} + {!!usedAlsoAs.length && ( + <> + Also Used As + {usedAlsoAs.map((theme) => theme?.anime ? ( + + ) : null)} + + )} + {!!relatedPlaylists.length && ( + <> + Part of these Playlists + {relatedPlaylists.slice(0, showMoreRelatedPlaylists ? undefined : 3).map((playlist) => ( + + ))} + {relatedPlaylists.length > 3 ? ( + + setShowMoreRelatedPlaylists(!showMoreRelatedPlaylists)} + /> + + ) : null} + + )} ) : 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 } } `, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b5abd89..0391e2b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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["entry"]>["theme"] | null; announcementSources: MDXRemoteSerializeResult[] } export default function HomePage({ featuredTheme, announcementSources }: HomePageProps) { + const { me } = useAuth(); const { currentYear, currentSeason } = useCurrentSeason(); const { data: recentlyAdded } = useSWR( @@ -132,6 +116,26 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag { fallbackData: range(10).map(() => null) } ); + const { data: recentlyAddedPlaylists } = useSWR( + ["HomePageRecentlyAddedPlaylists"], + async () => { + const { data } = await fetchDataClient(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 <> Welcome, to AnimeThemes.moe! @@ -147,140 +151,161 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag ) : null} - - - Explore The Database - - - Recently Added - + Explore The Database - - {recentlyAdded?.map((video, index) => ( - - {video ? ( - - ) : null} - - ))} - Most Viewed - {mostViewed?.map((video, index) => ( - - {video ? ( - - ) : null} - - ))} - + + + + + Search + + + + + + Shuffle + + + } /> + + + + Current Season + + + + - - - - - Search - - - - - - - - Shuffle - - - } /> - - - - - - Current Season - - - - - - - - - Events - - - - - - - + + + + + Events + + + + + + + Wiki + + + + + + + Blog + + + + + + {me.user ? ( + + ) : ( - My Profile - - - - + )} + My Profile + + + + - - - - Anime Index - - - - - - Artist Index - - - - - - Year Index - - - - - - Series Index - - - - - - Studio Index - - - - - - Wiki - - - - + + + Recently Added + + {recentlyAdded?.map((video, index) => ( + + {video ? ( + + ) : null} + + ))} + + + + Most Viewed + + {mostViewed?.map((video, index) => ( + + {video ? ( + + ) : null} + + ))} + + + + New Playlists + + {recentlyAddedPlaylists?.map((playlist, index) => ( + + {playlist ? ( + + ) : null} + + ))} + + + - - About The Project - - 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. - - - This page is still actively being worked on. If you are a developer and interested in contributing feel free to contact us on - Discord - . - - - The source code for this page can be found on - GitHub - . For our other open source projects we also have a - GitHub organization - . - - - + About The Project + + 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. + + + This page is still actively being worked on. If you are a developer and interested in contributing feel free to contact us on + Discord + . + + + The source code for this page can be found on + GitHub + . For our other open source projects we also have a + GitHub organization + . + + + Dive Deeper + + + + + Anime Index + + + + + + Artist Index + + + + + + Year Index + + + + + + Series Index + + + + + + Studio Index + + + + ; } diff --git a/src/pages/playlist/[playlistId]/index.tsx b/src/pages/playlist/[playlistId]/index.tsx index 36701f5..f9f4847 100644 --- a/src/pages/playlist/[playlistId]/index.tsx +++ b/src/pages/playlist/[playlistId]/index.tsx @@ -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}