mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
chore: Bumped most of the dependencies to the latest version (#229)
* Bumped Next.js to 15. * Bumped React to 19. * Bumped GraphQL to 16. * Bumped styled-components to 6. * Bumped Radix components to latest version. * Migrated Framer Motion to new package. * Migrated react-query to new package. * Lots of TypeScript fixes. * Removed FontAwesome Pro icons. * Started fading out SWR. * Dev Server now uses Turbopack.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import "react";
|
||||
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
[key: `--${string}`]: string | number;
|
||||
}
|
||||
}
|
||||
Generated
+5470
-14459
File diff suppressed because it is too large
Load Diff
+52
-43
@@ -3,7 +3,7 @@
|
||||
"version": "3.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"predev": "npm run compile-config",
|
||||
@@ -13,49 +13,52 @@
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc",
|
||||
"graphql-codegen": "graphql-codegen --config codegen.ts",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/pro-solid-svg-icons": "^6.5.2",
|
||||
"@graphql-tools/merge": "^8.2.1",
|
||||
"@graphql-tools/schema": "^8.2.0",
|
||||
"@graphql-tools/utils": "^8.6.5",
|
||||
"@next/bundle-analyzer": "^12.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"ahooks": "^3.7.6",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@graphql-tools/merge": "^9.0.16",
|
||||
"@graphql-tools/schema": "^10.0.15",
|
||||
"@graphql-tools/utils": "^10.7.1",
|
||||
"@next/bundle-analyzer": "^15.1.3",
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-slider": "^1.2.2",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@tanstack/react-query": "^5.62.15",
|
||||
"ahooks": "^3.8.4",
|
||||
"axios": "^1.2.2",
|
||||
"common-tags": "^1.8.0",
|
||||
"framer-motion": "^11.2.4",
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-parse-resolve-info": "^4.12.0",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql-parse-resolve-info": "^4.13.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"hast-util-has-property": "^2.0.1",
|
||||
"hast-util-heading-rank": "^2.1.1",
|
||||
"hast-util-to-string": "^2.0.0",
|
||||
"hast-util-has-property": "^3.0.0",
|
||||
"hast-util-heading-rank": "^3.0.0",
|
||||
"hast-util-to-string": "^3.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"next": "^13.3.0",
|
||||
"next-mdx-remote": "^4.3.0",
|
||||
"motion": "^11.15.0",
|
||||
"next": "^15.1.4",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-query": "^3.29.0",
|
||||
"rehype-pretty-code": "^0.9.4",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rehype-pretty-code": "^0.14.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sass": "^1.43.4",
|
||||
"shiki": "^0.14.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"swr": "^2.2.4",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"use-local-storage-state": "^19.3.1"
|
||||
"shiki": "^1.26.2",
|
||||
"styled-components": "^6.1.14",
|
||||
"swr": "^2.3.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-local-storage-state": "^19.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
@@ -64,19 +67,25 @@
|
||||
"@graphql-codegen/typescript-resolvers": "^4.1.0",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^17.0.41",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"esbuild": "^0.21.3",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-config-next": "^15.1.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^5.4.5"
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"overrides": {
|
||||
"ahooks": {
|
||||
"react": "$react",
|
||||
"react-dom": "$react-dom"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
diff --git a/node_modules/graphql-parse-resolve-info/build-turbo/index.js b/node_modules/graphql-parse-resolve-info/build-turbo/index.js
|
||||
index b10297b..6a56538 100644
|
||||
--- a/node_modules/graphql-parse-resolve-info/build-turbo/index.js
|
||||
+++ b/node_modules/graphql-parse-resolve-info/build-turbo/index.js
|
||||
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getAlias = exports.simplify = exports.parse = exports.simplifyParsedResolveInfoFragmentWithType = exports.parseResolveInfo = exports.getAliasFromResolveInfo = void 0;
|
||||
const assert = require("assert");
|
||||
const graphql_1 = require("graphql");
|
||||
-const values_1 = require("graphql/execution/values");
|
||||
+const values_1 = require("graphql");
|
||||
const debugFactory = require("debug");
|
||||
const debug = debugFactory("graphql-parse-resolve-info");
|
||||
const DEBUG_ENABLED = debug.enabled;
|
||||
diff --git a/node_modules/graphql-parse-resolve-info/node8plus/index.js b/node_modules/graphql-parse-resolve-info/node8plus/index.js
|
||||
index 4c3d55d..0e2feb3 100644
|
||||
--- a/node_modules/graphql-parse-resolve-info/node8plus/index.js
|
||||
+++ b/node_modules/graphql-parse-resolve-info/node8plus/index.js
|
||||
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getAlias = exports.simplify = exports.parse = exports.simplifyParsedResolveInfoFragmentWithType = exports.parseResolveInfo = exports.getAliasFromResolveInfo = void 0;
|
||||
const assert = require("assert");
|
||||
const graphql_1 = require("graphql");
|
||||
-const values_1 = require("graphql/execution/values");
|
||||
+const values_1 = require("graphql");
|
||||
const debugFactory = require("debug");
|
||||
const debug = debugFactory("graphql-parse-resolve-info");
|
||||
const DEBUG_ENABLED = debug.enabled;
|
||||
@@ -2,8 +2,8 @@ import { memo, useCallback, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faDiagramProject } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { m } from "framer-motion";
|
||||
import { faDiagramProject } from "@fortawesome/free-solid-svg-icons";
|
||||
import { m } from "motion/react";
|
||||
|
||||
import { BracketThemeSummaryCard } from "@/components/bracket/BracketThemeSummaryCard";
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -66,11 +66,11 @@ const StyledBracketThemeSummaryCard = styled(BracketThemeSummaryCard)`
|
||||
width: 384px;
|
||||
`;
|
||||
|
||||
interface BracketChartProps extends RequiredNonNullable<BracketPageQuery> {}
|
||||
type BracketChartProps = RequiredNonNullable<BracketPageQuery>;
|
||||
|
||||
export function BracketChart({ bracket }: BracketChartProps) {
|
||||
const [showBracketChart, setShowBracketChart] = useState(false);
|
||||
const bracketRef = useRef<HTMLDivElement>();
|
||||
const bracketRef = useRef<HTMLDivElement>(undefined);
|
||||
|
||||
const onBracketInit = useCallback((bracket: HTMLDivElement) => {
|
||||
bracketRef.current = bracket;
|
||||
@@ -146,7 +146,7 @@ export function BracketChart({ bracket }: BracketChartProps) {
|
||||
</Button>
|
||||
{showBracketChart ? (
|
||||
<StyledBracketContainer ref={onBracketInit}>
|
||||
<StyledBracket drag dragConstraints={bracketRef as RefObject<HTMLDivElement>}>
|
||||
<StyledBracket drag dragConstraints={bracketRef as RefObject<HTMLDivElement | null>}>
|
||||
<StyledCanvas ref={onCanvasInit} />
|
||||
{bracket.rounds
|
||||
.sort((a, b) => a.tier - b.tier)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faAward, faSeedling, faUsers } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faAward, faSeedling, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import { AnimatePresence, m } from "motion/react";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -55,7 +55,7 @@ export function BackToTopButton() {
|
||||
variant="primary"
|
||||
isCircle={true}
|
||||
onClick={scrollUp}
|
||||
onMouseDown={(event: React.PointerEvent) => event.preventDefault()}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
$bottomOffset={currentWatchListItem ? 76 : 0}
|
||||
>
|
||||
<Icon icon={faChevronUp} />
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { ComponentPropsWithoutRef, ForwardedRef, ReactNode } from "react";
|
||||
import { type ComponentPropsWithRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Solid } from "@/components/box/Solid";
|
||||
import { NestableSlot } from "@/components/utils/NestableSlot";
|
||||
import { withHover } from "@/styles/mixins";
|
||||
import theme from "@/theme";
|
||||
|
||||
export const Button = forwardRef(ButtonWithRef);
|
||||
|
||||
interface ButtonProps extends ComponentPropsWithoutRef<typeof BaseButton> {
|
||||
children?: ReactNode;
|
||||
interface ButtonProps extends ComponentPropsWithRef<typeof BaseButton> {
|
||||
asChild?: boolean;
|
||||
variant?: "solid" | "primary" | "warning" | "silent";
|
||||
isCircle?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function ButtonWithRef(
|
||||
{ variant = "solid", isCircle = false, disabled = false, title, ...props }: ButtonProps,
|
||||
ref: ForwardedRef<HTMLButtonElement>,
|
||||
) {
|
||||
export function Button({
|
||||
ref,
|
||||
asChild,
|
||||
children,
|
||||
variant = "solid",
|
||||
isCircle = false,
|
||||
disabled = false,
|
||||
title,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
let Component;
|
||||
if (variant === "solid") {
|
||||
Component = SolidButton;
|
||||
@@ -32,10 +36,22 @@ function ButtonWithRef(
|
||||
throw new Error(`Unknown button variant "${variant}"!`);
|
||||
}
|
||||
|
||||
return <Component $isCircle={isCircle} disabled={disabled} title={title} aria-label={title} ref={ref} {...props} />;
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
as={asChild ? NestableSlot : "button"}
|
||||
$isCircle={isCircle}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
const BaseButton = styled.button<{ $isCircle: boolean }>`
|
||||
const BaseButton = styled.button<{ $isCircle?: boolean }>`
|
||||
--gap: 0;
|
||||
--focus-ring-color: ${theme.colors["text-primary"]};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import { faFilter } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { IconTextButton } from "@/components/button/IconTextButton";
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode, RefObject } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { NestableSlottable } from "@/components/utils/NestableSlot";
|
||||
import theme from "@/theme";
|
||||
|
||||
const StyledButton = styled(Button)<{ $collapseBreakpoint: string }>`
|
||||
@@ -27,16 +27,14 @@ const StyledText = styled.span<{ $collapseBreakpoint: string }>`
|
||||
}
|
||||
`;
|
||||
|
||||
interface IconTextButtonProps extends ComponentPropsWithoutRef<typeof StyledButton> {
|
||||
interface IconTextButtonProps extends Omit<ComponentPropsWithoutRef<typeof StyledButton>, "$collapseBreakpoint"> {
|
||||
ref?: RefObject<HTMLButtonElement>;
|
||||
icon: IconDefinition | ReactNode;
|
||||
children?: ReactNode;
|
||||
collapsible?: true | keyof typeof theme.breakpoints;
|
||||
}
|
||||
|
||||
export const IconTextButton = forwardRef(function IconTextButton(
|
||||
{ icon, children, collapsible, ...props }: IconTextButtonProps,
|
||||
ref,
|
||||
) {
|
||||
export function IconTextButton({ ref, icon, children, collapsible, ...props }: IconTextButtonProps) {
|
||||
let collapseBreakpoint = "0px";
|
||||
|
||||
if (collapsible === true) {
|
||||
@@ -47,13 +45,19 @@ export const IconTextButton = forwardRef(function IconTextButton(
|
||||
|
||||
return (
|
||||
<StyledButton ref={ref} variant="silent" $collapseBreakpoint={collapseBreakpoint} {...props}>
|
||||
{isIconDefinition(icon) ? <Icon icon={icon} color="text-disabled" /> : icon}
|
||||
{children !== null && children !== undefined ? (
|
||||
<StyledText $collapseBreakpoint={collapseBreakpoint}>{children}</StyledText>
|
||||
) : null}
|
||||
<NestableSlottable child={children}>
|
||||
{(child) => (
|
||||
<>
|
||||
{isIconDefinition(icon) ? <Icon icon={icon} color="text-disabled" /> : icon}
|
||||
{child !== null && child !== undefined ? (
|
||||
<StyledText $collapseBreakpoint={collapseBreakpoint}>{child}</StyledText>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</NestableSlottable>
|
||||
</StyledButton>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function isIconDefinition(icon: IconDefinition | ReactNode): icon is IconDefinition {
|
||||
return !!icon && typeof icon === "object" && "icon" in icon;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useContext } from "react";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faCompactDisc, faPlay } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -32,14 +32,16 @@ export function VideoButton({ anime, theme, entry, video, ...props }: VideoButto
|
||||
: false;
|
||||
|
||||
return (
|
||||
<Link href={`/anime/${anime.slug}/${videoSlug}`} passHref legacyBehavior>
|
||||
<Button as="a" {...props}>
|
||||
<Button as="span" variant="primary" isCircle>
|
||||
<Icon icon={isPlaying ? faCompactDisc : faPlay} className={isPlaying ? "fa-spin" : undefined} />
|
||||
<Button asChild {...props}>
|
||||
<Link href={`/anime/${anime.slug}/${videoSlug}`}>
|
||||
<Button asChild variant="primary" isCircle>
|
||||
<span>
|
||||
<Icon icon={isPlaying ? faCompactDisc : faPlay} className={isPlaying ? "fa-spin" : undefined} />
|
||||
</span>
|
||||
</Button>
|
||||
<VideoTags video={video} hideTextOnMobile />
|
||||
</Button>
|
||||
</Link>
|
||||
<VideoTags video={video} />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, type MouseEvent } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import { uniqBy } from "lodash-es";
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import theme from "@/theme";
|
||||
import type { Colors } from "@/theme/colors";
|
||||
|
||||
export const Card = styled(Solid)<{
|
||||
hoverable?: boolean;
|
||||
color?: keyof Colors;
|
||||
$hoverable?: boolean;
|
||||
$color?: keyof Colors;
|
||||
}>`
|
||||
display: block;
|
||||
position: relative;
|
||||
@@ -23,7 +23,7 @@ export const Card = styled(Solid)<{
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.hoverable &&
|
||||
props.$hoverable &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
|
||||
@@ -40,6 +40,6 @@ export const Card = styled(Solid)<{
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background-color: ${(props) => (props.color ? theme.colors[props.color] : theme.colors["text-primary"])};
|
||||
background-color: ${(props) => (props.$color ? theme.colors[props.$color] : theme.colors["text-primary"])};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faExclamation } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Row } from "@/components/box/Flex";
|
||||
import { Card } from "@/components/card/Card";
|
||||
@@ -26,7 +26,7 @@ interface ErrorCardProps {
|
||||
|
||||
export function ErrorCard({ error }: ErrorCardProps) {
|
||||
return (
|
||||
<StyledCard color="text-warning">
|
||||
<StyledCard $color="text-warning">
|
||||
<Row style={{ "--gap": "1rem" }}>
|
||||
<Text color="text-warning">
|
||||
<Icon icon={faExclamation} />
|
||||
|
||||
@@ -31,8 +31,8 @@ export function StudioSummaryCard({ studio }: StudioSummaryCardProps) {
|
||||
to={`/studio/${studio.slug}`}
|
||||
image={extractImages(studio).largeCover}
|
||||
imageProps={{
|
||||
objectFit: "contain",
|
||||
backgroundColor,
|
||||
$objectFit: "contain",
|
||||
$backgroundColor: backgroundColor,
|
||||
onLoad: handleLoad,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -26,24 +26,24 @@ const StyledSummaryCard = styled(Card)`
|
||||
const StyledCover = styled.img.attrs({
|
||||
loading: "lazy",
|
||||
})<{
|
||||
objectFit?: Property.ObjectFit;
|
||||
backgroundColor?: Property.Background;
|
||||
isLoading?: boolean;
|
||||
isPlaceholder?: boolean;
|
||||
$objectFit?: Property.ObjectFit;
|
||||
$backgroundColor?: Property.Background;
|
||||
$isLoading?: boolean;
|
||||
$isPlaceholder?: boolean;
|
||||
}>`
|
||||
width: 48px;
|
||||
height: 64px;
|
||||
object-fit: ${(props) => props.objectFit ?? "cover"};
|
||||
background: ${(props) => props.backgroundColor};
|
||||
object-fit: ${(props) => props.$objectFit ?? "cover"};
|
||||
background: ${(props) => props.$backgroundColor};
|
||||
|
||||
${(props) =>
|
||||
props.isPlaceholder
|
||||
props.$isPlaceholder
|
||||
? css`
|
||||
padding: 0.5rem;
|
||||
object-fit: contain;
|
||||
background-color: white;
|
||||
`
|
||||
: props.isLoading
|
||||
: props.$isLoading
|
||||
? loadingAnimation
|
||||
: null}
|
||||
`;
|
||||
@@ -56,14 +56,14 @@ const StyledBody = styled(Column)`
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
type SummaryCardProps = ComponentPropsWithoutRef<typeof StyledSummaryCard> & {
|
||||
interface SummaryCardProps extends Omit<ComponentPropsWithoutRef<typeof StyledSummaryCard>, "title"> {
|
||||
title: string | ReactNode;
|
||||
description?: string | ReactNode;
|
||||
image?: string;
|
||||
imageProps?: ComponentPropsWithoutRef<typeof StyledCover>;
|
||||
to?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export function SummaryCard({ title, description, image, imageProps, to, children, ...props }: SummaryCardProps) {
|
||||
const [imageNotFound, setImageNotFound] = useState(false);
|
||||
@@ -75,8 +75,8 @@ export function SummaryCard({ title, description, image, imageProps, to, childre
|
||||
<StyledCover
|
||||
alt="Cover"
|
||||
src={(!imageNotFound && image) || withBasePath("/img/logo.svg")}
|
||||
isLoading={imageLoading}
|
||||
isPlaceholder={!image || imageNotFound}
|
||||
$isLoading={imageLoading}
|
||||
$isPlaceholder={!image || imageNotFound}
|
||||
loading="lazy"
|
||||
{...imageProps}
|
||||
onLoad={(event) => {
|
||||
@@ -85,11 +85,11 @@ export function SummaryCard({ title, description, image, imageProps, to, childre
|
||||
imageProps.onLoad(event);
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
onError={(event) => {
|
||||
setImageNotFound(true);
|
||||
setImageLoading(false);
|
||||
if (imageProps?.onError) {
|
||||
imageProps.onError();
|
||||
imageProps.onError(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
@@ -23,24 +23,24 @@ const StyledSummaryCard = styled(Card)`
|
||||
const StyledCover = styled.img.attrs({
|
||||
loading: "lazy",
|
||||
})<{
|
||||
objectFit?: Property.ObjectFit;
|
||||
backgroundColor?: Property.Background;
|
||||
isLoading?: boolean;
|
||||
isPlaceholder?: boolean;
|
||||
$objectFit?: Property.ObjectFit;
|
||||
$backgroundColor?: Property.Background;
|
||||
$isLoading?: boolean;
|
||||
$isPlaceholder?: boolean;
|
||||
}>`
|
||||
width: 48px;
|
||||
height: 64px;
|
||||
object-fit: ${(props) => props.objectFit ?? "cover"};
|
||||
background: ${(props) => props.backgroundColor};
|
||||
object-fit: ${(props) => props.$objectFit ?? "cover"};
|
||||
background: ${(props) => props.$backgroundColor};
|
||||
|
||||
${(props) =>
|
||||
props.isPlaceholder
|
||||
props.$isPlaceholder
|
||||
? css`
|
||||
padding: 0.5rem;
|
||||
object-fit: contain;
|
||||
background-color: white;
|
||||
`
|
||||
: props.isLoading
|
||||
: props.$isLoading
|
||||
? loadingAnimation
|
||||
: null}
|
||||
`;
|
||||
@@ -87,10 +87,10 @@ SummaryCard.Description = function SummaryCardDescription({ children }: SummaryC
|
||||
: children
|
||||
.filter((child) => child)
|
||||
.map((child, index, { length }) => (
|
||||
<>
|
||||
<Fragment key={index}>
|
||||
{child}
|
||||
{index < length - 1 && <span> • </span>}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</Text>
|
||||
);
|
||||
@@ -108,8 +108,8 @@ SummaryCard.Cover = function SummaryCardCover({ src, ...props }: SummaryCardCove
|
||||
<StyledCover
|
||||
alt="Cover"
|
||||
src={(!imageNotFound && src) || withBasePath("/img/logo.svg")}
|
||||
isLoading={imageLoading}
|
||||
isPlaceholder={!src || imageNotFound}
|
||||
$isLoading={imageLoading}
|
||||
$isPlaceholder={!src || imageNotFound}
|
||||
loading="lazy"
|
||||
{...props}
|
||||
onLoad={(event) => {
|
||||
@@ -118,11 +118,11 @@ SummaryCard.Cover = function SummaryCardCover({ src, ...props }: SummaryCardCove
|
||||
props.onLoad(event);
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
onError={(event) => {
|
||||
setImageNotFound(true);
|
||||
setImageLoading(false);
|
||||
if (props?.onError) {
|
||||
props.onError();
|
||||
props.onError(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import type { MouseEvent, PropsWithChildren } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -130,7 +130,7 @@ export function ThemeSummaryCard({
|
||||
</Link>
|
||||
<SummaryCard.Body>
|
||||
<SummaryCard.Title>
|
||||
<SongTitle song={theme.song} as={Link} href={href} onClick={() => onPlay?.()} />
|
||||
<SongTitle song={theme.song} href={href} onClick={() => onPlay?.()} />
|
||||
<Performances song={theme.song} artist={artist} />
|
||||
</SummaryCard.Title>
|
||||
<SummaryCard.Description>
|
||||
@@ -178,20 +178,17 @@ export function ThemeSummaryCard({
|
||||
?.filter((performance) => performance.artist.slug !== artist?.slug)
|
||||
.sort((a, b) => a.artist.name.localeCompare(b.artist.name))
|
||||
.map((performance) => (
|
||||
<Link
|
||||
<TableRow
|
||||
key={performance.artist.slug}
|
||||
as={Link}
|
||||
href={`/artist/${performance.artist.slug}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
>
|
||||
<TableRow as="a">
|
||||
<TableCell>
|
||||
<Text color="text-primary" weight="600">
|
||||
<TableCell>
|
||||
<Text color="text-primary" weight="600">
|
||||
{getDisplayedArtistName(performance)}
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Link>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type ForwardedRef, forwardRef, type ReactNode } from "react";
|
||||
import { type ReactNode, type Ref } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faPlay } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { SummaryCard } from "@/components/card/SummaryCard2";
|
||||
@@ -54,6 +54,7 @@ const StyledCoverOverlay = styled.div`
|
||||
`;
|
||||
|
||||
interface VideoSummaryCardProps {
|
||||
ref?: Ref<HTMLDivElement>;
|
||||
video: VideoSummaryCardVideoFragment;
|
||||
entry: VideoSummaryCardEntryFragment;
|
||||
menu?: ReactNode;
|
||||
@@ -62,10 +63,16 @@ interface VideoSummaryCardProps {
|
||||
isPlaying?: boolean;
|
||||
}
|
||||
|
||||
export const VideoSummaryCard = forwardRef(function VideoSummaryCard(
|
||||
{ video, entry, menu, append, onPlay, isPlaying, ...props }: VideoSummaryCardProps,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) {
|
||||
export function VideoSummaryCard({
|
||||
ref,
|
||||
video,
|
||||
entry,
|
||||
menu,
|
||||
append,
|
||||
onPlay,
|
||||
isPlaying,
|
||||
...props
|
||||
}: VideoSummaryCardProps) {
|
||||
const theme = entry.theme;
|
||||
const anime = theme?.anime;
|
||||
|
||||
@@ -90,7 +97,7 @@ export const VideoSummaryCard = forwardRef(function VideoSummaryCard(
|
||||
</StyledCoverLink>
|
||||
<SummaryCard.Body>
|
||||
<SummaryCard.Title>
|
||||
<SongTitle song={theme.song} as={Link} href={href} onClick={onPlay} />
|
||||
<SongTitle song={theme.song} href={href} onClick={onPlay} />
|
||||
<Performances song={theme.song} />
|
||||
</SummaryCard.Title>
|
||||
<SummaryCard.Description>
|
||||
@@ -108,7 +115,7 @@ export const VideoSummaryCard = forwardRef(function VideoSummaryCard(
|
||||
</SummaryCard>
|
||||
</StyledWrapper>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const VideoSummaryCardFragmentVideo = gql`
|
||||
${createVideoSlug.fragments.video}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import styled, { keyframes } from "styled-components";
|
||||
|
||||
import * as RadixDialog from "@radix-ui/react-dialog";
|
||||
@@ -47,37 +47,31 @@ const StyledDialogCard = styled(Card)`
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
const StyledHeader = styled.div`
|
||||
const StyledHeader = styled(Text).attrs({ variant: "h2", as: RadixDialog.Title })`
|
||||
margin: -24px -24px 24px -24px;
|
||||
padding: 16px 24px;
|
||||
background-color: ${theme.colors["background"]};
|
||||
`;
|
||||
|
||||
interface DialogContentProps extends RadixDialog.DialogContentProps {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(function DialogContent(
|
||||
{ title, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
export function DialogContent({ ref, title, children, ...props }: DialogContentProps) {
|
||||
return (
|
||||
<RadixDialog.Portal>
|
||||
<StyledOverlay>
|
||||
<RadixDialog.Content asChild {...props} ref={ref}>
|
||||
<StyledDialogCard>
|
||||
{title ? (
|
||||
<StyledHeader>
|
||||
<Text variant="h2">{title}</Text>
|
||||
</StyledHeader>
|
||||
) : null}
|
||||
{title ? <StyledHeader>{title}</StyledHeader> : null}
|
||||
{children}
|
||||
</StyledDialogCard>
|
||||
</RadixDialog.Content>
|
||||
</StyledOverlay>
|
||||
</RadixDialog.Portal>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const Dialog = RadixDialog.Root;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import type { FormEvent, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { isAxiosError } from "axios";
|
||||
import { mutate } from "swr";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import type { FormEvent, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faPen } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faPen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { isAxiosError } from "axios";
|
||||
import gql from "graphql-tag";
|
||||
import { mutate } from "swr";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { faMinus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import { mutate } from "swr";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { faArrowDown, faMinus, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowDown, faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { faMinus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import { mutate } from "swr";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faArrowRight, faTrophy } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowRight, faTrophy } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -74,15 +74,15 @@ export function AnimeAwardsNowAvailable({ year }: AnimeAwardsNowAvailableProps)
|
||||
return (
|
||||
<EventButtonContainer>
|
||||
<GradientBorder />
|
||||
<Link href="/event/anime-awards" passHref legacyBehavior>
|
||||
<EventButton forwardedAs="a">
|
||||
<EventButton asChild>
|
||||
<Link href="/event/anime-awards">
|
||||
<EventIcon icon={faTrophy} />
|
||||
<Text>
|
||||
<Text color="text-primary">/r/anime Awards {year}</Text>: Results are now available!
|
||||
</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</EventButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</EventButton>
|
||||
</EventButtonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
|
||||
import { faChevronCircleRight } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { Text } from "@/components/text/Text";
|
||||
|
||||
@@ -54,7 +54,7 @@ function AnimeThemeFilterInternal({ themes }: AnimeThemeFilterProps) {
|
||||
return (
|
||||
<Column style={{ "--gap": "24px" }}>
|
||||
{(groups.length > 1 || hasMultipleTypes) && (
|
||||
<HorizontalScroll fixShadows>
|
||||
<HorizontalScroll $fixShadows>
|
||||
<Row style={{ "--gap": "16px" }}>
|
||||
{groups.length > 1 && (
|
||||
<Listbox
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -48,6 +47,15 @@ const StyledSocialButton = styled(Button).attrs({ variant: "silent", isCircle: t
|
||||
font-size: 1.2rem;
|
||||
`;
|
||||
|
||||
const FooterLink = styled(Text).attrs({ as: Link })``;
|
||||
|
||||
const FooterTextLink = styled(FooterLink).attrs({
|
||||
link: true,
|
||||
block: true,
|
||||
color: "text-muted",
|
||||
noWrap: true,
|
||||
})``;
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<StyledFooter>
|
||||
@@ -93,11 +101,3 @@ export function Footer() {
|
||||
</StyledFooter>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterLink(props: ComponentPropsWithoutRef<typeof Text>) {
|
||||
return <Text as={Link} {...props} />;
|
||||
}
|
||||
|
||||
function FooterTextLink(props: ComponentPropsWithoutRef<typeof FooterLink>) {
|
||||
return <FooterLink link block color="text-muted" noWrap {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faTimes } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -41,12 +41,12 @@ const StyledResetButton = styled(Button).attrs({ variant: "silent", isCircle: tr
|
||||
}
|
||||
`;
|
||||
|
||||
interface InputProps extends ComponentPropsWithoutRef<typeof StyledSearchInput> {
|
||||
interface InputProps extends Omit<ComponentPropsWithoutRef<typeof StyledSearchInput>, "onChange"> {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
resettable?: boolean;
|
||||
icon?: IconDefinition;
|
||||
inputProps?: ComponentPropsWithoutRef<typeof StyledInput>;
|
||||
inputProps?: ComponentPropsWithRef<typeof StyledInput>;
|
||||
}
|
||||
|
||||
export function Input({ value, onChange, resettable = false, icon, inputProps = {}, ...props }: InputProps) {
|
||||
@@ -61,7 +61,7 @@ export function Input({ value, onChange, resettable = false, icon, inputProps =
|
||||
/>
|
||||
{resettable && !!value && (
|
||||
<StyledResetButton>
|
||||
<Icon icon={faTimes} onClick={() => onChange && onChange("")} />
|
||||
<Icon icon={faXmark} onClick={() => onChange && onChange("")} />
|
||||
</StyledResetButton>
|
||||
)}
|
||||
</StyledSearchInput>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import gql from "graphql-tag";
|
||||
import { range } from "lodash-es";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import {
|
||||
VideoSummaryCard,
|
||||
VideoSummaryCardFragmentEntry,
|
||||
VideoSummaryCardFragmentVideo,
|
||||
} from "@/components/card/VideoSummaryCard";
|
||||
import { Skeleton } from "@/components/skeleton/Skeleton";
|
||||
import type { HomePageMostViewedQuery } from "@/generated/graphql";
|
||||
import { fetchDataClient } from "@/lib/client/index";
|
||||
|
||||
export function MostViewedVideos() {
|
||||
const { data: mostViewed } = useQuery<HomePageMostViewedQuery["videoAll"] | Array<null>>({
|
||||
queryKey: ["HomePageTrending"],
|
||||
queryFn: async () => {
|
||||
const { data } = await fetchDataClient<HomePageMostViewedQuery>(gql`
|
||||
${VideoSummaryCardFragmentVideo}
|
||||
${VideoSummaryCardFragmentEntry}
|
||||
|
||||
query HomePageMostViewed {
|
||||
videoAll(orderBy: "views_count", orderDesc: true, limit: 10) {
|
||||
...VideoSummaryCardVideo
|
||||
entries {
|
||||
...VideoSummaryCardEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.videoAll;
|
||||
},
|
||||
placeholderData: range(10).map(() => null),
|
||||
});
|
||||
|
||||
return (
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
{mostViewed?.map((video, index) => (
|
||||
<Skeleton key={index} variant="summary-card" delay={index * 100}>
|
||||
{video ? <VideoSummaryCard video={video} entry={video.entries[0]} /> : null}
|
||||
</Skeleton>
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import gql from "graphql-tag";
|
||||
import { range } from "lodash-es";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import PlaylistSummaryCard from "@/components/card/PlaylistSummaryCard";
|
||||
import { Skeleton } from "@/components/skeleton/Skeleton";
|
||||
import type { HomePageRecentlyAddedPlaylistsQuery } from "@/generated/graphql";
|
||||
import { fetchDataClient } from "@/lib/client/index";
|
||||
|
||||
export function RecentlyAddedPlaylists() {
|
||||
const { data: recentlyAddedPlaylists } = useQuery<HomePageRecentlyAddedPlaylistsQuery["playlistAll"] | null[]>({
|
||||
queryKey: ["HomePageRecentlyAddedPlaylists"],
|
||||
queryFn: async () => {
|
||||
const { data } = await fetchDataClient<HomePageRecentlyAddedPlaylistsQuery>(gql`
|
||||
${PlaylistSummaryCard.fragments.playlist}
|
||||
${PlaylistSummaryCard.fragments.showOwner}
|
||||
|
||||
query HomePageRecentlyAddedPlaylists {
|
||||
playlistAll(orderBy: "created_at", orderDesc: true, limit: 10, onlyNonEmpty: true) {
|
||||
...PlaylistSummaryCardPlaylist
|
||||
...PlaylistSummaryCardShowOwner
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.playlistAll;
|
||||
},
|
||||
placeholderData: range(10).map(() => null),
|
||||
});
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import gql from "graphql-tag";
|
||||
import { range } from "lodash-es";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import {
|
||||
VideoSummaryCard,
|
||||
VideoSummaryCardFragmentEntry,
|
||||
VideoSummaryCardFragmentVideo,
|
||||
} from "@/components/card/VideoSummaryCard";
|
||||
import { Skeleton } from "@/components/skeleton/Skeleton";
|
||||
import type { HomePageRecentlyAddedQuery } from "@/generated/graphql";
|
||||
import { fetchDataClient } from "@/lib/client";
|
||||
|
||||
export function RecentlyAddedVideos() {
|
||||
const { data: recentlyAdded } = useQuery<HomePageRecentlyAddedQuery["videoAll"] | Array<null>>({
|
||||
queryKey: ["HomePageRecentlyAdded"],
|
||||
queryFn: async () => {
|
||||
const { data } = await fetchDataClient<HomePageRecentlyAddedQuery>(gql`
|
||||
${VideoSummaryCardFragmentVideo}
|
||||
${VideoSummaryCardFragmentEntry}
|
||||
|
||||
query HomePageRecentlyAdded {
|
||||
videoAll(orderBy: "id", orderDesc: true, limit: 10) {
|
||||
...VideoSummaryCardVideo
|
||||
entries {
|
||||
...VideoSummaryCardEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.videoAll;
|
||||
},
|
||||
placeholderData: range(10).map(() => null),
|
||||
});
|
||||
|
||||
return (
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
{recentlyAdded?.map((video, index) => (
|
||||
<Skeleton key={index} variant="summary-card" delay={index * 100}>
|
||||
{video ? <VideoSummaryCard video={video} entry={video.entries[0]} /> : null}
|
||||
</Skeleton>
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export function CoverImage({ resourceWithImages, ...props }: CoverImageProps) {
|
||||
const { smallCover, largeCover } = extractImages(resourceWithImages);
|
||||
|
||||
return (
|
||||
<AspectRatio ratio={2 / 3}>
|
||||
<AspectRatio $ratio={2 / 3}>
|
||||
<FullWidthImage
|
||||
key={largeCover}
|
||||
src={largeCover}
|
||||
|
||||
@@ -133,7 +133,7 @@ export function MultiCoverImage({ resourcesWithImages, ...props }: MultiCoverIma
|
||||
.slice(0, 4);
|
||||
|
||||
return (
|
||||
<AspectRatio ratio={2 / 3}>
|
||||
<AspectRatio $ratio={2 / 3}>
|
||||
<StyledCoverContainer>
|
||||
{images.length ? (
|
||||
images.map(({ largeCover, smallCover, resource }) => (
|
||||
|
||||
@@ -25,7 +25,7 @@ export function StudioCoverImage({ studio, ...props }: StudioCoverImageProps) {
|
||||
const [imageNotFound, setImageNotFound] = useState(!largeCover);
|
||||
|
||||
return (
|
||||
<AspectRatio ratio={2 / 3}>
|
||||
<AspectRatio $ratio={2 / 3}>
|
||||
{!imageNotFound ? (
|
||||
<StyledImage
|
||||
key={largeCover}
|
||||
|
||||
@@ -47,11 +47,9 @@ export function AlphabeticalIndex<T extends AlphabeticalIndexItem>({ items, chil
|
||||
<>
|
||||
<StyledLetterList>
|
||||
{itemsByFirstLetter.map(([firstLetter]) => (
|
||||
<Link key={firstLetter} href={`#${firstLetter}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{firstLetter.toUpperCase()}{" "}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={firstLetter} as={Link} href={`#${firstLetter}`} link>
|
||||
{firstLetter.toUpperCase()}{" "}
|
||||
</Text>
|
||||
))}
|
||||
</StyledLetterList>
|
||||
{itemsByFirstLetter.map(([firstLetter, itemsWithFirstLetter]) => (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faCheck, faSort, faTimes } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faCheck, faSort, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import * as RadixSelect from "@radix-ui/react-select";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -82,6 +82,7 @@ const StyledListboxOption = styled(RadixSelect.Item)`
|
||||
|
||||
type ListboxProps = Omit<RadixSelect.SelectProps, "value" | "defaultValue" | "onValueChange"> &
|
||||
(PropsNullable | PropsNotNullable) & {
|
||||
ref?: RefObject<HTMLButtonElement>;
|
||||
resettable?: boolean;
|
||||
highlightNonDefault?: boolean;
|
||||
};
|
||||
@@ -100,10 +101,17 @@ interface PropsNotNullable {
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const Listbox = forwardRef<HTMLButtonElement, ListboxProps>(function Listbox(
|
||||
{ value, onValueChange, defaultValue, nullable, resettable, highlightNonDefault, children, ...props },
|
||||
export function Listbox({
|
||||
ref,
|
||||
) {
|
||||
value,
|
||||
onValueChange,
|
||||
defaultValue,
|
||||
nullable,
|
||||
resettable,
|
||||
highlightNonDefault,
|
||||
children,
|
||||
...props
|
||||
}: ListboxProps) {
|
||||
const radixValue = value === null ? NULL_VALUE : value;
|
||||
const radixOnValueChange = (newValue: string) => {
|
||||
if (nullable) {
|
||||
@@ -130,7 +138,7 @@ export const Listbox = forwardRef<HTMLButtonElement, ListboxProps>(function List
|
||||
onClick={() => radixOnValueChange(radixDefaultValue)}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
<Icon icon={faXmark} />
|
||||
</StyledListboxReset>
|
||||
) : (
|
||||
<Icon icon={faSort} />
|
||||
@@ -149,16 +157,14 @@ export const Listbox = forwardRef<HTMLButtonElement, ListboxProps>(function List
|
||||
</StyledListboxPopover>
|
||||
</RadixSelect.Root>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export interface ListboxOptionProps extends Omit<RadixSelect.SelectItemProps, "value"> {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
export const ListboxOption = forwardRef<HTMLDivElement, ListboxOptionProps>(function ListboxOption(
|
||||
{ value, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
export const ListboxOption = function ListboxOption({ ref, value, children, ...props }: ListboxOptionProps) {
|
||||
const radixValue = value === null ? NULL_VALUE : value;
|
||||
|
||||
return (
|
||||
@@ -169,4 +175,4 @@ export const ListboxOption = forwardRef<HTMLDivElement, ListboxOptionProps>(func
|
||||
</RadixSelect.ItemIndicator>
|
||||
</StyledListboxOption>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { m } from "framer-motion";
|
||||
import { m } from "motion/react";
|
||||
|
||||
import { Text } from "@/components/text/Text";
|
||||
import theme from "@/theme";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import type { DropdownMenuContentProps } from "@radix-ui/react-dropdown-menu";
|
||||
@@ -26,10 +26,11 @@ const StyledMenuContent = styled(RadixMenu.Content)`
|
||||
export const Menu = RadixMenu.Root;
|
||||
export const MenuTrigger = RadixMenu.Trigger;
|
||||
|
||||
export const MenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(function MenuContent(
|
||||
{ children, ...props },
|
||||
forwardedRef,
|
||||
) {
|
||||
interface MenuContentProps extends DropdownMenuContentProps {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function MenuContent({ ref, children, ...props }: MenuContentProps) {
|
||||
return (
|
||||
<RadixMenu.Portal>
|
||||
<StyledMenuContent
|
||||
@@ -38,13 +39,13 @@ export const MenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
||||
collisionPadding={8}
|
||||
collisionBoundary={typeof document !== "undefined" ? document.body : []}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</StyledMenuContent>
|
||||
</RadixMenu.Portal>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const MenuItem = styled(RadixMenu.Item)`
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { faShare } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faShare } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { IconTextButton } from "@/components/button/IconTextButton";
|
||||
import { Menu, MenuContent, MenuItem, MenuTrigger } from "@/components/menu/Menu";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import { faArrowTurnDownRight, faArrowTurnRight, faEllipsisV, faPlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowTurnDown, faArrowTurnUp, faEllipsisVertical, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -38,7 +38,7 @@ export function ThemeMenu({ theme }: ThemeMenuProps) {
|
||||
<Menu modal={false}>
|
||||
<MenuTrigger asChild>
|
||||
<Button variant="silent" isCircle>
|
||||
<Icon icon={faEllipsisV} />
|
||||
<Icon icon={faEllipsisVertical} />
|
||||
</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
@@ -56,11 +56,11 @@ export function ThemeMenu({ theme }: ThemeMenuProps) {
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<MenuItem onSelect={() => addWatchListItem(video, entryFlipped)}>
|
||||
<Icon icon={faArrowTurnDownRight} color="text-disabled" />
|
||||
<Icon icon={faArrowTurnDown} color="text-disabled" />
|
||||
<Text>Add to Watch List</Text>
|
||||
</MenuItem>
|
||||
<MenuItem onSelect={() => addWatchListItemNext(video, entryFlipped)}>
|
||||
<Icon icon={faArrowTurnRight} color="text-disabled" />
|
||||
<Icon icon={faArrowTurnUp} color="text-disabled" />
|
||||
<Text>Play Next</Text>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { faRandom, faSearch, faTv, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faMagnifyingGlass, faShuffle, faTv, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { IconTextButton } from "@/components/button/IconTextButton";
|
||||
import { ShuffleDialog } from "@/components/dialog/ShuffleDialog";
|
||||
@@ -49,64 +49,55 @@ export function Navigation() {
|
||||
<>
|
||||
<StyledNavigation $floating={isFloating}>
|
||||
<StyledNavigationContainer onClick={(event) => event.stopPropagation()}>
|
||||
<Link href="/" passHref legacyBehavior>
|
||||
<StyledLogoContainer>
|
||||
<StyledLogo width="277" height="150" />
|
||||
</StyledLogoContainer>
|
||||
</Link>
|
||||
<StyledLogoContainer as={Link} href="/">
|
||||
<StyledLogo width="277" height="150" />
|
||||
</StyledLogoContainer>
|
||||
<StyledNavigationLinks>
|
||||
<Link href="/search" passHref legacyBehavior>
|
||||
<IconTextButton
|
||||
forwardedAs="a"
|
||||
icon={faSearch}
|
||||
variant="silent"
|
||||
collapsible
|
||||
style={{ "--gap": "8px" }}
|
||||
>
|
||||
Search
|
||||
</IconTextButton>
|
||||
</Link>
|
||||
<IconTextButton
|
||||
asChild
|
||||
icon={faMagnifyingGlass}
|
||||
variant="silent"
|
||||
collapsible
|
||||
style={{ "--gap": "8px" }}
|
||||
>
|
||||
<Link href="/search">Search</Link>
|
||||
</IconTextButton>
|
||||
<ShuffleDialog
|
||||
trigger={
|
||||
<IconTextButton variant="silent" icon={faRandom} collapsible style={{ "--gap": "8px" }}>
|
||||
<IconTextButton
|
||||
variant="silent"
|
||||
icon={faShuffle}
|
||||
collapsible
|
||||
style={{ "--gap": "8px" }}
|
||||
>
|
||||
Shuffle
|
||||
</IconTextButton>
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
href={currentYear && currentSeason ? `/year/${currentYear}/${currentSeason}` : "/"}
|
||||
passHref
|
||||
legacyBehavior
|
||||
>
|
||||
<IconTextButton
|
||||
forwardedAs="a"
|
||||
variant="silent"
|
||||
icon={faTv}
|
||||
collapsible
|
||||
style={{ "--gap": "8px" }}
|
||||
>
|
||||
|
||||
<IconTextButton asChild variant="silent" icon={faTv} collapsible style={{ "--gap": "8px" }}>
|
||||
<Link href={currentYear && currentSeason ? `/year/${currentYear}/${currentSeason}` : "/"}>
|
||||
Current Season
|
||||
</IconTextButton>
|
||||
</Link>
|
||||
<Link href="/profile" passHref legacyBehavior>
|
||||
<IconTextButton
|
||||
forwardedAs="a"
|
||||
variant="silent"
|
||||
icon={
|
||||
me.user ? (
|
||||
<StyledProfileImageIcon>
|
||||
<StyledProfileImage user={me.user} />
|
||||
</StyledProfileImageIcon>
|
||||
) : (
|
||||
faUser
|
||||
)
|
||||
}
|
||||
title="My Profile"
|
||||
collapsible
|
||||
>
|
||||
My Profile
|
||||
</IconTextButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</IconTextButton>
|
||||
|
||||
<IconTextButton
|
||||
asChild
|
||||
variant="silent"
|
||||
icon={
|
||||
me.user ? (
|
||||
<StyledProfileImageIcon>
|
||||
<StyledProfileImage user={me.user} />
|
||||
</StyledProfileImageIcon>
|
||||
) : (
|
||||
faUser
|
||||
)
|
||||
}
|
||||
title="My Profile"
|
||||
collapsible
|
||||
>
|
||||
<Link href="/profile">My Profile</Link>
|
||||
</IconTextButton>
|
||||
</StyledNavigationLinks>
|
||||
</StyledNavigationContainer>
|
||||
</StyledNavigation>
|
||||
|
||||
@@ -3,7 +3,7 @@ import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { faSearch } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { debounce } from "lodash-es";
|
||||
|
||||
import { Input } from "@/components/form/Input";
|
||||
@@ -60,7 +60,7 @@ export function SearchNavigation() {
|
||||
updateSearchQuery(router, newInputSearchQuery);
|
||||
};
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const inputRef = useRef<HTMLInputElement>(undefined);
|
||||
|
||||
const onMountInput = useCallback((input: HTMLInputElement) => {
|
||||
// Only focus the input on desktop devices
|
||||
@@ -111,58 +111,42 @@ export function SearchNavigation() {
|
||||
placeholder: "Search",
|
||||
}}
|
||||
resettable
|
||||
icon={faSearch}
|
||||
icon={faMagnifyingGlass}
|
||||
/>
|
||||
<HorizontalScroll fixShadows>
|
||||
<HorizontalScroll $fixShadows>
|
||||
<Switcher selectedItem={(entity as string) || null}>
|
||||
<SwitcherReset as={Link} prefetch={false} href={{ pathname: "/search", query }} />
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/anime", query }}
|
||||
value="anime"
|
||||
>
|
||||
Anime
|
||||
<SwitcherReset asChild>
|
||||
<Link href={{ pathname: "/search", query }} prefetch={false} />
|
||||
</SwitcherReset>
|
||||
<SwitcherOption asChild value="anime">
|
||||
<Link href={{ pathname: "/search/anime", query }} prefetch={false}>
|
||||
Anime
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/theme", query }}
|
||||
value="theme"
|
||||
>
|
||||
Theme
|
||||
<SwitcherOption asChild value="theme">
|
||||
<Link href={{ pathname: "/search/theme", query }} prefetch={false}>
|
||||
Theme
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/artist", query }}
|
||||
value="artist"
|
||||
>
|
||||
Artist
|
||||
<SwitcherOption asChild value="artist">
|
||||
<Link href={{ pathname: "/search/artist", query }} prefetch={false}>
|
||||
Artist
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/series", query }}
|
||||
value="series"
|
||||
>
|
||||
Series
|
||||
<SwitcherOption asChild value="series">
|
||||
<Link href={{ pathname: "/search/series", query }} prefetch={false}>
|
||||
Series
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/studio", query }}
|
||||
value="studio"
|
||||
>
|
||||
Studio
|
||||
<SwitcherOption asChild value="studio">
|
||||
<Link href={{ pathname: "/search/studio", query }} prefetch={false}>
|
||||
Studio
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
<SwitcherOption
|
||||
as={Link}
|
||||
prefetch={false}
|
||||
href={{ pathname: "/search/playlist", query }}
|
||||
value="playlist"
|
||||
>
|
||||
Playlist
|
||||
<SwitcherOption asChild value="playlist">
|
||||
<Link href={{ pathname: "/search/playlist", query }} prefetch={false}>
|
||||
Playlist
|
||||
</Link>
|
||||
</SwitcherOption>
|
||||
</Switcher>
|
||||
</HorizontalScroll>
|
||||
|
||||
@@ -11,16 +11,11 @@ export function SeasonNavigation(props: YearDetailPageProps | SeasonDetailPagePr
|
||||
|
||||
return (
|
||||
<Row style={{ "--justify-content": "center" }}>
|
||||
<HorizontalScroll fixShadows>
|
||||
<HorizontalScroll $fixShadows>
|
||||
<Switcher selectedItem={"season" in props ? props.season.value.toLowerCase() : null}>
|
||||
{year.seasons.map((season) => (
|
||||
<SwitcherOption
|
||||
key={season.value}
|
||||
href={`/year/${year.value}/${season.value.toLowerCase()}`}
|
||||
as={Link}
|
||||
value={season.value.toLowerCase()}
|
||||
>
|
||||
{season.value}
|
||||
<SwitcherOption key={season.value} asChild value={season.value.toLowerCase()}>
|
||||
<Link href={`/year/${year.value}/${season.value.toLowerCase()}`}>{season.value}</Link>
|
||||
</SwitcherOption>
|
||||
))}
|
||||
</Switcher>
|
||||
|
||||
@@ -30,25 +30,21 @@ export function YearNavigation({ year, yearAll }: YearDetailPageProps) {
|
||||
<Row style={{ "--align-items": "center" }}>
|
||||
<StyledYearPrevious>
|
||||
{previousYear && (
|
||||
<Link href={`/year/${previousYear}`} passHref legacyBehavior>
|
||||
<Button as="a" variant="silent">
|
||||
{previousYear}
|
||||
</Button>
|
||||
</Link>
|
||||
<Button asChild variant="silent">
|
||||
<Link href={`/year/${previousYear}`}>{previousYear}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</StyledYearPrevious>
|
||||
<Link href={`/year`} passHref legacyBehavior>
|
||||
<Button as="a" variant="silent">
|
||||
<Button asChild variant="silent">
|
||||
<Link href={`/year`}>
|
||||
<Text variant="h1">{year.value}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</Link>
|
||||
</Button>
|
||||
<StyledYearNext>
|
||||
{nextYear && (
|
||||
<Link href={`/year/${nextYear}`} passHref legacyBehavior>
|
||||
<Button as="a" variant="silent">
|
||||
{nextYear}
|
||||
</Button>
|
||||
</Link>
|
||||
<Button asChild variant="silent">
|
||||
<Link href={`/year/${nextYear}`}>{nextYear}</Link>
|
||||
</Button>
|
||||
)}
|
||||
</StyledYearNext>
|
||||
</Row>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { faChevronDown, faSpinner } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronDown, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
import { Button } from "@/components/button/Button";
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { faChevronDown } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import gql from "graphql-tag";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -66,8 +66,10 @@ export function SearchGlobal({ searchQuery }: SearchGlobalProps) {
|
||||
{ args: { query: searchQuery ?? null } },
|
||||
);
|
||||
|
||||
const { data, error, isLoading, isError } = useQuery(["searchGlobal", searchQuery], fetchSearchResults, {
|
||||
keepPreviousData: true,
|
||||
const { data, error, isLoading, isError } = useQuery({
|
||||
queryKey: ["searchGlobal", searchQuery],
|
||||
queryFn: fetchSearchResults,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
@@ -183,11 +185,11 @@ function GlobalSearchSection<T>({ entity, title, results, renderSummaryCard }: G
|
||||
<Column style={{ "--gap": "16px" }}>{resultsPreview.map(renderSummaryCard)}</Column>
|
||||
{hasMoreResults && (
|
||||
<Row style={{ "--justify-content": "center" }}>
|
||||
<Link href={{ pathname: `/search/${entity}`, query: urlParams }} passHref legacyBehavior>
|
||||
<Button as="a" variant="silent" isCircle title="See all results">
|
||||
<Button asChild variant="silent" isCircle title="See all results">
|
||||
<Link href={{ pathname: `/search/${entity}`, query: urlParams }}>
|
||||
<Icon icon={faChevronDown} />
|
||||
</Button>
|
||||
</Link>
|
||||
</Link>
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { forwardRef } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import * as RadixSlider from "@radix-ui/react-slider";
|
||||
@@ -46,7 +46,11 @@ const StyledThumb = styled(RadixSlider.Thumb)`
|
||||
}
|
||||
`;
|
||||
|
||||
export const Slider = forwardRef<HTMLSpanElement, RadixSlider.SliderProps>(function Slider(props, ref) {
|
||||
interface SliderProps extends RadixSlider.SliderProps {
|
||||
ref?: RefObject<HTMLSpanElement>;
|
||||
}
|
||||
|
||||
export function Slider({ ref, ...props }: SliderProps) {
|
||||
const value = (props.value || props.defaultValue) ?? [];
|
||||
|
||||
return (
|
||||
@@ -59,4 +63,4 @@ export const Slider = forwardRef<HTMLSpanElement, RadixSlider.SliderProps>(funct
|
||||
))}
|
||||
</StyledSlider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { createContext, useContext, useMemo } from "react";
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faTimes } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { LayoutGroup, m } from "framer-motion";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Slot, Slottable } from "@radix-ui/react-slot";
|
||||
import { uniqueId as createUniqueId } from "lodash-es";
|
||||
import { LayoutGroup, m } from "motion/react";
|
||||
|
||||
import { Solid } from "@/components/box/Solid";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { NestableSlot, NestableSlottable } from "@/components/utils/NestableSlot";
|
||||
import { withHover } from "@/styles/mixins";
|
||||
import theme from "@/theme";
|
||||
|
||||
@@ -63,7 +65,7 @@ const StyledButton = styled.button<StyledButtonProps>`
|
||||
|
||||
transition: color 500ms;
|
||||
|
||||
${withHover`
|
||||
${withHover<StyledButtonProps>`
|
||||
color: ${(props) => (props.$isSelected ? theme.colors["text-on-primary"] : theme.colors["text"])};;
|
||||
transition-duration: 250ms;
|
||||
`}
|
||||
@@ -83,7 +85,7 @@ const StyledTop = styled.span`
|
||||
z-index: ${theme.zIndices.switcherText};
|
||||
`;
|
||||
|
||||
type SwitcherProps<T extends string | null> = ComponentPropsWithoutRef<typeof StyledSwitcher> & {
|
||||
type SwitcherProps<T extends string | null> = Omit<ComponentPropsWithoutRef<typeof StyledSwitcher>, "onChange"> & {
|
||||
selectedItem: T;
|
||||
onChange?: (value: T) => void;
|
||||
children: ReactNode;
|
||||
@@ -110,32 +112,46 @@ export function Switcher<T extends string | null>({ selectedItem, onChange, chil
|
||||
}
|
||||
|
||||
interface SwitcherOptionProps extends ComponentPropsWithRef<typeof StyledButton> {
|
||||
children: ReactNode;
|
||||
value: string;
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export function SwitcherOption({ children, value, ...props }: SwitcherOptionProps) {
|
||||
export function SwitcherOption({ value, asChild, children, ...props }: SwitcherOptionProps) {
|
||||
const context = useContext(SwitcherContext);
|
||||
const isSelected = context.selectedItem === value;
|
||||
|
||||
return (
|
||||
<StyledButton type="button" $isSelected={isSelected} onClick={() => context.select?.(value)} {...props}>
|
||||
{isSelected && (
|
||||
<StyledButtonBackground
|
||||
layout
|
||||
layoutId="button-bg"
|
||||
layoutDependency={value}
|
||||
transition={{ duration: 0.25 }}
|
||||
/>
|
||||
)}
|
||||
<StyledTop>{children}</StyledTop>
|
||||
<StyledButton
|
||||
as={asChild ? NestableSlot : "button"}
|
||||
type={asChild ? undefined : "button"}
|
||||
$isSelected={isSelected}
|
||||
onClick={() => context.select?.(value)}
|
||||
{...props}
|
||||
>
|
||||
<NestableSlottable child={children}>
|
||||
{(child) => (
|
||||
<>
|
||||
{isSelected && (
|
||||
<StyledButtonBackground
|
||||
layout
|
||||
layoutId="button-bg"
|
||||
layoutDependency={value}
|
||||
transition={{ duration: 0.25 }}
|
||||
/>
|
||||
)}
|
||||
<StyledTop>{child}</StyledTop>
|
||||
</>
|
||||
)}
|
||||
</NestableSlottable>
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface SwitcherResetProps extends ComponentPropsWithRef<typeof StyledButton> {}
|
||||
interface SwitcherResetProps extends ComponentPropsWithRef<typeof StyledButton> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export function SwitcherReset(props: SwitcherResetProps) {
|
||||
export function SwitcherReset({ asChild, children, ...props }: SwitcherResetProps) {
|
||||
const context = useContext(SwitcherContext);
|
||||
|
||||
if (context.selectedItem === null) {
|
||||
@@ -145,12 +161,14 @@ export function SwitcherReset(props: SwitcherResetProps) {
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
as={asChild ? Slot : "button"}
|
||||
style={{ "--color": theme.colors["text-disabled"] }}
|
||||
$isCircle
|
||||
onClick={() => context.select?.(null)}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon={faTimes} />
|
||||
<Icon icon={faXmark} />
|
||||
<Slottable>{children}</Slottable>
|
||||
</StyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,45 +29,43 @@ export function ThemeTable({ themes, onPlay }: ThemeTableProps) {
|
||||
const anime = theme.anime as NonNullable<(typeof theme)["anime"]>;
|
||||
const videoSlug = createVideoSlug(theme, entry, video);
|
||||
return (
|
||||
<Link
|
||||
<TableRow
|
||||
key={anime.slug + videoSlug}
|
||||
as={Link}
|
||||
href={`/anime/${anime.slug}/${videoSlug}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
onClick={() => onPlay?.(theme.id, entryIndex, videoIndex)}
|
||||
>
|
||||
<TableRow as="a" onClick={() => onPlay?.(theme.id, entryIndex, videoIndex)}>
|
||||
<TableCell style={{ "--span": entryIndex || videoIndex ? 2 : undefined }}>
|
||||
{!videoIndex &&
|
||||
((entry.version ?? 1) > 1 ? (
|
||||
<Text variant="small" color="text-muted">
|
||||
{theme.type}
|
||||
{theme.sequence || null} v{entry.version}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="small">
|
||||
{theme.type}
|
||||
{theme.sequence || null}
|
||||
</Text>
|
||||
))}
|
||||
<TableCell style={{ "--span": entryIndex || videoIndex ? 2 : undefined }}>
|
||||
{!videoIndex &&
|
||||
((entry.version ?? 1) > 1 ? (
|
||||
<Text variant="small" color="text-muted">
|
||||
{theme.type}
|
||||
{theme.sequence || null} v{entry.version}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="small">
|
||||
{theme.type}
|
||||
{theme.sequence || null}
|
||||
</Text>
|
||||
))}
|
||||
</TableCell>
|
||||
{!entryIndex && !videoIndex && (
|
||||
<TableCell>
|
||||
<SongTitle song={theme.song} />
|
||||
</TableCell>
|
||||
{!entryIndex && !videoIndex && (
|
||||
<TableCell>
|
||||
<SongTitle song={theme.song} />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{!videoIndex && <EpisodeTag entry={entry} />}</TableCell>
|
||||
<TableCell>
|
||||
{!videoIndex && (
|
||||
<Row $wrap style={{ "--gap": "8px", "--align-items": "baseline" }}>
|
||||
<ContentWarningTags entry={entry} />
|
||||
</Row>
|
||||
)}
|
||||
<TableCell>{!videoIndex && <EpisodeTag entry={entry} />}</TableCell>
|
||||
<TableCell>
|
||||
{!videoIndex && (
|
||||
<Row $wrap style={{ "--gap": "8px", "--align-items": "baseline" }}>
|
||||
<ContentWarningTags entry={entry} />
|
||||
</Row>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<VideoTags video={video} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<VideoTags video={video} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "@/components/text/Text";
|
||||
import theme from "@/theme";
|
||||
|
||||
const StyledTag = styled.span`
|
||||
display: flex;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
background-color: ${theme.colors["text-disabled"]};
|
||||
color: ${theme.colors["solid-on-card"]};
|
||||
`;
|
||||
|
||||
const StyledText = styled(Text)`
|
||||
font-size: 0.7rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
interface BorderTagProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function BorderTag({ children, ...props }: BorderTagProps) {
|
||||
return (
|
||||
<StyledTag {...props}>
|
||||
<StyledText variant="small">{children}</StyledText>
|
||||
</StyledTag>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faExclamationCircle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -13,12 +13,12 @@ export function ContentWarningTags({ entry }: ContentWarningTagsProps) {
|
||||
return (
|
||||
<>
|
||||
{entry.spoiler && (
|
||||
<Tag icon={<Icon icon={faExclamationCircle} color="text-warning" />} textColor="text-warning-muted">
|
||||
<Tag icon={<Icon icon={faCircleExclamation} color="text-warning" />} textColor="text-warning-muted">
|
||||
SPOILER
|
||||
</Tag>
|
||||
)}
|
||||
{entry.nsfw && (
|
||||
<Tag icon={<Icon icon={faExclamationCircle} color="text-warning" />} textColor="text-warning-muted">
|
||||
<Tag icon={<Icon icon={faCircleExclamation} color="text-warning" />} textColor="text-warning-muted">
|
||||
NSFW
|
||||
</Tag>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faFilm } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Tag } from "@/components/tag/Tag";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HTMLAttributes, ReactComponentElement, ReactElement, ReactNode } from "react";
|
||||
import type { ComponentPropsWithoutRef, HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
@@ -19,11 +19,13 @@ const StyledTag = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledText = styled(Text)`
|
||||
const StyledText = styled(Text)<{
|
||||
$hideTextOnMobile: boolean;
|
||||
}>`
|
||||
letter-spacing: 0.05rem;
|
||||
|
||||
${(props) =>
|
||||
props.hideTextOnMobile &&
|
||||
props.$hideTextOnMobile &&
|
||||
css`
|
||||
@media (max-width: ${theme.breakpoints.mobileMax}) {
|
||||
display: none;
|
||||
@@ -31,8 +33,10 @@ const StyledText = styled(Text)`
|
||||
`}
|
||||
`;
|
||||
|
||||
type IconElement = ReactElement<ComponentPropsWithoutRef<typeof Icon>, typeof Icon>;
|
||||
|
||||
interface TagProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
icon?: ReactComponentElement<typeof Icon> | IconDefinition;
|
||||
icon?: IconElement | IconDefinition;
|
||||
children?: ReactNode;
|
||||
hideTextOnMobile?: boolean;
|
||||
textColor?: keyof Colors;
|
||||
@@ -43,7 +47,7 @@ export function Tag({ icon, children, hideTextOnMobile = false, textColor, ...pr
|
||||
<StyledTag {...props}>
|
||||
{!!icon && (isIcon(icon) ? icon : <Icon icon={icon} color="text-disabled" />)}
|
||||
{!!children && (
|
||||
<StyledText variant="small" hideTextOnMobile={hideTextOnMobile} color={textColor}>
|
||||
<StyledText variant="small" $hideTextOnMobile={hideTextOnMobile} color={textColor}>
|
||||
{children}
|
||||
</StyledText>
|
||||
)}
|
||||
@@ -51,8 +55,6 @@ export function Tag({ icon, children, hideTextOnMobile = false, textColor, ...pr
|
||||
);
|
||||
}
|
||||
|
||||
function isIcon(
|
||||
value: ReactComponentElement<typeof Icon> | IconDefinition,
|
||||
): value is ReactComponentElement<typeof Icon> {
|
||||
function isIcon(value: IconElement | IconDefinition): value is IconElement {
|
||||
return typeof value === "object" && "type" in value && (value as ReactElement).type === Icon;
|
||||
}
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import {
|
||||
faAlignSlash,
|
||||
faBringForward,
|
||||
faCommentLines,
|
||||
faCommentMusic,
|
||||
faCompactDisc,
|
||||
faEyes,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Row } from "@/components/box/Flex";
|
||||
import { BorderTag } from "@/components/tag/BorderTag";
|
||||
import { Tag } from "@/components/tag/Tag";
|
||||
import type { VideoTagsVideoFragment } from "@/generated/graphql";
|
||||
|
||||
const StyledVideoTags = styled(Row)`
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
interface VideoTagsProps {
|
||||
video: VideoTagsVideoFragment;
|
||||
hideTextOnMobile?: boolean;
|
||||
}
|
||||
|
||||
export function VideoTags({ video, hideTextOnMobile = false }: VideoTagsProps) {
|
||||
export function VideoTags({ video }: VideoTagsProps) {
|
||||
return (
|
||||
<StyledVideoTags>
|
||||
<Tag title="Resolution">{video.resolution}p</Tag>
|
||||
|
||||
{video.nc && <Tag icon={faAlignSlash} title="No Credits" />}
|
||||
{video.nc && <BorderTag title="No Credits">NC</BorderTag>}
|
||||
|
||||
{video.subbed && <Tag icon={faCommentLines} title="With Subtitles" />}
|
||||
{video.subbed && <BorderTag title="With Subtitles">Subs</BorderTag>}
|
||||
|
||||
{video.lyrics && <Tag icon={faCommentMusic} title="With Lyrics" />}
|
||||
{video.lyrics && <BorderTag title="With Lyrics">Lyrics</BorderTag>}
|
||||
|
||||
{video.uncen && <Tag icon={faEyes} title="Uncensored" />}
|
||||
{video.uncen && <BorderTag title="Uncensored">Uncen</BorderTag>}
|
||||
|
||||
{!!video.source && (
|
||||
<Tag icon={faCompactDisc} title="Source">
|
||||
{video.source.toUpperCase()}
|
||||
</Tag>
|
||||
)}
|
||||
{!!video.source && <BorderTag title="Source">{video.source.toUpperCase()}</BorderTag>}
|
||||
|
||||
{video.overlap !== "NONE" && (
|
||||
<Tag icon={faBringForward} title="Overlap" hideTextOnMobile={hideTextOnMobile}>
|
||||
{video.overlap.toUpperCase().replace("TRANSITION", "TRANS")}
|
||||
</Tag>
|
||||
<BorderTag title="Overlap">{video.overlap.toUpperCase().replace("TRANSITION", "TRANS")}</BorderTag>
|
||||
)}
|
||||
</StyledVideoTags>
|
||||
);
|
||||
|
||||
+10
-10
@@ -1,12 +1,11 @@
|
||||
import type { ReactElement } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import styled, { css, type ExecutionProps } from "styled-components";
|
||||
|
||||
import type { Property } from "csstype";
|
||||
|
||||
import theme from "@/theme";
|
||||
import type { Colors } from "@/theme/colors";
|
||||
|
||||
interface TextProps {
|
||||
interface TextProps extends ExecutionProps {
|
||||
variant?: "h1" | "h2" | "h3" | "small" | "code";
|
||||
link?: boolean;
|
||||
maxLines?: number;
|
||||
@@ -15,11 +14,15 @@ interface TextProps {
|
||||
italics?: boolean;
|
||||
weight?: Property.FontWeight;
|
||||
color?: keyof Colors;
|
||||
as?: string | ReactElement;
|
||||
wrapAnywhere?: boolean;
|
||||
}
|
||||
|
||||
export const Text = styled.span.attrs(getAttributes)<TextProps>`
|
||||
export const Text = styled.span.attrs<TextProps>(getAttributes).withConfig({
|
||||
shouldForwardProp: (prop) =>
|
||||
!["variant", "link", "maxLines", "noWrap", "block", "italics", "weight", "color", "wrapAnywhere"].includes(
|
||||
prop,
|
||||
),
|
||||
})`
|
||||
// Reset margin for elements like <p>
|
||||
margin: 0;
|
||||
scroll-margin-top: 4rem;
|
||||
@@ -115,12 +118,9 @@ export const Text = styled.span.attrs(getAttributes)<TextProps>`
|
||||
`;
|
||||
|
||||
function getAttributes(props: TextProps) {
|
||||
if (props.as) {
|
||||
return props.as;
|
||||
}
|
||||
|
||||
return {
|
||||
as: getAs(props.variant),
|
||||
...props,
|
||||
as: props.as || getAs(props.variant),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export function AnnouncementToast() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Toast hoverable color="text-warning" onClick={() => closeToast("announcement")}>
|
||||
<Toast $hoverable $color="text-warning" onClick={() => closeToast("announcement")}>
|
||||
<StyledBody>
|
||||
<StyledAnnouncements>
|
||||
{announcements.map((announcement) => (
|
||||
|
||||
@@ -12,15 +12,13 @@ interface PlaylistAddToastProps {
|
||||
|
||||
export function PlaylistAddToast({ theme }: PlaylistAddToastProps) {
|
||||
return (
|
||||
<Link href="/profile/playlist" passHref legacyBehavior>
|
||||
<Toast as="a" hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={theme.song} /> was added to the playlist!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
</Link>
|
||||
<Toast as={Link} href="/profile/playlist" $hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={theme.song} /> was added to the playlist!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,17 +15,15 @@ interface PlaylistTrackAddToastProps {
|
||||
|
||||
export function PlaylistTrackAddToast({ playlist, entry }: PlaylistTrackAddToastProps) {
|
||||
return (
|
||||
<Link href={`/playlist/${playlist.id}`} passHref legacyBehavior>
|
||||
<Toast as="a" hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={entry.theme?.song ?? null} /> was added to{" "}
|
||||
<Text color="text-primary">{playlist.name}</Text>!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
</Link>
|
||||
<Toast as={Link} href={`/playlist/${playlist.id}`} $hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={entry.theme?.song ?? null} /> was added to{" "}
|
||||
<Text color="text-primary">{playlist.name}</Text>!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,17 +18,15 @@ interface PlaylistTrackRemoveToastProps {
|
||||
|
||||
export function PlaylistTrackRemoveToast({ playlist, entry }: PlaylistTrackRemoveToastProps) {
|
||||
return (
|
||||
<Link href={`/playlist/${playlist.id}`} passHref legacyBehavior>
|
||||
<Toast as="a" hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={entry.theme?.song ?? null} /> was removed from{" "}
|
||||
<Text color="text-primary">{playlist.name}</Text>!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
</Link>
|
||||
<Toast as={Link} href={`/playlist/${playlist.id}`} $hoverable>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span>
|
||||
<SongTitle song={entry.theme?.song ?? null} /> was removed from{" "}
|
||||
<Text color="text-primary">{playlist.name}</Text>!
|
||||
</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { AnimatePresence, m } from "motion/react";
|
||||
|
||||
import { Container } from "@/components/container/Container";
|
||||
import { useToasts } from "@/context/toastContext";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const AspectRatio = styled.div<{ ratio: number }>`
|
||||
aspect-ratio: ${(props) => props.ratio};
|
||||
export const AspectRatio = styled.div<{ $ratio: number }>`
|
||||
aspect-ratio: ${(props) => props.$ratio};
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
|
||||
@@ -26,7 +26,7 @@ export function Busy({ isBusy, children, ...props }: BusyProps) {
|
||||
return (
|
||||
<StyledWrapper {...props}>
|
||||
<span style={{ visibility: isBusy ? "hidden" : undefined }}>{children}</span>
|
||||
{isBusy ? <StyledIcon icon={faSpinnerThird} className="fa-spin" /> : null}
|
||||
{isBusy ? <StyledIcon icon={faSpinner} className="fa-spin" /> : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from "react";
|
||||
interface ConditionalWrapperProps {
|
||||
condition: boolean;
|
||||
wrap: (children: JSX.Element) => JSX.Element;
|
||||
|
||||
@@ -38,11 +38,9 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
||||
Discord
|
||||
</Text>
|
||||
<span>. For now, you may </span>
|
||||
<Link href="/" passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
go back to the home page
|
||||
</Text>
|
||||
</Link>
|
||||
<Text as={Link} href="/" link>
|
||||
go back to the home page
|
||||
</Text>
|
||||
<span>.</span>
|
||||
</Text>
|
||||
</Container>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
export const HorizontalScroll = styled.div<{
|
||||
fixShadows?: boolean;
|
||||
$fixShadows?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
@@ -16,7 +16,7 @@ export const HorizontalScroll = styled.div<{
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
!!props.fixShadows &&
|
||||
!!props.$fixShadows &&
|
||||
css`
|
||||
margin-top: -16px;
|
||||
margin-bottom: -16px;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import * as React from "react";
|
||||
import type { JSX } from "react";
|
||||
|
||||
import { composeRefs } from "@radix-ui/react-compose-refs";
|
||||
|
||||
/**
|
||||
* Code is taken from this pull request: https://github.com/radix-ui/primitives/pull/2197
|
||||
*
|
||||
* This can be removed once the pull request got merged.
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Slot
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface SlotProps extends React.HTMLAttributes<HTMLElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Slot = React.forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
|
||||
const { children, ...slotProps } = props;
|
||||
|
||||
if (isSlottable(children)) {
|
||||
const slottable = children;
|
||||
|
||||
return (
|
||||
<SlotClone {...slotProps} ref={forwardedRef}>
|
||||
{React.isValidElement<React.PropsWithChildren<unknown>>(slottable.props.child)
|
||||
? React.cloneElement(
|
||||
slottable.props.child,
|
||||
undefined,
|
||||
slottable.props.children(slottable.props.child.props.children),
|
||||
)
|
||||
: null}
|
||||
</SlotClone>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SlotClone {...slotProps} ref={forwardedRef}>
|
||||
{children}
|
||||
</SlotClone>
|
||||
);
|
||||
});
|
||||
|
||||
Slot.displayName = "Slot";
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* SlotClone
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface SlotCloneProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SlotClone = React.forwardRef<any, SlotCloneProps>((props, forwardedRef) => {
|
||||
const { children, ...slotProps } = props;
|
||||
|
||||
if (React.isValidElement<React.RefAttributes<unknown>>(children)) {
|
||||
return React.cloneElement(children, {
|
||||
...mergeProps(slotProps, children.props),
|
||||
ref: forwardedRef ? composeRefs(forwardedRef, (children as any).ref) : (children as any).ref,
|
||||
});
|
||||
}
|
||||
|
||||
return React.Children.count(children) > 1 ? React.Children.only(null) : null;
|
||||
});
|
||||
|
||||
SlotClone.displayName = "SlotClone";
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Slottable
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
type SlottableProps = {
|
||||
child: React.ReactNode;
|
||||
children: (child: React.ReactNode) => JSX.Element;
|
||||
};
|
||||
|
||||
const Slottable = ({ child, children }: SlottableProps) => {
|
||||
return children(child);
|
||||
};
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------- */
|
||||
|
||||
type AnyProps = Record<string, any>;
|
||||
|
||||
function isSlottable(child: React.ReactNode): child is React.ReactElement<SlottableProps> {
|
||||
return React.isValidElement(child) && child.type === Slottable;
|
||||
}
|
||||
|
||||
function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
|
||||
// all child props should override
|
||||
const overrideProps = { ...childProps };
|
||||
|
||||
for (const propName in childProps) {
|
||||
const slotPropValue = slotProps[propName];
|
||||
const childPropValue = childProps[propName];
|
||||
|
||||
const isHandler = /^on[A-Z]/.test(propName);
|
||||
if (isHandler) {
|
||||
// if the handler exists on both, we compose them
|
||||
if (slotPropValue && childPropValue) {
|
||||
overrideProps[propName] = (...args: unknown[]) => {
|
||||
childPropValue(...args);
|
||||
slotPropValue(...args);
|
||||
};
|
||||
}
|
||||
// but if it exists only on the slot, we use only this one
|
||||
else if (slotPropValue) {
|
||||
overrideProps[propName] = slotPropValue;
|
||||
}
|
||||
}
|
||||
// if it's `style`, we merge them
|
||||
else if (propName === "style") {
|
||||
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
|
||||
} else if (propName === "className") {
|
||||
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
return { ...slotProps, ...overrideProps };
|
||||
}
|
||||
|
||||
export { Slot as NestableSlot, Slottable as NestableSlottable };
|
||||
export type { SlotProps };
|
||||
@@ -53,9 +53,13 @@ export function PageRevalidation({ lastBuildAt, apiRequests, ...props }: PageRev
|
||||
}
|
||||
|
||||
const minutesSinceLastBuild = Math.round((Date.now() - lastBuildAt) / 60000);
|
||||
const lastBuildDescription = minutesSinceLastBuild
|
||||
? `${minutesSinceLastBuild} minute${minutesSinceLastBuild === 1 ? "" : "s"}`
|
||||
: "a few seconds";
|
||||
const lastBuildDescription = (
|
||||
<span suppressHydrationWarning>
|
||||
{minutesSinceLastBuild
|
||||
? `${minutesSinceLastBuild} minute${minutesSinceLastBuild === 1 ? "" : "s"}`
|
||||
: "a few seconds"}
|
||||
</span>
|
||||
);
|
||||
|
||||
const apiRequestsDescription = apiRequests
|
||||
? ` using ${apiRequests} API request${apiRequests === 1 ? "" : "s"}`
|
||||
|
||||
@@ -75,9 +75,9 @@ export function Performances({ song, artist, maxPerformances = 3, expandable = f
|
||||
<Text variant="small" color="text-muted">
|
||||
<span> as </span>
|
||||
<span>
|
||||
<Link href={`/artist/${performedAs.artist.slug}`} passHref legacyBehavior>
|
||||
<StyledArtistLink>{performedAs.as}</StyledArtistLink>
|
||||
</Link>
|
||||
<StyledArtistLink as={Link} href={`/artist/${performedAs.artist.slug}`}>
|
||||
{performedAs.as}
|
||||
</StyledArtistLink>
|
||||
</span>
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -87,11 +87,9 @@ export function Performances({ song, artist, maxPerformances = 3, expandable = f
|
||||
<span>
|
||||
{performedWith.map((performance) => (
|
||||
<StyledArtist key={performance.artist.slug}>
|
||||
<Link href={`/artist/${performance.artist.slug}`} passHref legacyBehavior>
|
||||
<StyledArtistLink>
|
||||
{getDisplayedArtistName(performance)}
|
||||
</StyledArtistLink>
|
||||
</Link>
|
||||
<StyledArtistLink as={Link} href={`/artist/${performance.artist.slug}`}>
|
||||
{getDisplayedArtistName(performance)}
|
||||
</StyledArtistLink>
|
||||
</StyledArtist>
|
||||
))}
|
||||
</span>
|
||||
@@ -107,11 +105,9 @@ export function Performances({ song, artist, maxPerformances = 3, expandable = f
|
||||
<Text>
|
||||
{performancesShown.map((performance) => (
|
||||
<StyledArtist key={performance.artist.slug}>
|
||||
<Link href={`/artist/${performance.artist.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
<Text as={Link} href={`/artist/${performance.artist.slug}`} link>
|
||||
{getDisplayedArtistName(performance)}
|
||||
</Text>
|
||||
</Link>
|
||||
</Text>
|
||||
</StyledArtist>
|
||||
))}
|
||||
{!!performancesHidden.length && (
|
||||
|
||||
@@ -16,11 +16,9 @@ export function SongTitle({ song, href, ...props }: SongTitleProps) {
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} passHref legacyBehavior>
|
||||
<Text as="a" link title={songTitle} italics={!song?.title} wrapAnywhere {...props}>
|
||||
{songTitle}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text as={Link} href={href} link title={songTitle} italics={!song?.title} wrapAnywhere {...props}>
|
||||
{songTitle}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { m } from "framer-motion";
|
||||
import { m } from "motion/react";
|
||||
|
||||
import { Switcher } from "@/components/switcher/Switcher";
|
||||
import theme from "@/theme";
|
||||
|
||||
@@ -30,7 +30,7 @@ interface VideoPlayerContextValue {
|
||||
background: boolean;
|
||||
videoPagePath: string;
|
||||
playerRef: RefObject<HTMLVideoElement | HTMLAudioElement | null>;
|
||||
progressRef: RefObject<HTMLDivElement>;
|
||||
progressRef: RefObject<HTMLDivElement | null>;
|
||||
previousVideoPath: string | null;
|
||||
playPreviousTrack(navigate: boolean): void;
|
||||
nextVideoPath: string | null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useContext } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faBackwardStep, faForwardStep, faPause, faPlay, faPlus, faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faBackwardStep, faForwardStep, faPause, faPlay, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import { Solid } from "@/components/box/Solid";
|
||||
@@ -121,11 +121,9 @@ export function VideoPlayerBar() {
|
||||
{theme.group && ` (${theme.group.name})`}
|
||||
</Text>
|
||||
<Text variant="small"> from </Text>
|
||||
<Link href={`/anime/${anime.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{anime.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text as={Link} href={`/anime/${anime.slug}`} link>
|
||||
{anime.name}
|
||||
</Text>
|
||||
</Text>
|
||||
{!!theme.song?.performances?.length && (
|
||||
<Text variant="small" color="text-muted" maxLines={1}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Fragment, useContext } from "react";
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faCheck, faCog, faCompress, faExpand, faPlus, faShare } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faCheck, faCompress, faExpand, faGear, faPlus, faShare } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Row } from "@/components/box/Flex";
|
||||
import { IconTextButton } from "@/components/button/IconTextButton";
|
||||
@@ -90,7 +90,7 @@ export function VideoPlayerOverlay({ anime, themeIndex, entryIndex, videoIndex }
|
||||
/>
|
||||
<Menu modal={false}>
|
||||
<MenuTrigger asChild>
|
||||
<StyledOverlayButton icon={faCog} isCircle title="Change media type" />
|
||||
<StyledOverlayButton icon={faGear} isCircle title="Change media type" />
|
||||
</MenuTrigger>
|
||||
<MenuContent collisionPadding={16}>
|
||||
<MenuLabel>
|
||||
@@ -153,7 +153,7 @@ export function VideoPlayerOverlay({ anime, themeIndex, entryIndex, videoIndex }
|
||||
: "transparent"
|
||||
}
|
||||
/>
|
||||
<VideoTags video={otherVideo} hideTextOnMobile />
|
||||
<VideoTags video={otherVideo} />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faVolume, faVolumeHigh, faVolumeLow, faVolumeSlash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faVolumeHigh, faVolumeLow, faVolumeXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Row } from "@/components/box/Flex";
|
||||
import { IconTextButton } from "@/components/button/IconTextButton";
|
||||
@@ -30,14 +30,12 @@ export function VolumeControl(props: ComponentPropsWithoutRef<typeof StyledRow>)
|
||||
const [volume, setVolume] = useSetting(GlobalVolume);
|
||||
|
||||
let icon;
|
||||
if (volume > 0.66) {
|
||||
if (volume > 0.5) {
|
||||
icon = faVolumeHigh;
|
||||
} else if (volume > 0.33) {
|
||||
icon = faVolume;
|
||||
} else if (volume > 0) {
|
||||
icon = faVolumeLow;
|
||||
} else {
|
||||
icon = faVolumeSlash;
|
||||
icon = faVolumeXmark;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function VideoScript({ video }: Props) {
|
||||
) : null}
|
||||
</>
|
||||
) : video.script ? (
|
||||
<Text download variant="small" link color="text-disabled" onClick={downloadVideoScript}>
|
||||
<Text variant="small" link color="text-disabled" onClick={downloadVideoScript}>
|
||||
Click to download encoding script.
|
||||
</Text>
|
||||
) : (
|
||||
|
||||
+72
-72
@@ -860,6 +860,78 @@ export type AnimeThemeFilterThemeFragment = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type HomePageMostViewedQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
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<{
|
||||
alias: string | null;
|
||||
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 HomePageRecentlyAddedQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
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<{
|
||||
alias: string | null;
|
||||
as: string | null;
|
||||
artist: { slug: string; name: string };
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
}>;
|
||||
audio: { basename: string };
|
||||
}>;
|
||||
};
|
||||
|
||||
type CoverImageResourceWithImages_Anime_Fragment = { images: Array<{ link: string; facet: string | null }> };
|
||||
|
||||
type CoverImageResourceWithImages_Artist_Fragment = { images: Array<{ link: string; facet: string | null }> };
|
||||
@@ -2374,78 +2446,6 @@ export type EventPageQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
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<{
|
||||
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<{
|
||||
alias: string | null;
|
||||
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<{
|
||||
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<{
|
||||
alias: string | null;
|
||||
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 }>;
|
||||
|
||||
export type HomePageQuery = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useInfiniteQuery } from "react-query";
|
||||
import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { SearchArgs } from "@/generated/graphql";
|
||||
import type { SimpleSearchArgs } from "@/lib/client/search";
|
||||
@@ -12,14 +12,17 @@ export default function useEntitySearch<T>(
|
||||
}>,
|
||||
searchArgs: SimpleSearchArgs,
|
||||
) {
|
||||
const fetchEntityPage = ({ pageParam = 1 }) =>
|
||||
const fetchEntityPage = ({ pageParam }: { pageParam: number }) =>
|
||||
fetchResults({
|
||||
...toSearchArgs(searchArgs),
|
||||
page: pageParam,
|
||||
});
|
||||
|
||||
return useInfiniteQuery(["searchEntity", entity, searchArgs], fetchEntityPage, {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["searchEntity", entity, searchArgs],
|
||||
queryFn: fetchEntityPage,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
keepPreviousData: true,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
export default function useMouseRelax() {
|
||||
const [isRelaxed, setRelaxed] = useState(false);
|
||||
|
||||
const timeoutId = useRef<number>();
|
||||
const timeoutId = useRef<number>(undefined);
|
||||
|
||||
// Whenever the user moves or presses their pointer inside the element,
|
||||
// start a timer and cancel any previous timers. If the timer finishes without
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import Link from "next/link";
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
|
||||
import { faArrowLeft } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
|
||||
@@ -59,9 +59,11 @@ interface DocumentPageParams extends ParsedUrlQuery {
|
||||
}
|
||||
|
||||
export default function DocumentPage({ page }: DocumentPageProps) {
|
||||
const author = typeof page.source.frontmatter?.author === "string" ? page.source.frontmatter.author : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={page.name} author={page.source.frontmatter?.author} createdAt={page.created_at} />
|
||||
<PageHeader title={page.name} author={author} createdAt={page.created_at} />
|
||||
<StyledGrid>
|
||||
<SEO title={page.name} />
|
||||
<Markdown source={page.source} />
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { config } from "@fortawesome/fontawesome-svg-core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useFullscreen } from "ahooks";
|
||||
import { LazyMotion } from "framer-motion";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { LazyMotion } from "motion/react";
|
||||
import useLocalStorageState from "use-local-storage-state";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
@@ -257,7 +257,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
)}
|
||||
{pageProps.isSearch && <SearchNavigation />}
|
||||
{STAGING ? (
|
||||
<Card color="text-warning">
|
||||
<Card $color="text-warning">
|
||||
<Text as="p">
|
||||
<strong>WARNING</strong>: This version of the website is for testing purposes
|
||||
only. Some pages or functions might not work.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GetStaticProps } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faDownload } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
@@ -52,15 +52,10 @@ export default function DumpIndexPage({ dumpAll }: DumpIndexPageProps) {
|
||||
title={dump.path}
|
||||
description={`Created at: ${new Date(dump.created_at).toLocaleString()}`}
|
||||
>
|
||||
<IconTextButton
|
||||
variant="solid"
|
||||
forwardedAs="a"
|
||||
href={dump.link}
|
||||
download
|
||||
icon={faDownload}
|
||||
collapsible
|
||||
>
|
||||
Download
|
||||
<IconTextButton asChild variant="solid" icon={faDownload} collapsible>
|
||||
<a href={dump.link} download>
|
||||
Download
|
||||
</a>
|
||||
</IconTextButton>
|
||||
</SummaryCard>
|
||||
))}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
@@ -112,14 +112,31 @@ export default function VideoPage({
|
||||
const videoHeight = video.resolution ?? 720;
|
||||
const videoWidth = Math.floor((videoHeight / 9) * 16);
|
||||
|
||||
const currentVideoCard = useRef<HTMLDivElement>(null);
|
||||
const videoCardMap = useRef<Map<number, HTMLDivElement>>(null);
|
||||
|
||||
function getVideoCardMap() {
|
||||
if (!videoCardMap.current) {
|
||||
videoCardMap.current = new Map();
|
||||
}
|
||||
return videoCardMap.current;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWatchListItem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isCancelled) {
|
||||
currentVideoCard.current?.scrollIntoView({ block: "start", behavior: "smooth" });
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
const currentVideoCard = getVideoCardMap().get(currentWatchListItem.watchListId);
|
||||
if (!currentVideoCard) {
|
||||
return;
|
||||
}
|
||||
currentVideoCard.scrollIntoView({ block: "start", behavior: "smooth" });
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
@@ -148,8 +165,12 @@ export default function VideoPage({
|
||||
<meta name="twitter:player:width" content={String(videoWidth)} />
|
||||
<meta name="twitter:player:height" content={String(videoHeight)} />
|
||||
</SEO>
|
||||
<HorizontalScroll fixShadows>
|
||||
<StyledSwitcher selectedItem={selectedTab} onChange={setSelectedTab}>
|
||||
<HorizontalScroll $fixShadows>
|
||||
<StyledSwitcher
|
||||
selectedItem={selectedTab}
|
||||
// Explicit type cast, because StyledSwitcher breaks the generic type of Switcher
|
||||
onChange={(tab) => setSelectedTab(tab as typeof selectedTab)}
|
||||
>
|
||||
<SwitcherOption value="watch-list">Next Up</SwitcherOption>
|
||||
<SwitcherOption value="info">Info</SwitcherOption>
|
||||
<SwitcherOption value="related">Related</SwitcherOption>
|
||||
@@ -176,11 +197,14 @@ export default function VideoPage({
|
||||
{watchList.map((watchListItem) => (
|
||||
<VideoSummaryCard
|
||||
key={watchListItem.watchListId}
|
||||
ref={
|
||||
watchListItem.watchListId === currentWatchListItem?.watchListId
|
||||
? currentVideoCard
|
||||
: undefined
|
||||
}
|
||||
ref={(element: HTMLDivElement) => {
|
||||
const videoCardMap = getVideoCardMap();
|
||||
videoCardMap.set(watchListItem.watchListId, element);
|
||||
|
||||
return () => {
|
||||
videoCardMap.delete(watchListItem.watchListId);
|
||||
};
|
||||
}}
|
||||
video={watchListItem.video}
|
||||
entry={watchListItem.entry}
|
||||
onPlay={() => setCurrentWatchListItem(watchListItem)}
|
||||
|
||||
@@ -68,25 +68,21 @@ export default function AnimeDetailPage({ anime }: AnimeDetailPageProps) {
|
||||
</DescriptionList.Item>
|
||||
) : null}
|
||||
<DescriptionList.Item title="Premiere">
|
||||
<Link
|
||||
<Text
|
||||
as={Link}
|
||||
href={`/year/${anime.year}${anime.season ? `/${anime.season.toLowerCase()}` : ""}`}
|
||||
passHref
|
||||
legacyBehavior
|
||||
link
|
||||
>
|
||||
<Text as="a" link>
|
||||
{(anime.season ? anime.season + " " : "") + anime.year}
|
||||
</Text>
|
||||
</Link>
|
||||
{(anime.season ? anime.season + " " : "") + anime.year}
|
||||
</Text>
|
||||
</DescriptionList.Item>
|
||||
{anime.series?.length ? (
|
||||
<DescriptionList.Item title="Series">
|
||||
<StyledList>
|
||||
{anime.series.sort(seriesNameComparator).map((series) => (
|
||||
<Link key={series.slug} href={`/series/${series.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{series.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={series.slug} as={Link} href={`/series/${series.slug}`} link>
|
||||
{series.name}
|
||||
</Text>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
@@ -98,11 +94,9 @@ export default function AnimeDetailPage({ anime }: AnimeDetailPageProps) {
|
||||
<DescriptionList.Item title="Studios">
|
||||
<StyledList>
|
||||
{anime.studios.sort(studioNameComparator).map((studio) => (
|
||||
<Link key={studio.slug} href={`/studio/${studio.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{studio.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={studio.slug} as={Link} href={`/studio/${studio.slug}`} link>
|
||||
{studio.name}
|
||||
</Text>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
@@ -127,11 +121,11 @@ export default function AnimeDetailPage({ anime }: AnimeDetailPageProps) {
|
||||
{!!anime.synopsis && (
|
||||
<>
|
||||
<Text variant="h2">Synopsis</Text>
|
||||
<Card hoverable onClick={() => setCollapseSynopsis(!collapseSynopsis)}>
|
||||
<Card $hoverable onClick={() => setCollapseSynopsis(!collapseSynopsis)}>
|
||||
<HeightTransition>
|
||||
<Text
|
||||
as="p"
|
||||
maxLines={collapseSynopsis ? 2 : null}
|
||||
maxLines={collapseSynopsis ? 2 : undefined}
|
||||
dangerouslySetInnerHTML={{ __html: anime.synopsis }}
|
||||
/>
|
||||
</HeightTransition>
|
||||
|
||||
@@ -21,11 +21,9 @@ export default function AnimeIndexPage({ animeAll }: AnimeIndexPageProps) {
|
||||
<Text variant="h1">Anime Index</Text>
|
||||
<AlphabeticalIndex items={animeAll}>
|
||||
{(anime) => (
|
||||
<Link key={anime.slug} href={`/anime/${anime.slug}`} passHref legacyBehavior prefetch={false}>
|
||||
<Text as="a" block link>
|
||||
{anime.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={anime.slug} as={Link} href={`/anime/${anime.slug}`} prefetch={false} block link>
|
||||
{anime.name}
|
||||
</Text>
|
||||
)}
|
||||
</AlphabeticalIndex>
|
||||
</>
|
||||
|
||||
@@ -76,7 +76,7 @@ function getPerformanceFilter(key: string | null): (performance: Performance) =>
|
||||
}
|
||||
}
|
||||
|
||||
interface ArtistDetailPageProps extends RequiredNonNullable<ArtistDetailPageQuery> {}
|
||||
type ArtistDetailPageProps = RequiredNonNullable<ArtistDetailPageQuery>;
|
||||
|
||||
interface ArtistDetailPageParams extends ParsedUrlQuery {
|
||||
artistSlug: string;
|
||||
@@ -204,13 +204,11 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) {
|
||||
<DescriptionList.Item title="Members">
|
||||
<StyledList>
|
||||
{artist.members.map(({ member, alias, as }) => (
|
||||
<Link key={member.slug} href={`/artist/${member.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{member.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={member.slug} as={Link} href={`/artist/${member.slug}`} link>
|
||||
{member.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
@@ -219,13 +217,11 @@ export default function ArtistDetailPage({ artist }: ArtistDetailPageProps) {
|
||||
<DescriptionList.Item title="Member of">
|
||||
<StyledList>
|
||||
{artist.groups.map(({ group, alias, as }) => (
|
||||
<Link key={group.slug} href={`/artist/${group.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{group.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={group.slug} as={Link} href={`/artist/${group.slug}`} link>
|
||||
{group.name}
|
||||
{alias ? ` (as ${alias})` : null}
|
||||
{as ? ` (as ${as})` : null}
|
||||
</Text>
|
||||
))}
|
||||
</StyledList>
|
||||
</DescriptionList.Item>
|
||||
|
||||
@@ -21,11 +21,9 @@ export default function ArtistIndexPage({ artistAll }: ArtistIndexPageProps) {
|
||||
<Text variant="h1">Artist Index</Text>
|
||||
<AlphabeticalIndex items={artistAll}>
|
||||
{(artist) => (
|
||||
<Link key={artist.slug} href={`/artist/${artist.slug}`} passHref legacyBehavior prefetch={false}>
|
||||
<Text as="a" block link>
|
||||
{artist.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={artist.slug} as={Link} href={`/artist/${artist.slug}`} prefetch={false} block link>
|
||||
{artist.name}
|
||||
</Text>
|
||||
)}
|
||||
</AlphabeticalIndex>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
|
||||
import { faStopwatch } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faStopwatch } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import styled from "styled-components";
|
||||
import type { GetStaticProps } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faAward, faHashtag, faUsers } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { m } from "framer-motion";
|
||||
import { faAward, faHashtag, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import { m } from "motion/react";
|
||||
|
||||
import { SummaryCard } from "@/components/card/SummaryCard";
|
||||
import { CornerIcon } from "@/components/icon/CornerIcon";
|
||||
@@ -267,11 +267,9 @@ function AwardThemeSummaryCard({ theme, rank, votes, ...props }: AwardThemeSumma
|
||||
{theme.type + (theme.sequence || "")}
|
||||
{entry.version && entry.version > 1 ? `v${entry.version}` : null}
|
||||
</span>
|
||||
<Link href={`/anime/${theme.anime.slug}`} passHref legacyBehavior>
|
||||
<Text as="a" link>
|
||||
{theme.anime.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text as={Link} href={`/anime/${theme.anime.slug}`} link>
|
||||
{theme.anime.name}
|
||||
</Text>
|
||||
</SummaryCard.Description>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import Link from "next/link";
|
||||
|
||||
import { faArrowRight, faAward, faTrophy } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faArrowRight, faAward, faTrophy } from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
@@ -55,25 +55,25 @@ export default function EventPage({ awardAll, bracketAll }: EventPageProps) {
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Text variant="h2">Awards</Text>
|
||||
{awardAll.map(({ name, slug }) => (
|
||||
<Link key={slug} href={`/event/${slug}`} passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigButton key={slug} asChild>
|
||||
<Link href={`/event/${slug}`}>
|
||||
<BigIcon icon={faAward} />
|
||||
<StyledEventName>{name}</StyledEventName>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
))}
|
||||
</Column>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Text variant="h2">Brackets</Text>
|
||||
{bracketAll.map(({ name, slug }) => (
|
||||
<Link key={slug} href={`/event/${slug}`} passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigButton key={slug} asChild>
|
||||
<Link href={`/event/${slug}`}>
|
||||
<BigIcon icon={faTrophy} />
|
||||
<StyledEventName>{name}</StyledEventName>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
))}
|
||||
</Column>
|
||||
</StyledEventList>
|
||||
|
||||
+60
-158
@@ -6,43 +6,31 @@ import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import {
|
||||
faArrowRight,
|
||||
faAward,
|
||||
faBookOpenCover,
|
||||
faMegaphone,
|
||||
faRandom,
|
||||
faSearch,
|
||||
faBookOpen,
|
||||
faBullhorn,
|
||||
faMagnifyingGlass,
|
||||
faShuffle,
|
||||
faTv,
|
||||
faUser,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import gql from "graphql-tag";
|
||||
import { range } from "lodash-es";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { Column } from "@/components/box/Flex";
|
||||
import { Button } from "@/components/button/Button";
|
||||
import { AnnouncementCard } from "@/components/card/AnnouncementCard";
|
||||
import PlaylistSummaryCard from "@/components/card/PlaylistSummaryCard";
|
||||
import {
|
||||
VideoSummaryCard,
|
||||
VideoSummaryCardFragmentEntry,
|
||||
VideoSummaryCardFragmentVideo,
|
||||
} from "@/components/card/VideoSummaryCard";
|
||||
import { ShuffleDialog } from "@/components/dialog/ShuffleDialog";
|
||||
import { ExternalLink } from "@/components/external-link/ExternalLink";
|
||||
import { FeaturedTheme } from "@/components/featured-theme/FeaturedTheme";
|
||||
import { MostViewedVideos } from "@/components/home/MostViewedVideos";
|
||||
import { RecentlyAddedPlaylists } from "@/components/home/RecentlyAddedPlaylists";
|
||||
import { RecentlyAddedVideos } from "@/components/home/RecentlyAddedVideos";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { ProfileImage } from "@/components/image/ProfileImage";
|
||||
import { SEO } from "@/components/seo/SEO";
|
||||
import { Skeleton } from "@/components/skeleton/Skeleton";
|
||||
import { Text } from "@/components/text/Text";
|
||||
import type {
|
||||
HomePageMostViewedQuery,
|
||||
HomePageQuery,
|
||||
HomePageRecentlyAddedPlaylistsQuery,
|
||||
HomePageRecentlyAddedQuery,
|
||||
} from "@/generated/graphql";
|
||||
import type { HomePageQuery } from "@/generated/graphql";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
import useCurrentSeason from "@/hooks/useCurrentSeason";
|
||||
import { fetchDataClient } from "@/lib/client";
|
||||
import { fetchData } from "@/lib/server";
|
||||
import theme from "@/theme";
|
||||
import getSharedPageProps from "@/utils/getSharedPageProps";
|
||||
@@ -87,70 +75,6 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
|
||||
const { me } = useAuth();
|
||||
const { currentYear, currentSeason } = useCurrentSeason();
|
||||
|
||||
const { data: recentlyAdded } = useSWR<HomePageRecentlyAddedQuery["videoAll"] | null[]>(
|
||||
["HomePageRecentlyAdded"],
|
||||
async () => {
|
||||
const { data } = await fetchDataClient<HomePageRecentlyAddedQuery>(gql`
|
||||
${VideoSummaryCardFragmentVideo}
|
||||
${VideoSummaryCardFragmentEntry}
|
||||
|
||||
query HomePageRecentlyAdded {
|
||||
videoAll(orderBy: "id", orderDesc: true, limit: 10) {
|
||||
...VideoSummaryCardVideo
|
||||
entries {
|
||||
...VideoSummaryCardEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.videoAll;
|
||||
},
|
||||
{ fallbackData: range(10).map(() => null) },
|
||||
);
|
||||
|
||||
const { data: mostViewed } = useSWR<HomePageMostViewedQuery["videoAll"] | null[]>(
|
||||
["HomePageTrending"],
|
||||
async () => {
|
||||
const { data } = await fetchDataClient<HomePageMostViewedQuery>(gql`
|
||||
${VideoSummaryCardFragmentVideo}
|
||||
${VideoSummaryCardFragmentEntry}
|
||||
|
||||
query HomePageMostViewed {
|
||||
videoAll(orderBy: "views_count", orderDesc: true, limit: 10) {
|
||||
...VideoSummaryCardVideo
|
||||
entries {
|
||||
...VideoSummaryCardEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.videoAll;
|
||||
},
|
||||
{ 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, onlyNonEmpty: true) {
|
||||
...PlaylistSummaryCardPlaylist
|
||||
...PlaylistSummaryCardShowOwner
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return data.playlistAll;
|
||||
},
|
||||
{ fallbackData: range(10).map(() => null) },
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO />
|
||||
@@ -168,96 +92,74 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
|
||||
<Text variant="h2">Explore The Database</Text>
|
||||
|
||||
<Grid $columns={3}>
|
||||
<Link href="/search" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigIcon icon={faSearch} className="fa-flip-horizontal" />
|
||||
<BigButton asChild>
|
||||
<Link href="/search">
|
||||
<BigIcon icon={faMagnifyingGlass} className="fa-flip-horizontal" />
|
||||
<Text>Search</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
<ShuffleDialog
|
||||
trigger={
|
||||
<BigButton>
|
||||
<BigIcon icon={faRandom} />
|
||||
<BigIcon icon={faShuffle} />
|
||||
<Text>Shuffle</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
href={currentYear && currentSeason ? `/year/${currentYear}/${currentSeason}` : "/"}
|
||||
passHref
|
||||
legacyBehavior
|
||||
>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigButton asChild>
|
||||
<Link href={currentYear && currentSeason ? `/year/${currentYear}/${currentSeason}` : "/"}>
|
||||
<BigIcon icon={faTv} />
|
||||
<Text>Current Season</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
</Grid>
|
||||
|
||||
<Grid $columns={4}>
|
||||
<Link href="/event" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigButton asChild>
|
||||
<Link href="/event">
|
||||
<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} />
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild>
|
||||
<Link href="/wiki">
|
||||
<BigIcon icon={faBookOpen} />
|
||||
<Text>Wiki</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/blog" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
<BigIcon icon={faMegaphone} />
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild>
|
||||
<Link href="/blog">
|
||||
<BigIcon icon={faBullhorn} />
|
||||
<Text>Blog</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/profile" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a">
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild>
|
||||
<Link href="/profile">
|
||||
{me.user ? <BigProfileImage user={me.user} size={96} /> : <BigIcon icon={faUser} />}
|
||||
<Text>My Profile</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
</Grid>
|
||||
|
||||
<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} entry={video.entries[0]} /> : null}
|
||||
</Skeleton>
|
||||
))}
|
||||
</Column>
|
||||
<RecentlyAddedVideos />
|
||||
</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} entry={video.entries[0]} /> : null}
|
||||
</Skeleton>
|
||||
))}
|
||||
</Column>
|
||||
<MostViewedVideos />
|
||||
</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>
|
||||
<RecentlyAddedPlaylists />
|
||||
</Column>
|
||||
</Grid>
|
||||
|
||||
@@ -285,36 +187,36 @@ export default function HomePage({ featuredTheme, announcementSources }: HomePag
|
||||
<Text variant="h2">Dive Deeper</Text>
|
||||
|
||||
<Grid $columns={5}>
|
||||
<Link href="/anime" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
|
||||
<BigButton asChild style={{ "--height": "48px" }}>
|
||||
<Link href="/anime">
|
||||
<Text>Anime Index</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/artist" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild style={{ "--height": "48px" }}>
|
||||
<Link href="/artist">
|
||||
<Text>Artist Index</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/year" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild style={{ "--height": "48px" }}>
|
||||
<Link href="/year">
|
||||
<Text>Year Index</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/series" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild style={{ "--height": "48px" }}>
|
||||
<Link href="/series">
|
||||
<Text>Series Index</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
<Link href="/studio" passHref legacyBehavior>
|
||||
<BigButton forwardedAs="a" style={{ "--height": "48px" }}>
|
||||
</Link>
|
||||
</BigButton>
|
||||
<BigButton asChild style={{ "--height": "48px" }}>
|
||||
<Link href="/studio">
|
||||
<Text>Studio Index</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</BigButton>
|
||||
</Link>
|
||||
</Link>
|
||||
</BigButton>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,10 @@ import type { GetServerSideProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import {
|
||||
faArrowTurnDownRight,
|
||||
faArrowTurnRight,
|
||||
faArrowTurnDown,
|
||||
faArrowTurnUp,
|
||||
faCheck,
|
||||
faEllipsisV,
|
||||
faEllipsisVertical,
|
||||
faGripVertical,
|
||||
faPen,
|
||||
faPlus,
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
faTrash,
|
||||
faTrophy,
|
||||
faXmark,
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { isAxiosError } from "axios";
|
||||
import { Reorder, useDragControls } from "framer-motion";
|
||||
import gql from "graphql-tag";
|
||||
import { shuffle } from "lodash-es";
|
||||
import { Reorder, useDragControls } from "motion/react";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
@@ -519,9 +519,9 @@ function Description({ playlist, description, setDescription, isEditable, setEdi
|
||||
</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Card hoverable onClick={() => setCollapsed(!isCollapsed)}>
|
||||
<Card $hoverable onClick={() => setCollapsed(!isCollapsed)}>
|
||||
<HeightTransition>
|
||||
<Text as="p" maxLines={isCollapsed ? 2 : null}>
|
||||
<Text as="p" maxLines={isCollapsed ? 2 : undefined}>
|
||||
{description}
|
||||
</Text>
|
||||
</HeightTransition>
|
||||
@@ -627,7 +627,7 @@ function PlaylistTrack({ playlist, track, isOwner, isRanking, isDraggable, onPla
|
||||
<Menu modal={false}>
|
||||
<MenuTrigger asChild>
|
||||
<Button variant="silent" isCircle>
|
||||
<Icon icon={faEllipsisV} />
|
||||
<Icon icon={faEllipsisVertical} />
|
||||
</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
@@ -645,11 +645,11 @@ function PlaylistTrack({ playlist, track, isOwner, isRanking, isDraggable, onPla
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<MenuItem onSelect={() => addWatchListItem(track.video, track.entry)}>
|
||||
<Icon icon={faArrowTurnDownRight} color="text-disabled" />
|
||||
<Icon icon={faArrowTurnDown} color="text-disabled" />
|
||||
<Text>Add to Watch List</Text>
|
||||
</MenuItem>
|
||||
<MenuItem onSelect={() => addWatchListItemNext(track.video, track.entry)}>
|
||||
<Icon icon={faArrowTurnRight} color="text-disabled" />
|
||||
<Icon icon={faArrowTurnUp} color="text-disabled" />
|
||||
<Text>Play Next</Text>
|
||||
</MenuItem>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,12 @@ import { memo, useState } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import type { GetServerSideProps } from "next";
|
||||
|
||||
import { faEllipsisV, faExclamationCircle, faPersonToDoor, faTrash } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
faCircleExclamation,
|
||||
faEllipsisVertical,
|
||||
faRightFromBracket,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { isAxiosError } from "axios";
|
||||
import gql from "graphql-tag";
|
||||
import useSWR from "swr";
|
||||
@@ -200,10 +205,10 @@ export default function ProfilePage({ me: initialMe }: ProfilePageProps) {
|
||||
<LogoutButton />
|
||||
</StyledHeaderTop>
|
||||
{!me.user.email_verified_at ? (
|
||||
<Card color="text-warning">
|
||||
<Card $color="text-warning">
|
||||
<Column style={{ "--gap": "8px" }}>
|
||||
<Text color="text-warning" weight="bold">
|
||||
<Icon icon={faExclamationCircle} /> Your email address is not verified!
|
||||
<Icon icon={faCircleExclamation} /> Your email address is not verified!
|
||||
</Text>
|
||||
<Text>
|
||||
You haven't verified your email address, yet. Please do so to unlock all
|
||||
@@ -251,7 +256,7 @@ export default function ProfilePage({ me: initialMe }: ProfilePageProps) {
|
||||
<Menu modal={false}>
|
||||
<MenuTrigger asChild>
|
||||
<Button variant="silent" isCircle>
|
||||
<Icon icon={faEllipsisV} />
|
||||
<Icon icon={faEllipsisVertical} />
|
||||
</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
@@ -363,7 +368,7 @@ function LogoutButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<IconTextButton icon={faPersonToDoor} onClick={performLogout}>
|
||||
<IconTextButton icon={faRightFromBracket} onClick={performLogout}>
|
||||
<Busy isBusy={isBusy}>Logout</Busy>
|
||||
</IconTextButton>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { faExclamationCircle } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Reorder } from "framer-motion";
|
||||
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Reorder } from "motion/react";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
import { Button } from "@/components/button/Button";
|
||||
@@ -156,9 +156,9 @@ export default function PlaylistPage() {
|
||||
)}
|
||||
{refreshError !== null && <Text color="text-warning">{refreshError}</Text>}
|
||||
</Row>
|
||||
<Card color="text-warning">
|
||||
<Card $color="text-warning">
|
||||
<Text color="text-warning" weight="bold">
|
||||
<Icon icon={faExclamationCircle} /> Read this before using the local playlist:
|
||||
<Icon icon={faCircleExclamation} /> Read this before using the local playlist:
|
||||
</Text>
|
||||
<Text as="p">
|
||||
The local playlist is only saved in your browser's storage. You can't share it
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { faCompass, faSearch } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { faCompass, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Column, Row } from "@/components/box/Flex";
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -21,7 +21,7 @@ export default function SearchGlobalPage() {
|
||||
) : (
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Row style={{ "--gap": "16px" }}>
|
||||
<Icon icon={faSearch} color="text-primary" />
|
||||
<Icon icon={faMagnifyingGlass} color="text-primary" />
|
||||
<Text color="text-muted">Looking for something specific? Use the search bar on the left!</Text>
|
||||
</Row>
|
||||
<Row style={{ "--gap": "16px" }}>
|
||||
|
||||
@@ -36,7 +36,7 @@ const StyledDesktopOnly = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
interface SeriesDetailPageProps extends RequiredNonNullable<SeriesDetailPageQuery> {}
|
||||
type SeriesDetailPageProps = RequiredNonNullable<SeriesDetailPageQuery>;
|
||||
|
||||
interface SeriesDetailPageParams extends ParsedUrlQuery {
|
||||
seriesSlug: string;
|
||||
|
||||
@@ -21,11 +21,9 @@ export default function SeriesIndexPage({ seriesAll }: SeriesIndexPageProps) {
|
||||
<Text variant="h1">Series Index</Text>
|
||||
<AlphabeticalIndex items={seriesAll}>
|
||||
{(series) => (
|
||||
<Link key={series.slug} href={`/series/${series.slug}`} passHref legacyBehavior prefetch={false}>
|
||||
<Text as="a" block link>
|
||||
{series.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={series.slug} as={Link} href={`/series/${series.slug}`} prefetch={false} block link>
|
||||
{series.name}
|
||||
</Text>
|
||||
)}
|
||||
</AlphabeticalIndex>
|
||||
</>
|
||||
|
||||
@@ -54,7 +54,7 @@ const StyledList = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
interface StudioDetailPageProps extends RequiredNonNullable<StudioDetailPageQuery> {}
|
||||
type StudioDetailPageProps = RequiredNonNullable<StudioDetailPageQuery>;
|
||||
|
||||
interface StudioDetailPageParams extends ParsedUrlQuery {
|
||||
studioSlug: string;
|
||||
|
||||
@@ -21,11 +21,9 @@ export default function StudioIndexPage({ studioAll }: StudioIndexPageProps) {
|
||||
<Text variant="h1">Studio Index</Text>
|
||||
<AlphabeticalIndex items={studioAll}>
|
||||
{(studio) => (
|
||||
<Link key={studio.slug} href={`/studio/${studio.slug}`} passHref legacyBehavior prefetch={false}>
|
||||
<Text as="a" block link>
|
||||
{studio.name}
|
||||
</Text>
|
||||
</Link>
|
||||
<Text key={studio.slug} as={Link} href={`/studio/${studio.slug}`} prefetch={false} block link>
|
||||
{studio.name}
|
||||
</Text>
|
||||
)}
|
||||
</AlphabeticalIndex>
|
||||
</>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user