mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
feat: Replaced Markdown implementation with MDX (#164)
* Replaced announcement toast with MDX component on the home page. * Added special anime award announcement button. * Fixed size of corner icons. * Removed design page.
This commit is contained in:
Generated
+2876
-282
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -26,19 +26,23 @@
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-parse-resolve-info": "^4.12.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"hast-util-has-property": "^2.0.1",
|
||||
"hast-util-heading-rank": "^2.1.1",
|
||||
"hast-util-to-string": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.0.12",
|
||||
"next": "^12.3.0",
|
||||
"next-mdx-remote": "^4.3.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-query": "^3.29.0",
|
||||
"rehype-parse": "^8.0.4",
|
||||
"rehype-react": "^7.0.4",
|
||||
"rehype-pretty-code": "^0.9.4",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.43.4",
|
||||
"shiki": "^0.14.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"use-local-storage-state": "^17.2.0",
|
||||
"use-session-storage-state": "^17.2.0"
|
||||
},
|
||||
@@ -48,9 +52,7 @@
|
||||
"@graphql-codegen/typescript-operations": "2.4.1",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/marked": "^4.0.3",
|
||||
"@types/node": "^17.0.41",
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@types/react": "^18.0.12",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled, { css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import theme from "theme";
|
||||
import { withHover } from "styles/mixins";
|
||||
import { Solid } from "components/box";
|
||||
@@ -82,9 +82,9 @@ const SolidButton = styled(BaseButton)`
|
||||
background-color: ${theme.colors["solid"]};
|
||||
color: ${theme.colors["text-muted"]};
|
||||
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
color: ${theme.colors["text"]};
|
||||
`)}
|
||||
`}
|
||||
|
||||
${Solid} & {
|
||||
background-color: ${theme.colors["solid-on-card"]};
|
||||
@@ -95,19 +95,19 @@ const SilentButton = styled(BaseButton)`
|
||||
background-color: transparent;
|
||||
color: ${theme.colors["text-muted"]};
|
||||
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
background-color: ${theme.colors["solid"]};
|
||||
color: ${theme.colors["text"]};
|
||||
`)}
|
||||
`}
|
||||
|
||||
&:not(:hover) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
${Solid} & {
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
background-color: ${theme.colors["solid-on-card"]};
|
||||
`)}
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import { Markdown } from "components/markdown/Markdown";
|
||||
import { AnimeAwardsNowAvailable } from "components/event/AnimeAwardsNowAvailable";
|
||||
|
||||
interface AnnouncementCardProps {
|
||||
announcementSource: MDXRemoteSerializeResult;
|
||||
}
|
||||
|
||||
export function AnnouncementCard({ announcementSource }: AnnouncementCardProps) {
|
||||
return (
|
||||
<Markdown source={announcementSource} components={{ AnimeAwardsNowAvailable }} />
|
||||
);
|
||||
}
|
||||
@@ -24,9 +24,9 @@ export const Card = styled(Solid)<{
|
||||
${(props) => props.hoverable && css`
|
||||
cursor: pointer;
|
||||
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
background-color: ${theme.colors["solid-on-card"]};
|
||||
`)}
|
||||
`}
|
||||
`}
|
||||
|
||||
&:before {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import styled, { keyframes } from "styled-components";
|
||||
import theme from "theme";
|
||||
import Link from "next/link";
|
||||
import { faArrowRight, faTrophy } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Text } from "components/text";
|
||||
import { Icon } from "components/icon";
|
||||
import { Button } from "components/button";
|
||||
|
||||
const EventButtonContainer = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const EventButton = styled(Button)`
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const EventIcon = styled(Icon)`
|
||||
margin: 0 0 -1rem -2rem;
|
||||
|
||||
font-size: 56px;
|
||||
color: ${theme.colors["text-disabled"]};
|
||||
`;
|
||||
|
||||
const GradientBorder = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 8px;
|
||||
|
||||
width: calc(100% + 4px);
|
||||
height: calc(100% + 4px);
|
||||
transform: translate(-2px, -2px);
|
||||
|
||||
@property --gradient-angle {
|
||||
syntax: '<angle>';
|
||||
initial-value: 0deg;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
--gradient-angle: 360deg;
|
||||
|
||||
background: linear-gradient(var(--gradient-angle), ${theme.colors["text-primary"]}, #fff);
|
||||
animation: ${keyframes`
|
||||
from {
|
||||
--gradient-angle: 0deg;
|
||||
}
|
||||
to {
|
||||
--gradient-angle: 360deg;
|
||||
}
|
||||
`} 5s linear infinite;
|
||||
|
||||
transition: opacity 250ms;
|
||||
opacity: 0;
|
||||
|
||||
${EventButtonContainer}:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
interface AnimeAwardsNowAvailableProps {
|
||||
year: number;
|
||||
}
|
||||
|
||||
export function AnimeAwardsNowAvailable({ year }: AnimeAwardsNowAvailableProps) {
|
||||
return (
|
||||
<EventButtonContainer>
|
||||
<GradientBorder />
|
||||
<Link href="/event/anime-awards" passHref legacyBehavior>
|
||||
<EventButton forwardedAs="a">
|
||||
<EventIcon icon={faTrophy} />
|
||||
<Text><Text color="text-primary">/r/anime Awards {year}</Text>: Results are now available!</Text>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
</EventButton>
|
||||
</Link>
|
||||
</EventButtonContainer>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,11 @@ import styled from "styled-components";
|
||||
import { Icon } from "components/icon/Icon";
|
||||
import theme from "theme";
|
||||
|
||||
export const CornerIcon = styled(Icon).attrs({
|
||||
size: "2x"
|
||||
})`
|
||||
export const CornerIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
font-size: 32px;
|
||||
color: ${theme.colors["text-primary"]};
|
||||
transform: translate(50%, -33%) rotate(10deg);
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { TextLink } from "components/text/TextLink";
|
||||
import { Text } from "components/text";
|
||||
import { Card } from "components/card";
|
||||
import styled from "styled-components";
|
||||
import theme from "theme";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
line-height: 1.75;
|
||||
word-break: break-word;
|
||||
|
||||
& h1 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
& p + h2, & ul + h2, & ol + h2, & ${Card} + h2, & pre + h2 {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
& p + h3, & ul + h3, & ol + h3, & ${Card} + h3, & pre + h3 {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
& table {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
& thead {
|
||||
border-bottom: 1px solid ${theme.colors["text-muted"]};
|
||||
|
||||
& th {
|
||||
font-weight: 600;
|
||||
vertical-align: bottom;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& tbody tr {
|
||||
border-bottom: 1px solid ${theme.colors["text-disabled"]};
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& tbody td {
|
||||
vertical-align: baseline;
|
||||
padding: 8px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& pre {
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
& pre > code {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
& ${Card} {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface MarkdownProps {
|
||||
source: MDXRemoteSerializeResult;
|
||||
components?: MDXComponents;
|
||||
}
|
||||
|
||||
export function Markdown({ source, components = {} }: MarkdownProps) {
|
||||
return (
|
||||
<StyledMarkdown>
|
||||
<MDXRemote
|
||||
{...source}
|
||||
components={{
|
||||
a: (props: ComponentPropsWithoutRef<"a">) => {
|
||||
const { href } = props;
|
||||
|
||||
if (href?.startsWith("/")) {
|
||||
return <TextLink href={href} {...props}/>;
|
||||
}
|
||||
|
||||
return <Text as="a" link href={href} {...props}/>;
|
||||
},
|
||||
h1: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h1" {...props}/>,
|
||||
h2: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h2" {...props}/>,
|
||||
h3: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h2" as="h3" {...props}/>,
|
||||
code: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="code" {...props}/>,
|
||||
Card,
|
||||
...components,
|
||||
}}
|
||||
/>
|
||||
</StyledMarkdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { Heading } from "utils/rehypeExtractHeadings";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Text } from "components/text";
|
||||
import styled from "styled-components";
|
||||
import { m } from "framer-motion";
|
||||
import theme from "theme";
|
||||
|
||||
const StyledTableOfContents = styled.ul`
|
||||
position: sticky;
|
||||
// TODO: Magic value neccessary?
|
||||
top: 92px;
|
||||
align-self: flex-start;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
max-height: calc(100vh - 92px);
|
||||
padding-left: 16px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
|
||||
& > li {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableOfContentsHeading = styled.li<{ $depth: number }>`
|
||||
padding-left: ${(props) => props.$depth === 3 && "16px"};
|
||||
font-size: ${(props) => props.$depth === 3 && "0.9rem"};
|
||||
`;
|
||||
|
||||
const StyledDot = styled(m.div)`
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: ${theme.colors["text-primary"]};
|
||||
`;
|
||||
|
||||
export function TableOfContents({ headings }: { headings: Array<Heading> }) {
|
||||
const [currentSlug, setCurrentSlug] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
const headings = [...document.querySelectorAll<HTMLHeadingElement>("h2, h3")];
|
||||
|
||||
let currentHeading = null;
|
||||
for (const heading of headings) {
|
||||
if (heading.offsetTop > window.scrollY + window.innerHeight) {
|
||||
break;
|
||||
}
|
||||
currentHeading = heading;
|
||||
if (heading.offsetTop > window.scrollY) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
setCurrentSlug(currentHeading?.id);
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledTableOfContents>
|
||||
{headings.map(({ text, slug, depth }) => (
|
||||
<StyledTableOfContentsHeading key={slug} $depth={depth}>
|
||||
{slug === currentSlug && (
|
||||
<StyledDot layoutId="dot"/>
|
||||
)}
|
||||
<Text as="a" link color={slug === currentSlug ? "text-muted" : "text-disabled"} href={`#${slug}`}>{text}</Text>
|
||||
</StyledTableOfContentsHeading>
|
||||
))}
|
||||
</StyledTableOfContents>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled, { css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import { Menu as ReachMenu, MenuButton, MenuItem, MenuItems, MenuPopover, } from "@reach/menu-button";
|
||||
import { Button } from "components/button";
|
||||
import theme from "theme";
|
||||
@@ -46,10 +46,10 @@ const StyledMenuItem = styled(MenuItem)`
|
||||
cursor: pointer;
|
||||
color: ${theme.colors["text-muted"]};
|
||||
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
background-color: ${theme.colors["solid-on-card"]};
|
||||
color: ${theme.colors["text"]};
|
||||
`)}
|
||||
`}
|
||||
`;
|
||||
const StyledMenuOverlay = styled(MenuPopover)`
|
||||
@media (max-width: 720px) {
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import styled, { css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import { LayoutGroup, m } from "framer-motion";
|
||||
import theme from "theme";
|
||||
import { uniqueId as createUniqueId } from "lodash-es";
|
||||
import type {
|
||||
ComponentPropsWithoutRef,
|
||||
ComponentPropsWithRef,
|
||||
ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo
|
||||
} from "react";
|
||||
import type { ComponentPropsWithoutRef, ComponentPropsWithRef, ReactNode } from "react";
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { faTimes } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Icon } from "components/icon";
|
||||
import { withHover } from "styles/mixins";
|
||||
@@ -61,10 +54,10 @@ const StyledButton = styled.button<StyledButtonProps>`
|
||||
|
||||
transition: color 500ms;
|
||||
|
||||
${withHover(css<StyledButtonProps>`
|
||||
color: ${(props: StyledButtonProps) => props.$isSelected ? theme.colors["text-on-primary"] : theme.colors["text"]};;
|
||||
${withHover`
|
||||
color: ${(props) => props.$isSelected ? theme.colors["text-on-primary"] : theme.colors["text"]};;
|
||||
transition-duration: 250ms;
|
||||
`)}
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledButtonBackground = styled(m.div)`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled, { css } from "styled-components";
|
||||
import styled from "styled-components";
|
||||
import theme from "theme";
|
||||
import { withHover } from "styles/mixins";
|
||||
import type { Property } from "csstype";
|
||||
@@ -24,9 +24,9 @@ export const TableRow = styled.div`
|
||||
border-bottom: 2px solid ${theme.colors["solid-on-card"]};
|
||||
}
|
||||
|
||||
${withHover(css`
|
||||
${withHover`
|
||||
background-color: ${theme.colors["solid"]};
|
||||
`)}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const TableCell = styled.div<{ style?: { "--span"?: number } }>`
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-jsx";
|
||||
import { Text } from "components/text";
|
||||
|
||||
Prism.manual = true;
|
||||
|
||||
interface HighlightProps {
|
||||
children: string
|
||||
block?: boolean
|
||||
}
|
||||
|
||||
export function Highlight({ children, block = false }: HighlightProps) {
|
||||
const codeHighlighted = Prism.highlight(children, Prism.languages.jsx, "jsx");
|
||||
const codeBlock = (
|
||||
<Text
|
||||
variant="code"
|
||||
block={block}
|
||||
className="language-jsx"
|
||||
dangerouslySetInnerHTML={{ __html: codeHighlighted }}
|
||||
/>
|
||||
);
|
||||
|
||||
if (block) {
|
||||
return (
|
||||
<pre>
|
||||
{codeBlock}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return codeBlock;
|
||||
}
|
||||
@@ -177,6 +177,7 @@ export type Performance = {
|
||||
export type Query = {
|
||||
anime: Maybe<Anime>;
|
||||
animeAll: Array<Anime>;
|
||||
announcementAll: Array<Announcement>;
|
||||
artist: Maybe<Artist>;
|
||||
artistAll: Array<Artist>;
|
||||
balanceAll: Array<Balance>;
|
||||
@@ -602,19 +603,19 @@ export type TransparencyIndexPageQueryVariables = Exact<{ [key: string]: never;
|
||||
|
||||
export type TransparencyIndexPageQuery = { balanceAll: Array<{ id: number, date: string, service: string, frequency: string, usage: number, month_to_date_balance: number }>, transactionAll: Array<{ id: number, date: string, service: string, description: string, amount: number }> };
|
||||
|
||||
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, script: { link: string } | null, audio: { basename: string } }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> };
|
||||
export type VideoPageAnimeFragment = { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> };
|
||||
|
||||
export type VideoPageQueryVariables = Exact<{
|
||||
animeSlug: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, script: { link: string } | null, audio: { basename: string } }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> } | null };
|
||||
export type VideoPageQuery = { anime: { name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> } | null };
|
||||
|
||||
export type VideoPageAllQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, script: { link: string } | null, audio: { basename: string } }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> }> };
|
||||
export type VideoPageAllQuery = { animeAll: Array<{ name: string, slug: string, year: number | null, season: string | null, themes: Array<{ id: number, slug: string, type: string, sequence: number | null, group: string | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } }> } | null, entries: Array<{ episodes: string | null, nsfw: boolean, spoiler: boolean, version: number | null, videos: Array<{ basename: string, filename: string, lyrics: boolean, nc: boolean, overlap: VideoOverlap, resolution: number | null, source: VideoSource | null, subbed: boolean, uncen: boolean, tags: string, entries: Array<{ theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }>, audio: { basename: string }, script: { link: string } | null }> }>, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null }>, images: Array<{ facet: string | null, link: string }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }> }> };
|
||||
|
||||
export type AnimeDetailPageAnimeFragment = { slug: string, name: string, season: string | null, year: number | null, synopsis: string | null, synonyms: Array<{ text: string | null }>, series: Array<{ slug: string, name: string }>, studios: Array<{ slug: string, name: string }>, resources: Array<{ site: string | null, link: string | null, as: string | null }>, themes: Array<{ slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, episodes: string | null, spoiler: boolean, nsfw: boolean, videos: Array<{ filename: string, tags: string, resolution: number | null, nc: boolean, subbed: boolean, lyrics: boolean, uncen: boolean, source: VideoSource | null, overlap: VideoOverlap }> }> }>, images: Array<{ link: string, facet: string | null }> };
|
||||
|
||||
@@ -661,11 +662,6 @@ export type ArtistIndexPageQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type ArtistIndexPageQuery = { artistAll: Array<{ slug: string, name: string }> };
|
||||
|
||||
export type DesignPageQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type DesignPageQuery = { anime: { slug: string, name: string, year: number | null, season: string | null, themes: Array<{ group: string | null }>, images: Array<{ link: string, facet: string | null }> } | null };
|
||||
|
||||
export type CharacterFragmentFragment = { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null };
|
||||
|
||||
export type RoundFragmentFragment = { tier: number, name: string | null, pairings: Array<{ order: number, group: number, votesA: number | null, votesB: number | null, characterA: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null }, characterB: { id: number, seed: number, name: string, source: string, image: string, theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null, entries: Array<{ version: number | null, videos: Array<{ tags: string }> }> } | null } }> };
|
||||
@@ -702,7 +698,7 @@ export type RecentlyAddedQuery = { recentlyAdded: Array<{ slug: string, type: st
|
||||
export type HomePageQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type HomePageQuery = { featuredTheme: { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, tags: string }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } } | null };
|
||||
export type HomePageQuery = { featuredTheme: { theme: { slug: string, type: string, sequence: number | null, group: string | null, id: number, anime: { slug: string, name: string, images: Array<{ link: string, facet: string | null }> } | null, entries: Array<{ version: number | null, videos: Array<{ basename: string, tags: string }> }>, song: { title: string | null, performances: Array<{ as: string | null, artist: { slug: string, name: string } }> } | null } } | null, announcementAll: Array<{ content: string }> };
|
||||
|
||||
export type GalleryPageQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -105,6 +105,11 @@ const resolvers: IResolvers = {
|
||||
extractor: (result) => result.transactions,
|
||||
pagination: true
|
||||
}),
|
||||
announcementAll: apiResolver({
|
||||
endpoint: () => `/announcement`,
|
||||
extractor: (result) => result.announcements,
|
||||
pagination: true
|
||||
}),
|
||||
},
|
||||
Year: {
|
||||
seasons: apiResolver({
|
||||
|
||||
@@ -23,6 +23,7 @@ const typeDefs = `
|
||||
dumpAll: [Dump!]!
|
||||
balanceAll: [Balance!]!
|
||||
transactionAll: [Transaction!]!
|
||||
announcementAll: [Announcement!]!
|
||||
}
|
||||
|
||||
interface ResourceWithImages {
|
||||
|
||||
+2
-5
@@ -17,7 +17,7 @@ import withBasePath from "utils/withBasePath";
|
||||
import { SEO } from "components/seo";
|
||||
import { config } from "@fortawesome/fontawesome-svg-core";
|
||||
import { ToastProvider } from "context/toastContext";
|
||||
import { AnnouncementToast, ToastHub } from "components/toast";
|
||||
import { ToastHub } from "components/toast";
|
||||
import { Text } from "components/text";
|
||||
import { useRouter } from "next/router";
|
||||
import useSetting from "hooks/useSetting";
|
||||
@@ -129,10 +129,7 @@ export default function MyApp({ Component, pageProps }: AppProps<any>) {
|
||||
} }),
|
||||
stackContext(QueryClientProvider, { client: queryClient }),
|
||||
stackContext(ApolloProvider, { client: apolloClient, children: null }),
|
||||
stackContext(ToastProvider, { initialToasts: [ {
|
||||
id: "announcement",
|
||||
content: <AnnouncementToast/>
|
||||
} ] }),
|
||||
stackContext(ToastProvider, {}),
|
||||
stackContext(LazyMotion, { features: () => import("utils/motionFeatures").then(res => res.default) }),
|
||||
stackContext(ErrorBoundary, {}),
|
||||
]}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GetStaticProps } from "next";
|
||||
import getSharedPageProps from "utils/getSharedPageProps";
|
||||
import markdownToHtml from "utils/markdownToHtml";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { DocumentPageProps } from "pages/wiki/[...pageSlug]";
|
||||
import serializeMarkdown from "utils/serializeMarkdown";
|
||||
|
||||
export { default } from "pages/wiki/[...pageSlug]/index";
|
||||
|
||||
@@ -15,7 +15,7 @@ export const getStaticProps: GetStaticProps<DocumentPageProps> = async () => {
|
||||
...getSharedPageProps(),
|
||||
page: {
|
||||
name: "Privacy Policy",
|
||||
body: markdownToHtml(markdown),
|
||||
...await serializeMarkdown(markdown),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { GetStaticProps } from "next";
|
||||
import getSharedPageProps from "utils/getSharedPageProps";
|
||||
import markdownToHtml from "utils/markdownToHtml";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { DocumentPageProps } from "pages/wiki/[...pageSlug]";
|
||||
import serializeMarkdown from "utils/serializeMarkdown";
|
||||
|
||||
export { default } from "pages/wiki/[...pageSlug]/index";
|
||||
|
||||
@@ -15,7 +15,7 @@ export const getStaticProps: GetStaticProps<DocumentPageProps> = async () => {
|
||||
...getSharedPageProps(),
|
||||
page: {
|
||||
name: "Terms of Service",
|
||||
body: markdownToHtml(markdown),
|
||||
...await serializeMarkdown(markdown),
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,685 +0,0 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { Column, Row } from "components/box";
|
||||
import { AnimeSummaryCard, Card } from "components/card";
|
||||
import { Text } from "components/text";
|
||||
import { Button, IconTextButton, VideoButton } from "components/button";
|
||||
import {
|
||||
faArrowRight,
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
faLightbulb,
|
||||
faPlay,
|
||||
faRocket,
|
||||
faSpinner,
|
||||
faStar,
|
||||
faTag
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { Icon } from "components/icon";
|
||||
import styled from "styled-components";
|
||||
import theme from "theme";
|
||||
import { Collapse, HeightTransition } from "components/utils";
|
||||
import ColorThemeContext from "context/colorThemeContext";
|
||||
import { ExternalLink } from "components/external-link";
|
||||
import { Switcher } from "components/switcher";
|
||||
import useToggle from "hooks/useToggle";
|
||||
import { Tag } from "components/tag/Tag";
|
||||
import { CoverImage } from "components/image";
|
||||
import { DescriptionList } from "components/description-list";
|
||||
import { codeBlock } from "common-tags";
|
||||
import { Listbox } from "components/listbox";
|
||||
import { fetchData } from "lib/server";
|
||||
import { Menu } from "components/menu";
|
||||
import { Toast } from "components/toast";
|
||||
import { useToasts } from "context/toastContext";
|
||||
import { Input } from "components/form";
|
||||
import gql from "graphql-tag";
|
||||
import getSharedPageProps from "utils/getSharedPageProps";
|
||||
import { Highlight } from "components/utils/Highlight";
|
||||
import type { DesignPageQuery } from "generated/graphql";
|
||||
import type { GetStaticProps } from "next";
|
||||
import { SwitcherOption, SwitcherReset } from "components/switcher/Switcher";
|
||||
import type { RequiredNonNullable } from "utils/types";
|
||||
import type { Colors } from "theme/colors";
|
||||
|
||||
const ColorGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20ch, 1fr));
|
||||
grid-gap: 16px;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
const ColorBox = styled.div<{ style: { "--color"?: string } }>`
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 2px solid ${theme.colors["text-primary"]};
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--color);
|
||||
`;
|
||||
|
||||
const ExampleGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
justify-items: flex-start;
|
||||
align-items: center;
|
||||
grid-gap: 32px;
|
||||
`;
|
||||
const ExampleFullWidth = styled.div`
|
||||
grid-column: 1 / 3;
|
||||
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
interface DesignPageProps {
|
||||
demoData: RequiredNonNullable<DesignPageQuery>
|
||||
}
|
||||
|
||||
export default function DesignPage({ demoData }: DesignPageProps) {
|
||||
const { colorTheme, setColorTheme } = useContext(ColorThemeContext);
|
||||
const [moreColors, setMoreColors] = useState(false);
|
||||
|
||||
const colors = Object.keys(theme.colors) as Array<keyof Colors>;
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [season, setSeason] = useState(null);
|
||||
|
||||
const [maxLines, toggleMaxLines] = useToggle(1, 0);
|
||||
const [collapse, toggleCollapse] = useToggle();
|
||||
|
||||
const { dispatchToast, closeToast } = useToasts();
|
||||
|
||||
return (
|
||||
<Column style={{ "--gap": "48px" }}>
|
||||
<Text variant="h1">Design Documentation</Text>
|
||||
<Row style={{ "--justify-content": "space-between", "--align-items": "center" }}>
|
||||
<Text variant="h2">Color</Text>
|
||||
<Switcher selectedItem={colorTheme} onChange={setColorTheme}>
|
||||
<SwitcherOption value="system">System</SwitcherOption>
|
||||
<SwitcherOption value="light">Light</SwitcherOption>
|
||||
<SwitcherOption value="dark">Dark</SwitcherOption>
|
||||
</Switcher>
|
||||
</Row>
|
||||
<HeightTransition>
|
||||
<ColorGrid>
|
||||
{colors.slice(0, moreColors ? colors.length : Math.min(15, colors.length)).map((color) => (
|
||||
<Color key={color} color={color}/>
|
||||
))}
|
||||
</ColorGrid>
|
||||
</HeightTransition>
|
||||
<Row style={{ "--justify-content": "flex-end" }}>
|
||||
<IconTextButton
|
||||
icon={faChevronDown} rotation={moreColors ? 180 : undefined}
|
||||
onClick={() => setMoreColors((val) => !val)}
|
||||
>{moreColors ? "Show less" : "Show more"}</IconTextButton>
|
||||
</Row>
|
||||
|
||||
<Text variant="h2">Typography</Text>
|
||||
<ExampleGrid>
|
||||
<Highlight>{`<Text>This is normal text.</Text>`}</Highlight>
|
||||
<Text>This is normal text.</Text>
|
||||
|
||||
<ExampleFullWidth>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
<Text as="p" color="text-muted">Always wrap text in a <Highlight>{`<Text>`}</Highlight> component. This way basic styling is applied and default browser values get overridden.</Text>
|
||||
</ExampleFullWidth>
|
||||
|
||||
<Highlight>{`<Text variant="h1">This is a page title.</Text>`}</Highlight>
|
||||
<Text variant="h1">This is a page title.</Text>
|
||||
|
||||
<Highlight>{`<Text variant="h2">This is a section title.</Text>`}</Highlight>
|
||||
<Text variant="h2">This is a section title.</Text>
|
||||
|
||||
<Highlight>{`<Text variant="small">This is small text.</Text>`}</Highlight>
|
||||
<Text variant="small">This is small text.</Text>
|
||||
|
||||
<Highlight>{`<Text variant="code">This is code.</Text>`}</Highlight>
|
||||
<Text variant="code">This is code.</Text>
|
||||
|
||||
<ExampleFullWidth>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
<Text as="p" color="text-muted">Use the <Text variant="code">variant</Text> prop to apply pre-defined styles to your text.</Text>
|
||||
</ExampleFullWidth>
|
||||
|
||||
<Highlight>{`<Text maxLines={1}>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.</Text>`}</Highlight>
|
||||
<Text maxLines={1}>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.</Text>
|
||||
|
||||
<Highlight>{`<Text maxLines={3}>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.</Text>`}</Highlight>
|
||||
<Text maxLines={3}>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.</Text>
|
||||
|
||||
<ExampleFullWidth>
|
||||
<Icon icon={faArrowRight} color="text-primary" />
|
||||
<Text as="p" color="text-muted">Use the <Text variant="code">maxLines</Text> prop to add a limit to the number of lines a text should have. Overflowing text will get replaced with three dots (<Text variant="code">...</Text>)</Text>
|
||||
</ExampleFullWidth>
|
||||
|
||||
<Highlight>{`<Text color="text">This is colored text.</Text>`}</Highlight>
|
||||
<Text color="text">This is colored text.</Text>
|
||||
|
||||
<Highlight>{`<Text color="text-muted">This is colored text.</Text>`}</Highlight>
|
||||
<Text color="text-muted">This is colored text.</Text>
|
||||
|
||||
<Highlight>{`<Text color="text-disabled">This is colored text.</Text>`}</Highlight>
|
||||
<Text color="text-disabled">This is colored text.</Text>
|
||||
|
||||
<Highlight>{`<Text color="text-warning">This is colored text.</Text>`}</Highlight>
|
||||
<Text color="text-warning">This is colored text.</Text>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<ExternalLink href="https://reddit.com/r/AnimeThemes">
|
||||
This is an external link.
|
||||
</ExternalLink>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<ExternalLink href="https://reddit.com/r/AnimeThemes">
|
||||
This is an external link.
|
||||
</ExternalLink>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Iconography</Text>
|
||||
<ExampleGrid>
|
||||
<Highlight>{`<Icon icon={faRocket}/>`}</Highlight>
|
||||
<Icon icon={faRocket}/>
|
||||
|
||||
<Highlight>{`<Icon icon={faStar} color="text-primary"/>`}</Highlight>
|
||||
<Icon icon={faStar} color="text-primary"/>
|
||||
|
||||
<Highlight>{`<Icon icon={faSpinner} className="fa-spin"/>`}</Highlight>
|
||||
<Icon icon={faSpinner} className="fa-spin"/>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Tag</Text>
|
||||
<ExampleGrid>
|
||||
<Highlight>{`<Tag icon={faTag}>Tag</Tag>`}</Highlight>
|
||||
<Tag icon={faTag}>Tag</Tag>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Tag icon={
|
||||
<Icon icon={faExclamationTriangle} color="text-warning"/>
|
||||
}>
|
||||
<Text color="text-warning">Warning</Text>
|
||||
</Tag>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Tag icon={
|
||||
<Icon icon={faExclamationTriangle} color="text-warning"/>
|
||||
}>
|
||||
<Text color="text-warning">Warning</Text>
|
||||
</Tag>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Button</Text>
|
||||
<ExampleGrid>
|
||||
<Highlight>{`<Button>Button</Button>`}</Highlight>
|
||||
<Button>Button</Button>
|
||||
|
||||
<Highlight>{`<Button variant="primary">Primary</Button>`}</Highlight>
|
||||
<Button variant="primary">Primary</Button>
|
||||
|
||||
<Highlight>{`<Button variant="silent">Silent</Button>`}</Highlight>
|
||||
<Button variant="silent">Silent</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Card>
|
||||
<Button>On Card</Button>
|
||||
<Button variant="silent">On Card</Button>
|
||||
</Card>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Card>
|
||||
<Row style={{ "--gap": "8px" }}>
|
||||
<Button>On Card</Button>
|
||||
<Button variant="silent">Silent On Card</Button>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Button isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Button variant="primary" isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button variant="primary" isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Button variant="silent" isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button variant="silent" isCircle>
|
||||
<Icon icon={faLightbulb}/>
|
||||
</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Button>
|
||||
<Button as="span" variant="primary">
|
||||
Prefixed
|
||||
</Button>
|
||||
Button
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button>
|
||||
<Button as="span" variant="primary">
|
||||
Prefixed
|
||||
</Button>
|
||||
Button
|
||||
</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Button>
|
||||
<Button as="span" variant="primary" isCircle>
|
||||
<Icon icon={faPlay}/>
|
||||
</Button>
|
||||
Playing
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button>
|
||||
<Button as="span" variant="primary" isCircle>
|
||||
<Icon icon={faPlay}/>
|
||||
</Button>
|
||||
Play
|
||||
</Button>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<VideoButton
|
||||
anime={{ slug: "bakemonogatari" }}
|
||||
theme={{ slug: "OP1" }}
|
||||
entry={{ version: "" }}
|
||||
video={{
|
||||
resolution: 1080,
|
||||
nc: true,
|
||||
source: "BD",
|
||||
overlap: "None"
|
||||
}}
|
||||
/>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<VideoButton
|
||||
anime={{ slug: "bakemonogatari" }}
|
||||
theme={{ slug: "OP1" }}
|
||||
entry={{ version: null }}
|
||||
video={{
|
||||
resolution: 1080,
|
||||
nc: true,
|
||||
source: "BD",
|
||||
overlap: "NONE",
|
||||
lyrics: false,
|
||||
subbed: false,
|
||||
uncen: false,
|
||||
tags: "",
|
||||
filename: ""
|
||||
}}
|
||||
/>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Switcher</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
// Inside the render output
|
||||
<Switcher selectedItem={selectedItem} onChange={setSelectedItem}>
|
||||
<SwitcherReset/>
|
||||
<SwitcherOption value="anime">Anime</SwitcherOption>
|
||||
<SwitcherOption value="theme">Themes</SwitcherOption>
|
||||
<SwitcherOption value="artists">Artists</SwitcherOption>
|
||||
</Switcher>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Switcher selectedItem={selectedItem} onChange={setSelectedItem}>
|
||||
<SwitcherReset/>
|
||||
<SwitcherOption value="anime">Anime</SwitcherOption>
|
||||
<SwitcherOption value="theme">Themes</SwitcherOption>
|
||||
<SwitcherOption value="artists">Artists</SwitcherOption>
|
||||
</Switcher>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Card</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Card>
|
||||
<Text>This is text inside a card.</Text>
|
||||
</Card>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Card>
|
||||
<Text>This is text inside a card.</Text>
|
||||
</Card>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Card hoverable>
|
||||
<Text>This is a hoverable card.</Text>
|
||||
</Card>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Card hoverable>
|
||||
<Text>This is a hoverable card.</Text>
|
||||
</Card>
|
||||
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const anime = /* ... */
|
||||
|
||||
// Inside the render output
|
||||
<AnimeSummaryCard anime={anime}/>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<AnimeSummaryCard anime={demoData.anime}/>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Input</Text>
|
||||
<ExampleGrid>
|
||||
<Highlight>
|
||||
{`<Input/>`}
|
||||
</Highlight>
|
||||
<Input value="" onChange={() => { /* Do nothing. */ }}/>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Image</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const anime = /* ... */
|
||||
|
||||
// Inside the render output
|
||||
<div style={{ width: "200px" }}>
|
||||
<CoverImage resourceWithImages={anime}/>
|
||||
</div>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<div style={{ width: "200px" }}>
|
||||
<CoverImage resourceWithImages={demoData.anime}/>
|
||||
</div>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Description List</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<DescriptionList>
|
||||
<DescriptionList.Item title="Topic">
|
||||
Description
|
||||
</DescriptionList.Item>
|
||||
<DescriptionList.Item title="Another Topic">
|
||||
<ExternalLink href="https://reddit.com/r/AnimeThemes">
|
||||
Link
|
||||
</ExternalLink>
|
||||
</DescriptionList.Item>
|
||||
</DescriptionList>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<DescriptionList>
|
||||
<DescriptionList.Item title="Topic">
|
||||
Description
|
||||
</DescriptionList.Item>
|
||||
<DescriptionList.Item title="Another Topic">
|
||||
<ExternalLink href="https://reddit.com/r/AnimeThemes">
|
||||
Link
|
||||
</ExternalLink>
|
||||
</DescriptionList.Item>
|
||||
</DescriptionList>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Experimental</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const [season, setSeason] = useState(null);
|
||||
|
||||
// Inside the render output
|
||||
<Listbox
|
||||
options={[
|
||||
null,
|
||||
"Winter",
|
||||
"Spring",
|
||||
"Summer",
|
||||
"Fall"
|
||||
]}
|
||||
selectedValue={season}
|
||||
onSelect={setSeason}
|
||||
nullValue="Any"
|
||||
resettable
|
||||
width="150px"
|
||||
/>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Listbox
|
||||
value={season}
|
||||
onChange={setSeason}
|
||||
resettable
|
||||
>
|
||||
<Listbox.Option value={null} hidden>Any</Listbox.Option>
|
||||
<Listbox.Option value="winter">Winter</Listbox.Option>
|
||||
<Listbox.Option value="spring">Spring</Listbox.Option>
|
||||
<Listbox.Option value="summer">Summer</Listbox.Option>
|
||||
<Listbox.Option value="fall">Fall</Listbox.Option>
|
||||
</Listbox>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
<Menu>
|
||||
<Menu.Option onSelect={() => alert("1")}>Option 1</Menu.Option>
|
||||
<Menu.Option onSelect={() => alert("2")}>Option 2</Menu.Option>
|
||||
<Menu.Option onSelect={() => alert("3")}>Option 3</Menu.Option>
|
||||
</Menu>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Menu>
|
||||
<Menu.Option onSelect={() => alert("1")}>Option 1</Menu.Option>
|
||||
<Menu.Option onSelect={() => alert("2")}>Option 2</Menu.Option>
|
||||
<Menu.Option onSelect={() => alert("3")}>Option 3</Menu.Option>
|
||||
</Menu>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const { dispatchToast, closeToast } = useToasts();
|
||||
|
||||
// Inside the render output
|
||||
<Button onClick={() => dispatchToast(
|
||||
"example-toast",
|
||||
<Toast
|
||||
hoverable
|
||||
onClick={() => closeToast("example-toast")}
|
||||
>
|
||||
This is a toast.
|
||||
It will close in 10 seconds or if you click on it.
|
||||
</Toast>,
|
||||
10_000
|
||||
)}>
|
||||
Show Toast
|
||||
</Button>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Button onClick={() => dispatchToast(
|
||||
"example-toast",
|
||||
<Toast
|
||||
hoverable
|
||||
onClick={() => closeToast("example-toast")}
|
||||
>
|
||||
This is a toast.
|
||||
It will close in 10 seconds or if you click on it.
|
||||
</Toast>,
|
||||
10_000
|
||||
)}>
|
||||
Show Toast
|
||||
</Button>
|
||||
</ExampleGrid>
|
||||
|
||||
<Text variant="h2">Utils</Text>
|
||||
<ExampleGrid>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const [maxLines, toggleMaxLines] = useToggle(1, 0);
|
||||
|
||||
// Inside the render output
|
||||
<Card>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<HeightTransition>
|
||||
<Text maxLines={maxLines}>
|
||||
Lorem ipsum dolor sit amet ...
|
||||
</Text>
|
||||
</HeightTransition>
|
||||
<Button onClick={toggleMaxLines}>
|
||||
Toggle text length
|
||||
</Button>
|
||||
</Column>
|
||||
</Card>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Card>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<HeightTransition>
|
||||
<Text maxLines={maxLines}>
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
</Text>
|
||||
</HeightTransition>
|
||||
<Button onClick={toggleMaxLines}>
|
||||
Toggle text length
|
||||
</Button>
|
||||
</Column>
|
||||
</Card>
|
||||
<pre>
|
||||
<Highlight block>
|
||||
{codeBlock`
|
||||
// At the top of the component
|
||||
const [collapse, toggleCollapse] = useToggle();
|
||||
|
||||
// Inside the render output
|
||||
<Card>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Collapse collapse={collapse}>
|
||||
<Text>
|
||||
Lorem ipsum dolor sit amet ...
|
||||
</Text>
|
||||
</Collapse>
|
||||
<Button onClick={toggleCollapse}>
|
||||
Toggle collapse
|
||||
</Button>
|
||||
</Column>
|
||||
</Card>
|
||||
`}
|
||||
</Highlight>
|
||||
</pre>
|
||||
<Card>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Collapse collapse={collapse}>
|
||||
<Text>
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
</Text>
|
||||
</Collapse>
|
||||
<Button onClick={toggleCollapse}>
|
||||
Toggle collapse
|
||||
</Button>
|
||||
</Column>
|
||||
</Card>
|
||||
</ExampleGrid>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
interface ColorProps {
|
||||
color: keyof Colors
|
||||
}
|
||||
|
||||
function Color({ color }: ColorProps) {
|
||||
return (
|
||||
<Column style={{ "--align-items": "center", "--gap": "8px" }}>
|
||||
<ColorBox style={{ "--color": theme.colors[color] }}/>
|
||||
<Text variant="code">{color}</Text>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<DesignPageProps> = async () => {
|
||||
const { data, apiRequests } = await fetchData<DesignPageQuery>(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
|
||||
query DesignPage {
|
||||
anime(slug: "bakemonogatari") {
|
||||
...AnimeSummaryCardAnime
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const anime = data.anime;
|
||||
|
||||
if (!anime) {
|
||||
return {
|
||||
notFound: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
demoData: { anime }
|
||||
}
|
||||
};
|
||||
};
|
||||
+17
-3
@@ -19,6 +19,9 @@ import { Skeleton } from "components/skeleton";
|
||||
import type { HomePageQuery, RecentlyAddedQuery } from "generated/graphql";
|
||||
import type { GetStaticProps } from "next";
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import { AnnouncementCard } from "components/card/AnnouncementCard";
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import serializeMarkdown from "utils/serializeMarkdown";
|
||||
|
||||
const BigButton = styled(Button)`
|
||||
justify-content: flex-end;
|
||||
@@ -84,10 +87,11 @@ const About = styled(Column)`
|
||||
`;
|
||||
|
||||
interface HomePageProps {
|
||||
featuredTheme: NonNullable<HomePageQuery["featuredTheme"]>["theme"] | null
|
||||
featuredTheme: NonNullable<HomePageQuery["featuredTheme"]>["theme"] | null;
|
||||
announcementSources: MDXRemoteSerializeResult[]
|
||||
}
|
||||
|
||||
export default function HomePage({ featuredTheme }: HomePageProps) {
|
||||
export default function HomePage({ featuredTheme, announcementSources }: HomePageProps) {
|
||||
const { currentYear, currentSeason } = useCurrentSeason();
|
||||
|
||||
const { data, loading } = useQuery<RecentlyAddedQuery>(gql`
|
||||
@@ -105,6 +109,10 @@ export default function HomePage({ featuredTheme }: HomePageProps) {
|
||||
<SEO/>
|
||||
<Text variant="h1">Welcome, to AnimeThemes.moe!</Text>
|
||||
|
||||
{announcementSources.length > 0 ? (
|
||||
<AnnouncementCard announcementSource={announcementSources[0]} />
|
||||
) : null}
|
||||
|
||||
{featuredTheme ? (
|
||||
<>
|
||||
<Text variant="h2">Featured Theme</Text>
|
||||
@@ -249,13 +257,19 @@ export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
|
||||
...FeaturedThemeTheme
|
||||
}
|
||||
}
|
||||
announcementAll {
|
||||
content
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return {
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
featuredTheme: data?.featuredTheme?.theme ?? null
|
||||
featuredTheme: data?.featuredTheme?.theme ?? null,
|
||||
announcementSources: await Promise.all(
|
||||
data?.announcementAll.map(async (announcement) => (await serializeMarkdown(announcement.content)).source)
|
||||
),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { fetchData } from "lib/server";
|
||||
import type { Heading, Markdown } from "utils/markdownToHtml";
|
||||
import markdownToHtml from "utils/markdownToHtml";
|
||||
import { unified } from "unified";
|
||||
import rehypeReact from "rehype-react";
|
||||
import { Text } from "components/text";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { createElement, Fragment, useEffect, useState } from "react";
|
||||
import rehypeParse from "rehype-parse";
|
||||
import styled from "styled-components";
|
||||
import { m } from "framer-motion";
|
||||
import theme from "theme";
|
||||
import { SEO } from "components/seo";
|
||||
import fetchStaticPaths from "utils/fetchStaticPaths";
|
||||
@@ -18,8 +9,12 @@ import getSharedPageProps from "utils/getSharedPageProps";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { DocumentPageAllQuery, DocumentPageQuery, DocumentPageQueryVariables } from "generated/graphql";
|
||||
import { TextLink } from "components/text/TextLink";
|
||||
import type { RequiredNonNullable } from "utils/types";
|
||||
import type { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import serializeMarkdown from "utils/serializeMarkdown";
|
||||
import type { Heading } from "utils/rehypeExtractHeadings";
|
||||
import { Markdown } from "components/markdown/Markdown";
|
||||
import { TableOfContents } from "components/markdown/TableOfContents";
|
||||
|
||||
const StyledGrid = styled.div`
|
||||
display: flex;
|
||||
@@ -39,138 +34,10 @@ const StyledGrid = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
line-height: 1.75;
|
||||
word-break: break-word;
|
||||
|
||||
& h1 {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
& h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
& p + h2, & ul + h2, & ol + h2 {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
& p + h3, & ul + h3, & ol + h3 {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
& table {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
& thead {
|
||||
border-bottom: 1px solid ${theme.colors["text-muted"]};
|
||||
|
||||
& th {
|
||||
font-weight: 600;
|
||||
vertical-align: bottom;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& tbody tr {
|
||||
border-bottom: 1px solid ${theme.colors["text-disabled"]};
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& tbody td {
|
||||
vertical-align: baseline;
|
||||
padding: 8px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& pre {
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
& pre > code {
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
padding: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableOfContents = styled.ul`
|
||||
position: sticky;
|
||||
// TODO: Magic value neccessary?
|
||||
top: 92px;
|
||||
align-self: flex-start;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
max-height: calc(100vh - 92px);
|
||||
padding-left: 16px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
|
||||
& > li {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableOfContentsHeading = styled.li<{ $depth: number }>`
|
||||
padding-left: ${(props) => props.$depth === 3 && "16px"};
|
||||
font-size: ${(props) => props.$depth === 3 && "0.9rem"};
|
||||
`;
|
||||
|
||||
const StyledDot = styled(m.div)`
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background-color: ${theme.colors["text-primary"]};
|
||||
`;
|
||||
|
||||
export interface DocumentPageProps extends SharedPageProps {
|
||||
page: Omit<RequiredNonNullable<DocumentPageQuery>["page"], "body"> & {
|
||||
body: Markdown
|
||||
source: MDXRemoteSerializeResult;
|
||||
headings: Heading[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,79 +46,15 @@ interface DocumentPageParams extends ParsedUrlQuery {
|
||||
}
|
||||
|
||||
export default function DocumentPage({ page }: DocumentPageProps) {
|
||||
const components = unified()
|
||||
.use(rehypeParse, { fragment: true })
|
||||
.use(rehypeReact, {
|
||||
createElement,
|
||||
Fragment,
|
||||
components: {
|
||||
a: (props: ComponentPropsWithoutRef<"a">) => {
|
||||
const { href } = props;
|
||||
|
||||
if (href?.startsWith("/")) {
|
||||
return <TextLink href={href} {...props}/>;
|
||||
}
|
||||
|
||||
return <Text as="a" link href={href} {...props}/>;
|
||||
},
|
||||
h1: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h1" {...props}/>,
|
||||
h2: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h2" {...props}/>,
|
||||
h3: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="h2" as="h3" {...props}/>,
|
||||
code: (props: ComponentPropsWithoutRef<typeof Text>) => <Text variant="code" {...props}/>
|
||||
}
|
||||
})
|
||||
.processSync(page.body.html).result;
|
||||
|
||||
return (
|
||||
<StyledGrid>
|
||||
<SEO title={page.name}/>
|
||||
<StyledMarkdown>
|
||||
{components}
|
||||
</StyledMarkdown>
|
||||
<TableOfContents headings={page.body.headings}/>
|
||||
<Markdown source={page.source} />
|
||||
<TableOfContents headings={page.headings} />
|
||||
</StyledGrid>
|
||||
);
|
||||
}
|
||||
|
||||
function TableOfContents({ headings }: { headings: Array<Heading> }) {
|
||||
const [currentSlug, setCurrentSlug] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
const headings = [...document.querySelectorAll<HTMLHeadingElement>("h2, h3")];
|
||||
|
||||
let currentHeading = null;
|
||||
for (const heading of headings) {
|
||||
if (heading.offsetTop > window.scrollY + window.innerHeight) {
|
||||
break;
|
||||
}
|
||||
currentHeading = heading;
|
||||
if (heading.offsetTop > window.scrollY) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
setCurrentSlug(currentHeading?.id);
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledTableOfContents>
|
||||
{headings.map(({ text, slug, depth }) => (
|
||||
<StyledTableOfContentsHeading key={slug} $depth={depth}>
|
||||
{slug === currentSlug && (
|
||||
<StyledDot layoutId="dot"/>
|
||||
)}
|
||||
<Text as="a" link color={slug === currentSlug ? "text-muted" : "text-disabled"} href={`#${slug}`}>{text}</Text>
|
||||
</StyledTableOfContentsHeading>
|
||||
))}
|
||||
</StyledTableOfContents>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<DocumentPageProps, DocumentPageParams> = async ({ params }) => {
|
||||
const { data, apiRequests } = await fetchData<DocumentPageQuery, DocumentPageQueryVariables>(gql`
|
||||
query DocumentPage($pageSlug: String!) {
|
||||
@@ -275,8 +78,8 @@ export const getStaticProps: GetStaticProps<DocumentPageProps, DocumentPageParam
|
||||
...getSharedPageProps(apiRequests),
|
||||
page: {
|
||||
...data.page,
|
||||
body: markdownToHtml(data.page.body)
|
||||
}
|
||||
...await serializeMarkdown(data.page.body),
|
||||
},
|
||||
},
|
||||
// Revalidate after 1 hour (= 3600 seconds).
|
||||
revalidate: 3600
|
||||
|
||||
@@ -14,7 +14,7 @@ export const loadingAnimation = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const withHover = (...args: Parameters<typeof css>) => css`
|
||||
export const withHover = <T extends object>(...args: Parameters<typeof css<T>>) => css`
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
${css(...args)}
|
||||
@@ -24,8 +24,8 @@ export const withHover = (...args: Parameters<typeof css>) => css`
|
||||
|
||||
export const withColorTheme = (
|
||||
colorTheme: "light" | "dark",
|
||||
) => (
|
||||
...args: Parameters<typeof css>
|
||||
) => <T extends object>(
|
||||
...args: Parameters<typeof css<T>>
|
||||
) => css`
|
||||
[data-theme="system"] & {
|
||||
@media (prefers-color-scheme: ${colorTheme}) {
|
||||
@@ -40,8 +40,8 @@ export const withColorTheme = (
|
||||
|
||||
export const defineColorTheme = (
|
||||
colorTheme: "light" | "dark",
|
||||
) => (
|
||||
...args: Parameters<typeof css>
|
||||
) => <T extends object>(
|
||||
...args: Parameters<typeof css<T>>
|
||||
) => css`
|
||||
&[data-theme="system"] {
|
||||
@media (prefers-color-scheme: ${colorTheme}) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { marked } from "marked";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-powershell";
|
||||
import "prismjs/components/prism-json";
|
||||
|
||||
export interface Markdown {
|
||||
html: string
|
||||
headings: Array<Heading>
|
||||
}
|
||||
|
||||
export interface Heading {
|
||||
text: string
|
||||
depth: number
|
||||
slug: string
|
||||
}
|
||||
|
||||
export default function markdownToHtml(markdown: string): Markdown {
|
||||
const headings: Array<Heading> = [];
|
||||
|
||||
const slugger = new marked.Slugger();
|
||||
|
||||
const html = marked(markdown, {
|
||||
highlight(code, lang) {
|
||||
if (lang) {
|
||||
if (lang in Prism.languages) {
|
||||
return Prism.highlight(code, Prism.languages[lang], lang);
|
||||
}
|
||||
return `[!] Unknown syntax highlighting language: ${lang}\n${code}`;
|
||||
}
|
||||
return code;
|
||||
},
|
||||
walkTokens(token) {
|
||||
if (token.type === "heading") {
|
||||
// To remove links and other markdown entities from the text we join only the text parts together.
|
||||
const text = joinTextDeep(token.tokens);
|
||||
// Slugger keeps track of seen slugs, so we have to generate a slug for every heading.
|
||||
// This way links will work in the table of contents later.
|
||||
const slug = slugger.slug(text);
|
||||
if (token.depth === 2 || token.depth === 3) {
|
||||
headings.push({
|
||||
text: text,
|
||||
depth: token.depth,
|
||||
slug
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
html,
|
||||
headings
|
||||
};
|
||||
}
|
||||
|
||||
function joinTextDeep(tokens: Array<marked.Token>): string {
|
||||
return tokens.map((token) =>
|
||||
"tokens" in token && token.tokens
|
||||
? joinTextDeep(token.tokens)
|
||||
: "text" in token ? token.text : ""
|
||||
).join("");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { hasProperty } from "hast-util-has-property";
|
||||
import { headingRank } from "hast-util-heading-rank";
|
||||
import { toString } from "hast-util-to-string";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { Root } from "hast";
|
||||
|
||||
export interface Heading {
|
||||
text: string
|
||||
depth: number
|
||||
slug: string
|
||||
}
|
||||
|
||||
interface ExtractHeadingsConfig {
|
||||
minRank?: number;
|
||||
maxRank?: number;
|
||||
headings: Heading[];
|
||||
}
|
||||
|
||||
export default function rehypeExtractHeadings({
|
||||
minRank = 2,
|
||||
maxRank = 3,
|
||||
headings,
|
||||
}: ExtractHeadingsConfig) {
|
||||
return (tree: Root) => {
|
||||
visit(tree, "element" as const, (node) => {
|
||||
const rank = headingRank(node);
|
||||
if (
|
||||
rank &&
|
||||
rank >= minRank &&
|
||||
rank <= maxRank &&
|
||||
hasProperty(node, "id") &&
|
||||
node.properties?.id
|
||||
) {
|
||||
headings.push({
|
||||
text: toString(node),
|
||||
slug: node.properties.id.toString(),
|
||||
depth: rank,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import type { Heading } from "utils/rehypeExtractHeadings";
|
||||
import rehypeExtractHeadings from "utils/rehypeExtractHeadings";
|
||||
import type { Compatible } from "vfile";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
|
||||
export default async function serializeMarkdown(markdown: Compatible) {
|
||||
const headings: Heading[] = [];
|
||||
|
||||
const source = await serialize(markdown, {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeExtractHeadings, { headings }],
|
||||
[rehypePrettyCode, { theme: "github-dark-dimmed" }],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
source,
|
||||
headings,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user