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