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:
Mani
2025-01-19 02:46:18 +01:00
committed by GitHub
parent 88238966fd
commit b0724e568a
105 changed files with 6600 additions and 15351 deletions
Vendored
+7
View File
@@ -0,0 +1,7 @@
import "react";
declare module "react" {
interface CSSProperties {
[key: `--${string}`]: string | number;
}
}
+5470 -14459
View File
File diff suppressed because it is too large Load Diff
+52 -43
View File
@@ -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;
+5 -5
View File
@@ -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";
+3 -3
View File
@@ -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} />
+28 -12
View File
@@ -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 -1
View File
@@ -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";
+16 -12
View File
@@ -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;
+10 -8
View File
@@ -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>
); );
} }
+2 -2
View File
@@ -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";
+4 -4
View File
@@ -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"])};
} }
`; `;
+2 -2
View File
@@ -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} />
+2 -2
View File
@@ -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,
}} }}
/> />
+14 -14
View File
@@ -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);
} }
}} }}
/> />
+15 -15
View File
@@ -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> &bull; </span>} {index < length - 1 && <span> &bull; </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);
} }
}} }}
/> />
+10 -13
View File
@@ -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>
+15 -8
View File
@@ -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}
+6 -12
View File
@@ -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;
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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
+9 -9
View File
@@ -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} />;
}
+5 -5
View File
@@ -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>
+47
View File
@@ -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>
);
}
+1 -1
View File
@@ -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}
+1 -1
View File
@@ -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 }) => (
+1 -1
View File
@@ -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}
+3 -5
View File
@@ -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]) => (
+18 -12
View File
@@ -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 -1
View File
@@ -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";
+8 -7
View File
@@ -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 -1
View File
@@ -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";
+4 -4
View File
@@ -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>
</> </>
+42 -51
View File
@@ -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>
+31 -47
View File
@@ -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>
+10 -14
View File
@@ -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 -1
View File
@@ -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";
+10 -8
View File
@@ -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>
)} )}
</> </>
+7 -3
View File
@@ -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>
); );
}); }
+37 -19
View File
@@ -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>
); );
} }
+32 -34
View File
@@ -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>
); );
}), }),
), ),
+33
View File
@@ -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>
);
}
+3 -3
View File
@@ -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 -1
View File
@@ -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";
+10 -8
View File
@@ -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;
} }
+9 -23
View File
@@ -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
View File
@@ -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),
}; };
} }
+1 -1
View File
@@ -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) => (
+8 -10
View File
@@ -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>
); );
} }
+9 -11
View File
@@ -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 -1
View File
@@ -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";
+2 -2
View File
@@ -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};
`; `;
+2 -2
View File
@@ -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;
+3 -5
View File
@@ -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>
+2 -2
View File
@@ -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;
+127
View File
@@ -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 };
+7 -3
View File
@@ -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"}`
+8 -12
View File
@@ -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 && (
+3 -5
View File
@@ -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";
+1 -1
View File
@@ -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 (
+1 -1
View File
@@ -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
View File
@@ -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 = {
+7 -4
View File
@@ -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,
}); });
} }
+1 -1
View File
@@ -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
+4 -2
View File
@@ -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
View File
@@ -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.
+5 -10
View File
@@ -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)}
+13 -19
View File
@@ -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>
+3 -5
View File
@@ -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>
</> </>
+11 -15
View File
@@ -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>
+3 -5
View File
@@ -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>
</> </>
+1 -1
View File
@@ -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";
+5 -7
View File
@@ -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>
); );
+9 -9
View File
@@ -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
View File
@@ -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>
</> </>
); );
+10 -10
View File
@@ -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>
</> </>
+10 -5
View File
@@ -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&apos;t verified your email address, yet. Please do so to unlock all You haven&apos;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>
); );
+4 -4
View File
@@ -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&apos;s storage. You can&apos;t share it The local playlist is only saved in your browser&apos;s storage. You can&apos;t share it
+2 -2
View File
@@ -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" }}>
+1 -1
View File
@@ -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;
+3 -5
View File
@@ -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>
</> </>
+1 -1
View File
@@ -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;
+3 -5
View File
@@ -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