mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 17:44:35 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7827a8acd2 | |||
| 10d4144313 | |||
| a3df88e856 | |||
| 65234d474c |
@@ -30,10 +30,6 @@ This is a list of all available options:
|
||||
; The URL to the AnimeThemes API which will be used on the server.
|
||||
ANIMETHEMES_API_URL=http://localhost
|
||||
|
||||
; If specified, this API key will be used to make requests to the AnimeThemes API.
|
||||
; This is used to by-pass rate limiting.
|
||||
ANIMETHEMES_API_KEY=...
|
||||
|
||||
; Set to any truthy value to activate minimal build mode.
|
||||
; In minimal build mode, only a small subset of pages get prerendered at build time.
|
||||
MINIMAL_BUILD=true
|
||||
|
||||
@@ -42,6 +42,8 @@ const config: CodegenConfig = {
|
||||
Performance: "@/lib/common/animethemes/types#ApiPerformance",
|
||||
Playlist: "@/lib/common/animethemes/types#ApiPlaylist",
|
||||
PlaylistTrack: "@/lib/common/animethemes/types#ApiPlaylistTrack",
|
||||
ExternalProfile: "@/lib/common/animethemes/types#ApiExternalProfile",
|
||||
ExternalProfileEntry: "@/lib/common/animethemes/types#ApiExternalProfileEntry",
|
||||
Season: "@/lib/common/animethemes/types#ApiSeason",
|
||||
Series: "@/lib/common/animethemes/types#ApiSeries",
|
||||
Song: "@/lib/common/animethemes/types#ApiSong",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" width="135.95" height="102.56" viewBox="0 0 135.95 102.56"><path d="M92.99,76.36V6.24c0-4.02-2.22-6.24-6.24-6.24h-13.72c-4.02,0-6.24,2.22-6.24,6.24v33.3c0,.94,9.04,5.29,9.27,6.21,6.89,26.9,1.5,48.43-5.03,49.44,10.67.53,11.85,5.66,3.9,2.15,1.22-14.35,5.96-14.33,19.6-.53.12.12,2.8,5.74,2.96,5.74h32.22c4.02,0,6.24-2.22,6.24-6.24v-13.72c0-4.02-2.22-6.24-6.24-6.24h-36.73Z" style="fill:#02a9ff; fill-rule:evenodd;"/><path d="M36.03,0L0,102.56h28l6.1-17.74h30.49l5.96,17.74h27.86L62.5,0h-26.47ZM40.47,62.09l8.73-28.41,9.56,28.41h-18.29Z" style="fill:#fefefe; fill-rule:evenodd;"/></svg>
|
||||
|
After Width: | Height: | Size: 667 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" width="194.72" height="79.18" viewBox="0 0 194.72 79.18"><g id="b"><path id="c" d="M0,0v68.71h17.76V26.8l15.47,19.77,16.68-19.77v41.91h17.76V0h-17.76l-16.68,21.41L17.76,0H0Z" style="fill:#fff;"/><path id="d" d="M151.86,0v68.71h39.08l3.78-14.66h-25.1V0h-17.76Z" style="fill:#fff;"/><path id="e" d="M119.01,0c-21.64,0-35.07,10.21-39.37,25.39-4.2,14.82.34,34.37,10.29,53.79l14.86-10.47s-7.06-9.22-8.39-23.04h21.98v23.04h19.73V17.03h-19.73v14.97h-18.22c1.72-11.2,8.29-17.31,15.47-17.31h25.82l-5.12-14.69h-17.31Z" style="fill:#fff;"/></g></svg>
|
||||
|
After Width: | Height: | Size: 624 B |
@@ -1,4 +1,4 @@
|
||||
import { Fragment, type MouseEvent } from "react";
|
||||
import { Fragment, type MouseEvent, type ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -60,7 +60,9 @@ const StyledThemeGroupContainer = styled.div`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
type AnimeSummaryCardProps =
|
||||
type AnimeSummaryCardProps = {
|
||||
children?: ReactNode;
|
||||
} & (
|
||||
| {
|
||||
anime: AnimeSummaryCardAnimeFragment;
|
||||
expandable?: false;
|
||||
@@ -68,9 +70,10 @@ type AnimeSummaryCardProps =
|
||||
| {
|
||||
anime: AnimeSummaryCardAnimeFragment & AnimeSummaryCardAnimeExpandableFragment;
|
||||
expandable: true;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export function AnimeSummaryCard({ anime, expandable = false, ...props }: AnimeSummaryCardProps) {
|
||||
export function AnimeSummaryCard({ anime, expandable = false, children, ...props }: AnimeSummaryCardProps) {
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
const { smallCover } = extractImages(anime);
|
||||
const isMobile = useMediaQuery(`(max-width: ${theme.breakpoints.mobileMax})`);
|
||||
@@ -135,6 +138,7 @@ export function AnimeSummaryCard({ anime, expandable = false, ...props }: AnimeS
|
||||
)}
|
||||
</StyledThemeContainerInline>
|
||||
)}
|
||||
{children}
|
||||
</SummaryCard>
|
||||
{expandable ? (
|
||||
<Collapse collapse={!isExpanded}>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { type ReactNode, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Row } from "@/components/box/Flex";
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/dialog/Dialog";
|
||||
import { SearchFilter } from "@/components/search-filter/SearchFilter";
|
||||
import { SearchFilterGroup } from "@/components/search-filter/SearchFilterGroup";
|
||||
import { Switcher, SwitcherOption } from "@/components/switcher/Switcher";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import type { ExternalProfileSite } from "@/generated/graphql";
|
||||
import { CLIENT_API_URL } from "@/utils/config";
|
||||
|
||||
interface ExternalProfileAddProps {
|
||||
trigger: ReactNode;
|
||||
preselectedSite?: ExternalProfileSite;
|
||||
}
|
||||
|
||||
export function ExternalProfileAddDialog({ trigger, preselectedSite }: ExternalProfileAddProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent title="Add External Profile">
|
||||
{/* Only render the form when dialog is open, so it will reset after closing. */}
|
||||
{open ? (
|
||||
<ExternalProfileAddForm preselectedSite={preselectedSite} onCancel={() => setOpen(false)} />
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledForm = styled.form`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
`;
|
||||
const StyledSwitcher = styled(Switcher)`
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ExternalProfileAddFormProps {
|
||||
preselectedSite?: ExternalProfileSite;
|
||||
onCancel(): void;
|
||||
}
|
||||
|
||||
function ExternalProfileAddForm({ preselectedSite, onCancel }: ExternalProfileAddFormProps) {
|
||||
const [site, setSite] = useState<ExternalProfileSite | "">(preselectedSite ?? "");
|
||||
|
||||
const authUrl = `${CLIENT_API_URL}/externaltoken/auth`;
|
||||
|
||||
return (
|
||||
<StyledForm action={authUrl}>
|
||||
<input type="hidden" name="site" value={site} />
|
||||
<SearchFilterGroup>
|
||||
<SearchFilter>
|
||||
<Text variant="h2">External Site</Text>
|
||||
<StyledSwitcher
|
||||
selectedItem={site}
|
||||
onChange={(newSite) => setSite(newSite as ExternalProfileSite | "")}
|
||||
>
|
||||
<SwitcherOption value="AniList">AniList</SwitcherOption>
|
||||
<SwitcherOption value="MyAnimeList">MyAnimeList</SwitcherOption>
|
||||
</StyledSwitcher>
|
||||
</SearchFilter>
|
||||
</SearchFilterGroup>
|
||||
<Text as="p">
|
||||
To add an external profile you need to grant AnimeThemes access to your profile. We will use this access
|
||||
only to get the anime from your list.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
By default your external profile will be private, so no one can see your list except yourself.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Clicking on{" "}
|
||||
<Text as="button" type="submit" color="text-primary" link>
|
||||
Authorize
|
||||
</Text>{" "}
|
||||
will redirect you to {site}, where you can grant us access.
|
||||
</Text>
|
||||
<Row $wrap style={{ "--gap": "8px", "--justify-content": "flex-end" }}>
|
||||
<Button type="button" variant="silent" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary">
|
||||
Authorize
|
||||
</Button>
|
||||
</Row>
|
||||
</StyledForm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { ExternalProfileAddDialog } from "@/components/external-profile/ExternalProfileAddDialog";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import { withHover } from "@/styles/mixins";
|
||||
|
||||
const StyledGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto 1fr;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
& > * {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
`;
|
||||
const StyledButton = styled(Button)`
|
||||
grid-column: 1 / -1;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
gap: inherit;
|
||||
|
||||
background-color: var(--background-color);
|
||||
color: #ffffff;
|
||||
|
||||
transition: background-color 200ms;
|
||||
|
||||
${withHover`
|
||||
background-color: color-mix(in oklab, var(--background-color), #000 10%);
|
||||
`}
|
||||
`;
|
||||
const StyledSubgrid = styled.div`
|
||||
grid-column: 2 / -2;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
`;
|
||||
const StyledLogo = styled.img`
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
height: 1em;
|
||||
`;
|
||||
|
||||
export default function ExternalProfileList() {
|
||||
return (
|
||||
<StyledGrid>
|
||||
<ExternalProfileAddDialog
|
||||
preselectedSite="AniList"
|
||||
trigger={
|
||||
<StyledButton style={{ "--background-color": "#152232" }}>
|
||||
<StyledSubgrid>
|
||||
<StyledLogo src="/img/anilist.svg" alt="AniList" />
|
||||
<Text>Add AniList</Text>
|
||||
</StyledSubgrid>
|
||||
</StyledButton>
|
||||
}
|
||||
/>
|
||||
<ExternalProfileAddDialog
|
||||
preselectedSite="MyAnimeList"
|
||||
trigger={
|
||||
<StyledButton style={{ "--background-color": "#2e51a2" }}>
|
||||
<StyledSubgrid>
|
||||
<StyledLogo src="/img/myanimelist.svg" alt="MyAnimeList" />
|
||||
<Text>Add MyAnimeList</Text>
|
||||
</StyledSubgrid>
|
||||
</StyledButton>
|
||||
}
|
||||
/>
|
||||
</StyledGrid>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
ApiPerformance,
|
||||
ApiPlaylist,
|
||||
ApiPlaylistTrack,
|
||||
ApiExternalProfile,
|
||||
ApiExternalProfileEntry,
|
||||
ApiSeason,
|
||||
ApiSeries,
|
||||
ApiSong,
|
||||
@@ -180,6 +182,36 @@ export type Entry = {
|
||||
videos: Array<Video>;
|
||||
};
|
||||
|
||||
export type ExternalProfile = {
|
||||
__typename?: "ExternalProfile";
|
||||
entries: Array<ExternalProfileEntry>;
|
||||
id: Scalars["Int"]["output"];
|
||||
name: Scalars["String"]["output"];
|
||||
site: ExternalProfileSite;
|
||||
user?: Maybe<UserPublic>;
|
||||
visibility: ExternalProfileVisibility;
|
||||
};
|
||||
|
||||
export type ExternalProfileEntry = {
|
||||
__typename?: "ExternalProfileEntry";
|
||||
anime: Anime;
|
||||
id: Scalars["Int"]["output"];
|
||||
is_favorite: Scalars["Boolean"]["output"];
|
||||
profile: ExternalProfile;
|
||||
score: Scalars["Float"]["output"];
|
||||
watch_status: Scalars["String"]["output"];
|
||||
};
|
||||
|
||||
export enum ExternalProfileSite {
|
||||
AniList = "AniList",
|
||||
MyAnimeList = "MyAnimeList",
|
||||
}
|
||||
|
||||
export enum ExternalProfileVisibility {
|
||||
Private = "Private",
|
||||
Public = "Public",
|
||||
}
|
||||
|
||||
export type FeaturedTheme = {
|
||||
__typename?: "FeaturedTheme";
|
||||
end_at: Scalars["String"]["output"];
|
||||
@@ -282,6 +314,8 @@ export type Query = {
|
||||
bracket?: Maybe<Bracket>;
|
||||
bracketAll: Array<Bracket>;
|
||||
dumpAll: Array<Dump>;
|
||||
externalProfile?: Maybe<ExternalProfile>;
|
||||
externalProfileAll: Array<ExternalProfile>;
|
||||
featuredTheme?: Maybe<FeaturedTheme>;
|
||||
imageAll: Array<Image>;
|
||||
me: UserScopedQuery;
|
||||
@@ -332,6 +366,16 @@ export type QueryBracketArgs = {
|
||||
slug: Scalars["String"]["input"];
|
||||
};
|
||||
|
||||
export type QueryExternalProfileArgs = {
|
||||
id: Scalars["Int"]["input"];
|
||||
};
|
||||
|
||||
export type QueryExternalProfileAllArgs = {
|
||||
limit?: InputMaybe<Scalars["Int"]["input"]>;
|
||||
name?: InputMaybe<Scalars["String"]["input"]>;
|
||||
site?: InputMaybe<ExternalProfileSite>;
|
||||
};
|
||||
|
||||
export type QueryImageAllArgs = {
|
||||
facet?: InputMaybe<Scalars["String"]["input"]>;
|
||||
limit?: InputMaybe<Scalars["Int"]["input"]>;
|
||||
@@ -721,8 +765,13 @@ export type ResolversTypes = {
|
||||
Dump: ResolverTypeWrapper<ApiDump>;
|
||||
EntitySearchResult: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>["EntitySearchResult"]>;
|
||||
Entry: ResolverTypeWrapper<ApiEntry>;
|
||||
ExternalProfile: ResolverTypeWrapper<ApiExternalProfile>;
|
||||
ExternalProfileEntry: ResolverTypeWrapper<ApiExternalProfileEntry>;
|
||||
ExternalProfileSite: ExternalProfileSite;
|
||||
ExternalProfileVisibility: ExternalProfileVisibility;
|
||||
FeaturedTheme: ResolverTypeWrapper<ApiFeaturedTheme>;
|
||||
Filter: Filter;
|
||||
Float: ResolverTypeWrapper<Scalars["Float"]["output"]>;
|
||||
GlobalSearchResult: ResolverTypeWrapper<
|
||||
Omit<GlobalSearchResult, "anime" | "artists" | "playlists" | "series" | "studios" | "themes"> & {
|
||||
anime: Array<ResolversTypes["Anime"]>;
|
||||
@@ -795,8 +844,11 @@ export type ResolversParentTypes = {
|
||||
Dump: ApiDump;
|
||||
EntitySearchResult: ResolversInterfaceTypes<ResolversParentTypes>["EntitySearchResult"];
|
||||
Entry: ApiEntry;
|
||||
ExternalProfile: ApiExternalProfile;
|
||||
ExternalProfileEntry: ApiExternalProfileEntry;
|
||||
FeaturedTheme: ApiFeaturedTheme;
|
||||
Filter: Filter;
|
||||
Float: Scalars["Float"]["output"];
|
||||
GlobalSearchResult: Omit<
|
||||
GlobalSearchResult,
|
||||
"anime" | "artists" | "playlists" | "series" | "studios" | "themes"
|
||||
@@ -1024,6 +1076,32 @@ export type EntryResolvers<
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ExternalProfileResolvers<
|
||||
ContextType = any,
|
||||
ParentType extends ResolversParentTypes["ExternalProfile"] = ResolversParentTypes["ExternalProfile"],
|
||||
> = {
|
||||
entries?: Resolver<Array<ResolversTypes["ExternalProfileEntry"]>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes["Int"], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||
site?: Resolver<ResolversTypes["ExternalProfileSite"], ParentType, ContextType>;
|
||||
user?: Resolver<Maybe<ResolversTypes["UserPublic"]>, ParentType, ContextType>;
|
||||
visibility?: Resolver<ResolversTypes["ExternalProfileVisibility"], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ExternalProfileEntryResolvers<
|
||||
ContextType = any,
|
||||
ParentType extends ResolversParentTypes["ExternalProfileEntry"] = ResolversParentTypes["ExternalProfileEntry"],
|
||||
> = {
|
||||
anime?: Resolver<ResolversTypes["Anime"], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes["Int"], ParentType, ContextType>;
|
||||
is_favorite?: Resolver<ResolversTypes["Boolean"], ParentType, ContextType>;
|
||||
profile?: Resolver<ResolversTypes["ExternalProfile"], ParentType, ContextType>;
|
||||
score?: Resolver<ResolversTypes["Float"], ParentType, ContextType>;
|
||||
watch_status?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type FeaturedThemeResolvers<
|
||||
ContextType = any,
|
||||
ParentType extends ResolversParentTypes["FeaturedTheme"] = ResolversParentTypes["FeaturedTheme"],
|
||||
@@ -1149,6 +1227,18 @@ export type QueryResolvers<
|
||||
>;
|
||||
bracketAll?: Resolver<Array<ResolversTypes["Bracket"]>, ParentType, ContextType>;
|
||||
dumpAll?: Resolver<Array<ResolversTypes["Dump"]>, ParentType, ContextType>;
|
||||
externalProfile?: Resolver<
|
||||
Maybe<ResolversTypes["ExternalProfile"]>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
RequireFields<QueryExternalProfileArgs, "id">
|
||||
>;
|
||||
externalProfileAll?: Resolver<
|
||||
Array<ResolversTypes["ExternalProfile"]>,
|
||||
ParentType,
|
||||
ContextType,
|
||||
Partial<QueryExternalProfileAllArgs>
|
||||
>;
|
||||
featuredTheme?: Resolver<Maybe<ResolversTypes["FeaturedTheme"]>, ParentType, ContextType>;
|
||||
imageAll?: Resolver<Array<ResolversTypes["Image"]>, ParentType, ContextType, Partial<QueryImageAllArgs>>;
|
||||
me?: Resolver<ResolversTypes["UserScopedQuery"], ParentType, ContextType>;
|
||||
@@ -1469,6 +1559,8 @@ export type Resolvers<ContextType = any> = {
|
||||
Dump?: DumpResolvers<ContextType>;
|
||||
EntitySearchResult?: EntitySearchResultResolvers<ContextType>;
|
||||
Entry?: EntryResolvers<ContextType>;
|
||||
ExternalProfile?: ExternalProfileResolvers<ContextType>;
|
||||
ExternalProfileEntry?: ExternalProfileEntryResolvers<ContextType>;
|
||||
FeaturedTheme?: FeaturedThemeResolvers<ContextType>;
|
||||
GlobalSearchResult?: GlobalSearchResultResolvers<ContextType>;
|
||||
Image?: ImageResolvers<ContextType>;
|
||||
|
||||
@@ -132,6 +132,28 @@ export type Entry = {
|
||||
videos: Array<Video>;
|
||||
};
|
||||
|
||||
export type ExternalProfile = {
|
||||
entries: Array<ExternalProfileEntry>;
|
||||
id: Scalars["Int"]["output"];
|
||||
name: Scalars["String"]["output"];
|
||||
site: ExternalProfileSite;
|
||||
user: Maybe<UserPublic>;
|
||||
visibility: ExternalProfileVisibility;
|
||||
};
|
||||
|
||||
export type ExternalProfileEntry = {
|
||||
anime: Anime;
|
||||
id: Scalars["Int"]["output"];
|
||||
is_favorite: Scalars["Boolean"]["output"];
|
||||
profile: ExternalProfile;
|
||||
score: Scalars["Float"]["output"];
|
||||
watch_status: Scalars["String"]["output"];
|
||||
};
|
||||
|
||||
export type ExternalProfileSite = "AniList" | "MyAnimeList";
|
||||
|
||||
export type ExternalProfileVisibility = "Private" | "Public";
|
||||
|
||||
export type FeaturedTheme = {
|
||||
end_at: Scalars["String"]["output"];
|
||||
entry: Maybe<Entry>;
|
||||
@@ -220,6 +242,8 @@ export type Query = {
|
||||
bracket: Maybe<Bracket>;
|
||||
bracketAll: Array<Bracket>;
|
||||
dumpAll: Array<Dump>;
|
||||
externalProfile: Maybe<ExternalProfile>;
|
||||
externalProfileAll: Array<ExternalProfile>;
|
||||
featuredTheme: Maybe<FeaturedTheme>;
|
||||
imageAll: Array<Image>;
|
||||
me: UserScopedQuery;
|
||||
@@ -270,6 +294,16 @@ export type QueryBracketArgs = {
|
||||
slug: Scalars["String"]["input"];
|
||||
};
|
||||
|
||||
export type QueryExternalProfileArgs = {
|
||||
id: Scalars["Int"]["input"];
|
||||
};
|
||||
|
||||
export type QueryExternalProfileAllArgs = {
|
||||
limit: InputMaybe<Scalars["Int"]["input"]>;
|
||||
name: InputMaybe<Scalars["String"]["input"]>;
|
||||
site: InputMaybe<ExternalProfileSite>;
|
||||
};
|
||||
|
||||
export type QueryImageAllArgs = {
|
||||
facet: InputMaybe<Scalars["String"]["input"]>;
|
||||
limit: InputMaybe<Scalars["Int"]["input"]>;
|
||||
@@ -2490,6 +2524,151 @@ export type EventPageQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type EventPageQuery = { bracketAll: Array<{ slug: string; name: string }> };
|
||||
|
||||
export type ExternalProfilePageExternalProfileQueryVariables = Exact<{
|
||||
externalProfileId: Scalars["Int"]["input"];
|
||||
}>;
|
||||
|
||||
export type ExternalProfilePageExternalProfileQuery = {
|
||||
externalProfile: {
|
||||
id: number;
|
||||
name: string;
|
||||
site: ExternalProfileSite;
|
||||
visibility: ExternalProfileVisibility;
|
||||
entries: Array<{
|
||||
watch_status: string;
|
||||
is_favorite: boolean;
|
||||
score: number;
|
||||
anime: {
|
||||
slug: string;
|
||||
name: string;
|
||||
year: number | null;
|
||||
season: string | null;
|
||||
media_format: string | null;
|
||||
themes: Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
sequence: number | null;
|
||||
group: { name: string; slug: string } | null;
|
||||
anime: { slug: string };
|
||||
entries: Array<{
|
||||
version: number | null;
|
||||
episodes: string | null;
|
||||
spoiler: boolean;
|
||||
nsfw: boolean;
|
||||
videos: Array<{
|
||||
tags: string;
|
||||
resolution: number | null;
|
||||
nc: boolean;
|
||||
subbed: boolean;
|
||||
lyrics: boolean;
|
||||
uncen: boolean;
|
||||
source: VideoSource | null;
|
||||
overlap: VideoOverlap;
|
||||
}>;
|
||||
}>;
|
||||
song: { title: string | null } | null;
|
||||
}>;
|
||||
images: Array<{ link: string; facet: string | null }>;
|
||||
};
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ExternalProfilePageExternalProfileFragment = {
|
||||
id: number;
|
||||
name: string;
|
||||
site: ExternalProfileSite;
|
||||
visibility: ExternalProfileVisibility;
|
||||
entries: Array<{
|
||||
watch_status: string;
|
||||
is_favorite: boolean;
|
||||
score: number;
|
||||
anime: {
|
||||
slug: string;
|
||||
name: string;
|
||||
year: number | null;
|
||||
season: string | null;
|
||||
media_format: string | null;
|
||||
themes: Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
sequence: number | null;
|
||||
group: { name: string; slug: string } | null;
|
||||
anime: { slug: string };
|
||||
entries: Array<{
|
||||
version: number | null;
|
||||
episodes: string | null;
|
||||
spoiler: boolean;
|
||||
nsfw: boolean;
|
||||
videos: Array<{
|
||||
tags: string;
|
||||
resolution: number | null;
|
||||
nc: boolean;
|
||||
subbed: boolean;
|
||||
lyrics: boolean;
|
||||
uncen: boolean;
|
||||
source: VideoSource | null;
|
||||
overlap: VideoOverlap;
|
||||
}>;
|
||||
}>;
|
||||
song: { title: string | null } | null;
|
||||
}>;
|
||||
images: Array<{ link: string; facet: string | null }>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ExternalProfilePageQueryVariables = Exact<{
|
||||
name: Scalars["String"]["input"];
|
||||
site: ExternalProfileSite;
|
||||
}>;
|
||||
|
||||
export type ExternalProfilePageQuery = {
|
||||
externalProfileAll: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
site: ExternalProfileSite;
|
||||
visibility: ExternalProfileVisibility;
|
||||
entries: Array<{
|
||||
watch_status: string;
|
||||
is_favorite: boolean;
|
||||
score: number;
|
||||
anime: {
|
||||
slug: string;
|
||||
name: string;
|
||||
year: number | null;
|
||||
season: string | null;
|
||||
media_format: string | null;
|
||||
themes: Array<{
|
||||
id: number;
|
||||
type: string;
|
||||
sequence: number | null;
|
||||
group: { name: string; slug: string } | null;
|
||||
anime: { slug: string };
|
||||
entries: Array<{
|
||||
version: number | null;
|
||||
episodes: string | null;
|
||||
spoiler: boolean;
|
||||
nsfw: boolean;
|
||||
videos: Array<{
|
||||
tags: string;
|
||||
resolution: number | null;
|
||||
nc: boolean;
|
||||
subbed: boolean;
|
||||
lyrics: boolean;
|
||||
uncen: boolean;
|
||||
source: VideoSource | null;
|
||||
overlap: VideoOverlap;
|
||||
}>;
|
||||
}>;
|
||||
song: { title: string | null } | null;
|
||||
}>;
|
||||
images: Array<{ link: string; facet: string | null }>;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type HomePageQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type HomePageQuery = {
|
||||
|
||||
@@ -87,6 +87,13 @@ export const INCLUDES = {
|
||||
previous: "previous",
|
||||
next: "next",
|
||||
},
|
||||
ExternalProfile: {
|
||||
entries: "externalentries",
|
||||
},
|
||||
ExternalProfileEntry: {
|
||||
profile: "externalprofile",
|
||||
anime: "anime",
|
||||
},
|
||||
UserAuth: {
|
||||
permissions: "permissions",
|
||||
roles: "roles",
|
||||
@@ -202,6 +209,12 @@ const ALLOWED_INCLUDES: Record<string, Array<string>> = {
|
||||
"video.audio",
|
||||
"animethemeentry",
|
||||
],
|
||||
ExternalProfile: [
|
||||
"externalentries.anime.animethemes.group",
|
||||
"externalentries.anime.animethemes.animethemeentries.videos",
|
||||
"externalentries.anime.animethemes.song",
|
||||
"externalentries.anime.images",
|
||||
],
|
||||
UserAuth: ["permissions", "roles.permissions"],
|
||||
FeaturedTheme: [
|
||||
"animethemeentry.animetheme.anime.images",
|
||||
|
||||
@@ -15,6 +15,9 @@ import type {
|
||||
ApiAudioShow,
|
||||
ApiDumpIndex,
|
||||
ApiEntryShow,
|
||||
ApiExternalProfileEntryShow,
|
||||
ApiExternalProfileIndex,
|
||||
ApiExternalProfileShow,
|
||||
ApiFeaturedThemeShow,
|
||||
ApiImageIndex,
|
||||
ApiPageIndex,
|
||||
@@ -123,6 +126,15 @@ const resolvers: Resolvers = {
|
||||
`/playlist?sort=${orderDesc ? "-" : ""}${orderBy}&fields[playlist]=id,name,description,visibility,tracks_count${onlyNonEmpty ? "&filter[playlist][tracks_count-gte]=1" : ""}`,
|
||||
extractFromResponse: (response) => response.playlists,
|
||||
}),
|
||||
externalProfile: createApiResolver<ApiExternalProfileShow>()({
|
||||
endpoint: (_, { id }) => `/externalprofile/${id}`,
|
||||
extractFromResponse: (response) => response.externalprofile,
|
||||
}),
|
||||
externalProfileAll: createApiResolverPaginated<ApiExternalProfileIndex>()({
|
||||
endpoint: (_, { name, site }) =>
|
||||
`/externalprofile?filter[externalprofile][name]=${name}&filter[externalprofile][site]=${site}`,
|
||||
extractFromResponse: (response) => response.externalprofiles,
|
||||
}),
|
||||
me: () => ({}),
|
||||
},
|
||||
UserScopedQuery: {
|
||||
@@ -451,6 +463,24 @@ const resolvers: Resolvers = {
|
||||
baseInclude: INCLUDES.PlaylistTrack.video,
|
||||
}),
|
||||
},
|
||||
ExternalProfile: {
|
||||
entries: createApiResolverNotNull<ApiExternalProfileShow<"externalentries">>()({
|
||||
extractFromParent: (externalProfile) => externalProfile.externalentries,
|
||||
endpoint: (externalProfile) => `/externalprofile/${externalProfile.id}`,
|
||||
extractFromResponse: (response) => response.externalprofile.externalentries,
|
||||
type: "ExternalProfile",
|
||||
baseInclude: INCLUDES.ExternalProfile.entries,
|
||||
}),
|
||||
},
|
||||
ExternalProfileEntry: {
|
||||
anime: createApiResolverNotNull<ApiExternalProfileEntryShow<"anime">>()({
|
||||
extractFromParent: (entry) => entry.anime,
|
||||
endpoint: (entry) => `/externalprofile/${entry.externalprofile.id}/externalentry/${entry.id}`,
|
||||
extractFromResponse: (response) => response.externalentry.anime,
|
||||
type: "ExternalProfileEntry",
|
||||
baseInclude: INCLUDES.ExternalProfileEntry.anime,
|
||||
}),
|
||||
},
|
||||
UserAuth: {
|
||||
roles: createApiResolverNotNull<ApiUserShow<"roles">>()({
|
||||
extractFromParent: (user) => user.roles,
|
||||
|
||||
@@ -23,6 +23,8 @@ const typeDefs = gql`
|
||||
dumpAll: [Dump!]!
|
||||
playlist(id: String!): Playlist
|
||||
playlistAll(limit: Int, orderBy: String, orderDesc: Boolean, onlyNonEmpty: Boolean): [Playlist!]!
|
||||
externalProfile(id: Int!): ExternalProfile
|
||||
externalProfileAll(limit: Int, name: String, site: ExternalProfileSite): [ExternalProfile!]!
|
||||
announcementAll: [Announcement!]!
|
||||
me: UserScopedQuery!
|
||||
}
|
||||
@@ -271,6 +273,34 @@ const typeDefs = gql`
|
||||
next: PlaylistTrack
|
||||
}
|
||||
|
||||
type ExternalProfile {
|
||||
id: Int!
|
||||
name: String!
|
||||
site: ExternalProfileSite!
|
||||
visibility: ExternalProfileVisibility!
|
||||
entries: [ExternalProfileEntry!]!
|
||||
user: UserPublic
|
||||
}
|
||||
|
||||
enum ExternalProfileSite {
|
||||
AniList
|
||||
MyAnimeList
|
||||
}
|
||||
|
||||
enum ExternalProfileVisibility {
|
||||
Public
|
||||
Private
|
||||
}
|
||||
|
||||
type ExternalProfileEntry {
|
||||
id: Int!
|
||||
score: Float!
|
||||
is_favorite: Boolean!
|
||||
watch_status: String!
|
||||
profile: ExternalProfile!
|
||||
anime: Anime!
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: Int!
|
||||
name: String!
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// General Types
|
||||
// --------------------------------
|
||||
|
||||
import type { ExternalProfileSite, ExternalProfileVisibility } from "@/generated/graphql-resolvers";
|
||||
|
||||
export interface ApiIndex {
|
||||
links: ApiIndexLinks;
|
||||
meta: ApiIndexMeta;
|
||||
@@ -143,6 +145,23 @@ export interface ApiPlaylistTrack {
|
||||
video?: ApiVideo;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
site: ExternalProfileSite;
|
||||
visibility: ExternalProfileVisibility;
|
||||
externalentries?: Array<ApiExternalProfileEntry>;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfileEntry {
|
||||
id: number;
|
||||
score: number;
|
||||
is_favorite: boolean;
|
||||
watch_status: string;
|
||||
externalprofile: ApiExternalProfile;
|
||||
anime?: ApiAnime;
|
||||
}
|
||||
|
||||
export interface ApiResource {
|
||||
id: number;
|
||||
link: string | null;
|
||||
@@ -324,6 +343,22 @@ export interface ApiPlaylistTrackIndex extends ApiIndex {
|
||||
tracks: Array<ApiPlaylistTrack>;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfileShow<Includes extends keyof ApiExternalProfile = never> {
|
||||
externalprofile: ApiExternalProfile & Required<Pick<ApiExternalProfile, Includes>>;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfileIndex extends ApiIndex {
|
||||
externalprofiles: Array<ApiExternalProfile>;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfileEntryShow<Includes extends keyof ApiExternalProfileEntry = never> {
|
||||
externalentry: ApiExternalProfileEntry & Required<Pick<ApiExternalProfileEntry, Includes>>;
|
||||
}
|
||||
|
||||
export interface ApiExternalProfileEntryIndex extends ApiIndex {
|
||||
externalentries: Array<ApiExternalProfileEntry>;
|
||||
}
|
||||
|
||||
export interface ApiThemeShow<Includes extends keyof ApiTheme = never> {
|
||||
animetheme: ApiTheme & Required<Pick<ApiTheme, Includes>>;
|
||||
}
|
||||
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import type { GetServerSideProps } from "next";
|
||||
|
||||
import { faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
import { FilterToggleButton } from "@/components/button/FilterToggleButton";
|
||||
import { AnimeSummaryCard } from "@/components/card/AnimeSummaryCard";
|
||||
import { SidebarContainer } from "@/components/container/SidebarContainer";
|
||||
import { DescriptionList } from "@/components/description-list/DescriptionList";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { MultiCoverImage } from "@/components/image/MultiCoverImage";
|
||||
import { SearchFilterGroup } from "@/components/search-filter/SearchFilterGroup";
|
||||
import { SearchFilterSortBy } from "@/components/search-filter/SearchFilterSortBy";
|
||||
import { SEO } from "@/components/seo/SEO";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import { Collapse } from "@/components/utils/Collapse";
|
||||
import type {
|
||||
ExternalProfilePageExternalProfileQuery,
|
||||
ExternalProfilePageExternalProfileQueryVariables,
|
||||
ExternalProfilePageQuery,
|
||||
ExternalProfilePageQueryVariables,
|
||||
ExternalProfileSite,
|
||||
} from "@/generated/graphql";
|
||||
import useToggle from "@/hooks/useToggle";
|
||||
import { fetchDataClient } from "@/lib/client";
|
||||
import { fetchData } from "@/lib/server";
|
||||
import theme from "@/theme/index";
|
||||
import {
|
||||
ANIME_A_Z,
|
||||
ANIME_NEW_OLD,
|
||||
ANIME_OLD_NEW,
|
||||
ANIME_Z_A,
|
||||
either,
|
||||
getComparator,
|
||||
sortTransformed,
|
||||
} from "@/utils/comparators";
|
||||
import type { SharedPageProps } from "@/utils/getSharedPageProps";
|
||||
import getSharedPageProps from "@/utils/getSharedPageProps";
|
||||
import type { Comparator } from "@/utils/types";
|
||||
|
||||
const SITE_MAPPINGS: Record<string, ExternalProfileSite> = {
|
||||
al: "AniList",
|
||||
anilist: "AniList",
|
||||
mal: "MyAnimeList",
|
||||
myanimelist: "MyAnimeList",
|
||||
};
|
||||
|
||||
const StyledDesktopOnly = styled.div`
|
||||
gap: 24px;
|
||||
|
||||
@media (max-width: ${theme.breakpoints.mobileMax}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SCORE_ASC = "score-asc";
|
||||
const SCORE_DESC = "score-desc";
|
||||
const FAVORITES_FIRST = "favorites-first";
|
||||
|
||||
const comparators = {
|
||||
[ANIME_A_Z]: sortTransformed(getComparator(ANIME_A_Z), (entry) => entry.anime),
|
||||
[ANIME_Z_A]: sortTransformed(getComparator(ANIME_Z_A), (entry) => entry.anime),
|
||||
[ANIME_OLD_NEW]: sortTransformed(getComparator(ANIME_OLD_NEW), (entry) => entry.anime),
|
||||
[ANIME_NEW_OLD]: sortTransformed(getComparator(ANIME_NEW_OLD), (entry) => entry.anime),
|
||||
[SCORE_ASC]: (a, b) => a.score - b.score,
|
||||
[SCORE_DESC]: (a, b) => b.score - a.score,
|
||||
[FAVORITES_FIRST]: (a, b) => (b.is_favorite ? 1 : 0) - (a.is_favorite ? 1 : 0),
|
||||
} satisfies Record<string, Comparator<ExternalProfilePageQuery["externalProfileAll"][number]["entries"][number]>>;
|
||||
|
||||
type ExternalProfilePageProps = SharedPageProps & {
|
||||
externalProfile: ExternalProfilePageQuery["externalProfileAll"][number];
|
||||
};
|
||||
|
||||
interface ExternalProfilePageParams extends ParsedUrlQuery {
|
||||
name: string;
|
||||
site: ExternalProfileSite;
|
||||
}
|
||||
|
||||
export default function ExternalProfilePage({ externalProfile: initialExternalProfile }: ExternalProfilePageProps) {
|
||||
const { data: externalProfile } = useSWR(
|
||||
["ExternalProfilePageExternalProfile", `/api/externalprofile/${initialExternalProfile.id}`],
|
||||
async () => {
|
||||
const { data } = await fetchDataClient<
|
||||
ExternalProfilePageExternalProfileQuery,
|
||||
ExternalProfilePageExternalProfileQueryVariables
|
||||
>(
|
||||
gql`
|
||||
${ExternalProfilePage.fragments.externalProfile}
|
||||
|
||||
query ExternalProfilePageExternalProfile($externalProfileId: Int!) {
|
||||
externalProfile(id: $externalProfileId) {
|
||||
...ExternalProfilePageExternalProfile
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ externalProfileId: initialExternalProfile.id },
|
||||
);
|
||||
|
||||
if (!data.externalProfile) {
|
||||
location.reload();
|
||||
throw new Error("External profile was removed or user lost auth.");
|
||||
}
|
||||
|
||||
return data.externalProfile;
|
||||
},
|
||||
{ fallbackData: initialExternalProfile },
|
||||
);
|
||||
|
||||
const favorites = useMemo(
|
||||
() => externalProfile.entries.filter((entry) => entry.is_favorite),
|
||||
[externalProfile.entries],
|
||||
);
|
||||
|
||||
const entriesFiltered = useMemo(
|
||||
() => externalProfile.entries.filter((entry) => entry.watch_status === "Completed"),
|
||||
[externalProfile.entries],
|
||||
);
|
||||
|
||||
// For the cover image we want to show the user's favorites.
|
||||
// If there aren't enough favorites we resort to the entries with the highest score.
|
||||
const coverImageAnime = useMemo(
|
||||
() =>
|
||||
entriesFiltered
|
||||
.sort(either(comparators[FAVORITES_FIRST]).or(comparators[SCORE_DESC]).chain())
|
||||
.map((entry) => entry.anime),
|
||||
[entriesFiltered],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO title={externalProfile.name} />
|
||||
<Text variant="h1">{externalProfile.name}</Text>
|
||||
<SidebarContainer>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
<StyledDesktopOnly>
|
||||
<MultiCoverImage resourcesWithImages={coverImageAnime} />
|
||||
</StyledDesktopOnly>
|
||||
<DescriptionList>
|
||||
<DescriptionList.Item title="Links">
|
||||
<Text link>{externalProfile.site}</Text>
|
||||
</DescriptionList.Item>
|
||||
</DescriptionList>
|
||||
</Column>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
{favorites.length > 0 && <ExternalProfileEntryList title="Favorites" entries={favorites} />}
|
||||
<ExternalProfileEntryList title="All Anime" entries={entriesFiltered} />
|
||||
</Column>
|
||||
</SidebarContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExternalProfileEntryListProps {
|
||||
title: string;
|
||||
entries: ExternalProfilePageProps["externalProfile"]["entries"];
|
||||
}
|
||||
|
||||
const ExternalProfileEntryList = memo(function SeriesAnime({ title, entries }: ExternalProfileEntryListProps) {
|
||||
const [showFilter, toggleShowFilter] = useToggle();
|
||||
const [sortBy, setSortBy] = useState<keyof typeof comparators>(SCORE_DESC);
|
||||
|
||||
const entriesSorted = useMemo(
|
||||
() => [...entries].filter((entry) => entry.watch_status === "Completed").sort(comparators[sortBy]),
|
||||
[entries, sortBy],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row style={{ "--justify-content": "space-between", "--align-items": "center" }}>
|
||||
<Text variant="h2">
|
||||
{title}
|
||||
<Text color="text-disabled"> ({entries.length})</Text>
|
||||
</Text>
|
||||
<FilterToggleButton onClick={toggleShowFilter} />
|
||||
</Row>
|
||||
<Collapse collapse={!showFilter}>
|
||||
<SearchFilterGroup>
|
||||
<SearchFilterSortBy value={sortBy} setValue={setSortBy}>
|
||||
<SearchFilterSortBy.Option value={SCORE_DESC}>Score (High ➜ Low)</SearchFilterSortBy.Option>
|
||||
<SearchFilterSortBy.Option value={SCORE_ASC}>Score (Low ➜ High)</SearchFilterSortBy.Option>
|
||||
<SearchFilterSortBy.Option value={ANIME_A_Z}>A ➜ Z</SearchFilterSortBy.Option>
|
||||
<SearchFilterSortBy.Option value={ANIME_Z_A}>Z ➜ A</SearchFilterSortBy.Option>
|
||||
<SearchFilterSortBy.Option value={ANIME_OLD_NEW}>Old ➜ New</SearchFilterSortBy.Option>
|
||||
<SearchFilterSortBy.Option value={ANIME_NEW_OLD}>New ➜ Old</SearchFilterSortBy.Option>
|
||||
</SearchFilterSortBy>
|
||||
</SearchFilterGroup>
|
||||
</Collapse>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
{entriesSorted.map((entry) => (
|
||||
<AnimeSummaryCard key={entry.anime.slug} anime={entry.anime} expandable>
|
||||
{entry.score > 0 && (
|
||||
<Text variant="small" color={"gold"} noWrap title="Score">
|
||||
<Icon icon={faStar} /> {entry.score}
|
||||
</Text>
|
||||
)}
|
||||
</AnimeSummaryCard>
|
||||
))}
|
||||
</Column>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
ExternalProfilePage.fragments = {
|
||||
externalProfile: gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.expandable}
|
||||
|
||||
fragment ExternalProfilePageExternalProfile on ExternalProfile {
|
||||
id
|
||||
name
|
||||
site
|
||||
visibility
|
||||
entries {
|
||||
watch_status
|
||||
is_favorite
|
||||
score
|
||||
anime {
|
||||
...AnimeSummaryCardAnime
|
||||
...AnimeSummaryCardAnimeExpandable
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<ExternalProfilePageProps, ExternalProfilePageParams> = async ({
|
||||
params,
|
||||
req,
|
||||
}) => {
|
||||
if (!params) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const site = SITE_MAPPINGS[params.site.toLowerCase()];
|
||||
|
||||
if (!site) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const { data, apiRequests } = await fetchData<ExternalProfilePageQuery, ExternalProfilePageQueryVariables>(
|
||||
gql`
|
||||
${ExternalProfilePage.fragments.externalProfile}
|
||||
|
||||
query ExternalProfilePage($name: String!, $site: ExternalProfileSite!) {
|
||||
externalProfileAll(name: $name, site: $site) {
|
||||
...ExternalProfilePageExternalProfile
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ name: params.name, site: site },
|
||||
{ req },
|
||||
);
|
||||
|
||||
if (!data.externalProfileAll.length) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
externalProfile: data.externalProfileAll[0],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { PlaylistAddDialog } from "@/components/dialog/PlaylistAddDialog";
|
||||
import { PlaylistRemoveDialog } from "@/components/dialog/PlaylistRemoveDialog";
|
||||
import { RegisterDialog } from "@/components/dialog/RegisterDialog";
|
||||
import { UserInformationDialog } from "@/components/dialog/UserInformationDialog";
|
||||
import ExternalProfileList from "@/components/external-profile/ExternalProfileList";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { ProfileImage } from "@/components/image/ProfileImage";
|
||||
import { Listbox, ListboxOption } from "@/components/listbox/Listbox";
|
||||
@@ -65,6 +66,8 @@ const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
min-height: 38px;
|
||||
`;
|
||||
|
||||
const StyledHeaderTop = styled(StyledHeader)`
|
||||
@@ -240,6 +243,12 @@ export default function ProfilePage({ me: initialMe }: ProfilePageProps) {
|
||||
) : null}
|
||||
<StyledProfileGrid>
|
||||
<Column style={{ "--gap": "48px" }}>
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
<StyledHeader>
|
||||
<Text variant="h2">External Profiles</Text>
|
||||
</StyledHeader>
|
||||
<ExternalProfileList />
|
||||
</Column>
|
||||
{me.playlistAll ? (
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
<StyledHeader>
|
||||
|
||||
Reference in New Issue
Block a user