diff --git a/gatsby-theme-oi-wiki/.eslintrc.js b/gatsby-theme-oi-wiki/.eslintrc.js index e5b6df75..be19a6da 100644 --- a/gatsby-theme-oi-wiki/.eslintrc.js +++ b/gatsby-theme-oi-wiki/.eslintrc.js @@ -34,6 +34,8 @@ module.exports = { ], rules: { 'object-curly-spacing': ['error', 'always'], + 'quotes': ['error', 'single', {'allowTemplateLiterals': true}], + 'semi': ['error', 'never', {'beforeStatementContinuationChars': 'always'}], 'react/prop-types': [0], 'comma-dangle': [2, 'always-multiline'], 'no-unused-vars': 'off', diff --git a/gatsby-theme-oi-wiki/package.json b/gatsby-theme-oi-wiki/package.json index 2bff867d..7f1f2922 100644 --- a/gatsby-theme-oi-wiki/package.json +++ b/gatsby-theme-oi-wiki/package.json @@ -59,7 +59,10 @@ "use-persisted-state": "^0.3.0" }, "devDependencies": { + "@types/autosuggest-highlight": "^3.1.1", + "@types/mark.js": "^8.11.6", "@types/react-helmet": "^6.1.1", + "@types/use-persisted-state": "^0.3.0", "@typescript-eslint/eslint-plugin": "^4.28.3", "@typescript-eslint/parser": "^4.28.3", "eslint": "^7.29.0", @@ -70,6 +73,7 @@ "eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^4.1.2", "gatsby-plugin-webpack-bundle-analyser-v2": "^1.1.24", + "schema-dts": "^0.9.0", "ts-graphql-plugin": "^2.1.3", "typescript": "^4.3.5" }, diff --git a/gatsby-theme-oi-wiki/src/components/AuthorsArray.tsx b/gatsby-theme-oi-wiki/src/components/AuthorsArray.tsx index ef37d2b2..aed3975c 100644 --- a/gatsby-theme-oi-wiki/src/components/AuthorsArray.tsx +++ b/gatsby-theme-oi-wiki/src/components/AuthorsArray.tsx @@ -8,25 +8,28 @@ const useStyles = makeStyles((theme) => ({ }, })) -const AuthorsArray: React.FC<{authors: string }> = ({ authors }) => { - const arr = authors && authors.split(',').map((x) => x.trim()) +export interface AuthorsArrayProps { + authors: string; +} + +const AuthorsArray: React.FC = ({ authors }) => { + const arr = authors?.split(',').map((x) => x.trim()) const classes = useStyles() return (
- {arr && - arr.map((author) => ( - - ))} + {arr?.map((author) => ( + + ))}
) } diff --git a/gatsby-theme-oi-wiki/src/components/BackTop.tsx b/gatsby-theme-oi-wiki/src/components/BackTop.tsx index eae3c496..bc7a355c 100644 --- a/gatsby-theme-oi-wiki/src/components/BackTop.tsx +++ b/gatsby-theme-oi-wiki/src/components/BackTop.tsx @@ -30,8 +30,8 @@ const BackTop: React.FC = () => { const handleClick: OnClickHandler = () => { smoothScrollTo(0) } - const [yPos, setyPos] = useState(0) - useThrottledOnScroll(() => setyPos(window.scrollY), 166) + const [yPos, setYPos] = useState(0) + useThrottledOnScroll(() => setYPos(window.scrollY), 166) return ( 400}> diff --git a/gatsby-theme-oi-wiki/src/components/Comment/Card/ReactionButton.tsx b/gatsby-theme-oi-wiki/src/components/Comment/Card/ReactionButton.tsx index b1a13b2d..61771de6 100644 --- a/gatsby-theme-oi-wiki/src/components/Comment/Card/ReactionButton.tsx +++ b/gatsby-theme-oi-wiki/src/components/Comment/Card/ReactionButton.tsx @@ -52,7 +52,7 @@ const ReactionButton: React.FC = (props) => { } setIsClicked(!isClicked) } - const SvgTag = props.icon; + const SvgTag = props.icon return ( diff --git a/gatsby-theme-oi-wiki/src/components/Link/LinkTooltip.tsx b/gatsby-theme-oi-wiki/src/components/Link/LinkTooltip.tsx index 96f5972d..aee9ddf4 100644 --- a/gatsby-theme-oi-wiki/src/components/Link/LinkTooltip.tsx +++ b/gatsby-theme-oi-wiki/src/components/Link/LinkTooltip.tsx @@ -1,58 +1,58 @@ import React, { useState } from 'react' import ToolCard from './ToolCard' import { useDidUpdateEffect } from './hooks' +import { Nullable } from '../../types/common' export interface PreviewData { text: string, title: string, } -async function getExcerpt (url) : Promise { - console.log('fetching', url) - const res = await fetch(url).then(res => res.json()) - return res -} - -type Props = { - url: string, // api url - children: any, - to: string, // link +export interface LinkTooltipProps { + /** api url */ + url: string, + /** link */ + to: string, } export type FetchStatus = 'error' | 'fetching' | 'fetched' | 'not_fetched' -const LinkTooltip : React.FC = function (props: Props) { +async function getExcerpt(url: string): Promise { + return await fetch(url).then(res => res.json()) +} + +const LinkTooltip: React.FC = (props) => { const { url, children } = props - const [content, setContent] = useState(null) + const [content, setContent] = useState>(null) const [status, setStatus] = useState('not_fetched') const [open, setOpen] = useState() + useDidUpdateEffect(() => { if (status === 'not_fetched') { - setStatus('fetching') // 防止重复获取 + // 防止重复获取 + setStatus('fetching') getExcerpt(url).then(data => { setContent(data) setStatus('fetched') }).catch(e => { console.error(e) - setStatus('error') // 获取失败 + setStatus('error') }) } }, [open]) - return ( - { - setOpen(true) - }} - content={content} - to={props.to} - status={status} - closeDelay={200} - openDelay={500} - > - {children} - - ) + return { + setOpen(true) + }} + content={content} + to={props.to} + status={status} + closeDelay={200} + openDelay={500} + > + {children} + } export default LinkTooltip diff --git a/gatsby-theme-oi-wiki/src/components/Link/ToolCard.tsx b/gatsby-theme-oi-wiki/src/components/Link/ToolCard.tsx index 21362668..905937a8 100644 --- a/gatsby-theme-oi-wiki/src/components/Link/ToolCard.tsx +++ b/gatsby-theme-oi-wiki/src/components/Link/ToolCard.tsx @@ -1,19 +1,21 @@ -import React, { useState, useRef, useEffect } from 'react' +import React, { createRef, useCallback, useEffect, useRef, useState } from 'react' +import { Card, CardContent, CircularProgress, Fade, makeStyles } from '@material-ui/core' +import { Link as GatsbyLink } from 'gatsby' + +import { FetchStatus, PreviewData } from './LinkTooltip' +import { useDelay } from './hooks' import { getElementSize, getElementViewPosition, Position, Size } from './utils' -import { - Card, - CardContent, - Fade, - CircularProgress, - makeStyles, -} from '@material-ui/core' +import { Nullable } from '../../types/common' import { Alert } from '@material-ui/lab' -import { PreviewData, FetchStatus } from './LinkTooltip' -import { useDelay } from './hooks' -import { Link as GatsbyLink } from 'gatsby' + const lines = 4 const lineHeight = 1.5 +const cardDis = '2rem' const useStyles = makeStyles((theme) => ({ + container: { + position: 'relative', + display: 'inline-block', + }, fade: { lineHeight: `${lineHeight}em`, height: `${lineHeight * lines}em`, @@ -27,142 +29,144 @@ const useStyles = makeStyles((theme) => ({ right: 0, width: '30%', height: '1.5em', - background: (theme.palette as any).fadeTextBackground, + background: theme.palette.fadeTextBackground, }, }, + toolCard: { + position: 'absolute', + zIndex: 9999, + width: '320px', + left: 0, + top: cardDis, + maxWidth: 'calc(100vw - 40px)', + }, + aboveMedian: { + top: 'initial', + bottom: cardDis, + }, + fetching: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, })) -type PositionAndSize = { + +interface PositionAndSize { pos: Position, size: Size, } -function adjustElementPosition (element: HTMLElement, { pos, size }: PositionAndSize) : void { - if (!element) return - const viewport = { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - } - const linkWidth = element.parentElement?.parentElement?.offsetWidth || 0 - function setLower (): void { - element.style.removeProperty('bottom') - element.style.setProperty('top', '2em') - } - function setUpper (): void { - element.style.removeProperty('top') - element.style.setProperty('bottom', '2em') - } - function setRight (): void { // 控制横坐标 - element.style.removeProperty('right') - let offset = 0 - offset = Math.max(offset, -pos.x + 12) // 不能超过屏幕左边 - offset = Math.min(offset, -pos.x + Math.max(0, viewport.width - size.width) - 12) // 不能超过屏幕右边 - element.style.setProperty('left', `${offset}px`) - } - function setLeft (): void { // 控制横坐标 - element.style.removeProperty('left') - let offset = 0 - offset = Math.min(offset, pos.x + linkWidth - size.width - 12) // 不能超过屏幕左边 - element.style.setProperty('right', `${offset}px`) - } - if (pos.y < viewport.height / 2) { // 位于上半部分 - setLower() - } else { - setUpper() - } - if (pos.x + linkWidth / 2 < viewport.width / 2) { - setRight() - } else { - setLeft() - } - element.style.setProperty('max-width', `${viewport.width - 24}px`) -} -type Props = { - children: any, - content: PreviewData, +export interface ToolCardProps { + content: Nullable, status: FetchStatus, onOpen?: () => void, - onHover?: () => void, // 不会延迟执行 + /** 不会延迟执行 */ + onHover?: () => void, openDelay?: number, closeDelay?: number, to: string, } -const ToolCard: React.FC = function (props: Props) { + +const ToolCard: React.FC = (props) => { const classes = useStyles() - const { children, content, status } = props - const closeDelay = props.closeDelay || 0 - const openDelay = props.openDelay || 0 + const { children, content, status, closeDelay = 0, openDelay = 0, onHover, to } = props const [open, setOpen] = useState(false) - const poperRef = useRef(null) + const rootRef = createRef() + const popperRef = useRef() const [onOpen, onClose] = useDelay(() => { setOpen(true) - props.onOpen && props.onOpen() + props.onOpen?.() }, () => { setOpen(false) }, openDelay, closeDelay) - const position = useRef(null) + + const adjustElementPosition = useCallback((element: HTMLElement, { pos, size }: PositionAndSize): void => { + if (!element || !rootRef.current) return + + const viewport = { + width: window?.innerWidth || document.documentElement.clientWidth, + height: window?.innerHeight || document.documentElement.clientHeight, + } + const betterDis = 20 + + let left, right + + // On the left half of the screen + if (pos.x < viewport.width / 2) { + const cardRightX = pos.x + size.width + const toRight = viewport.width - cardRightX - betterDis + if (toRight >= 0) { + left = 0 + } else { + const gap = pos.x + toRight + left = gap >= 0 ? toRight : gap + } + } else { + const { width } = getElementSize(rootRef.current) + const rootRightX = pos.x + width + const toLeft = rootRightX - size.width - betterDis + if (toLeft >= 0) { + right = 0 + } else { + const gap = viewport.width + toLeft + right = gap >= 0 ? toLeft : gap + } + } + + element.style.setProperty('right', typeof right === 'undefined' ? 'auto' : `${right}px`) + element.style.setProperty('left', typeof left === 'undefined' ? 'auto' : `${left}px`) + element.classList.toggle(classes.aboveMedian, pos.y > viewport.height / 2) + + }, [classes.aboveMedian, rootRef]) useEffect(() => { - if (open) { + if (open && popperRef.current) { const data: PositionAndSize = { - pos: getElementViewPosition(poperRef.current.parentElement), - size: getElementSize(poperRef.current), + pos: getElementViewPosition(popperRef.current.parentElement as HTMLElement), + size: getElementSize(popperRef.current), } - position.current = data - adjustElementPosition(poperRef.current, data) + adjustElementPosition(popperRef.current, data) } - }, [open, content]) + }, [open, content, popperRef, adjustElementPosition, rootRef]) return ( { onOpen() - props.onHover && props.onHover() + onHover?.() }} - onMouseLeave={() => onClose()} + onMouseLeave={onClose} + ref={rootRef} > - - - { - (status === 'fetching' || status === 'not_fetched') && - - - - } - {status === 'fetched' && - -
- - {content.title + ' '} - - {content.text} -
-
- } - { - status === 'error' && - 无法获取页面预览 - } -
-
+ {/* to temporarily fix issue https://github.com/OI-wiki/gatsby-oi-wiki/issues/928 */} +
+ + + + {(() => { + if (status === 'not_fetched' || status === 'fetching') { + return + + + } else if (status === 'fetched') { + return +
+ {content?.title + ' '} + {content?.text} +
+
+ } else { + return 无法获取页面预览 + } + })()} +
+
+
{children} ) diff --git a/gatsby-theme-oi-wiki/src/components/Link/hooks.tsx b/gatsby-theme-oi-wiki/src/components/Link/hooks.tsx index 1fbf928a..ef4f23ae 100644 --- a/gatsby-theme-oi-wiki/src/components/Link/hooks.tsx +++ b/gatsby-theme-oi-wiki/src/components/Link/hooks.tsx @@ -1,41 +1,59 @@ -import { useRef, useEffect } from 'react' +import React, { useEffect, useRef } from 'react' +import { Nullable } from '../../types/common' -export function useDidUpdateEffect (fn, inputs): void { +export function useDidUpdateEffect(fn: (...args: any) => void, inputs: React.DependencyList): void { const didMountRef = useRef(false) + const fnRef = useRef(fn) useEffect(() => { - if (didMountRef.current) { fn() } else { didMountRef.current = true } + if (didMountRef.current) fnRef.current() + else didMountRef.current = true + // eslint-disable-next-line react-hooks/exhaustive-deps }, inputs) } -export function useDelay (onOpen: () => void, onClose: () => void, openDelay: number, closeDelay: number) : [() => void, () => void] { - const closeHandle = useRef(null) - const openHandle = useRef(null) - function open () : void { - if (closeHandle.current) { // 正在准备close,则不让它close +export function useDelay(onOpen: () => void, onClose: () => void, openDelay: number, closeDelay: number): [() => void, () => void] { + const closeHandle = useRef>() + const openHandle = useRef>() + + const clearClose = (): void => { + if (closeHandle.current) { clearTimeout(closeHandle.current) closeHandle.current = null } - if (!openHandle.current) { // 如果之前没有 open 时间就创建一个 + } + + const clearOpen = (): void => { + if (openHandle.current) { + clearTimeout(openHandle.current) + openHandle.current = null + } + } + + function open(): void { + // 正在准备close,则不让它close + clearClose() + + // 如果之前没有 open 事件就创建一个 + if (!openHandle.current) { openHandle.current = setTimeout(() => { onOpen() - openHandle.current = null + clearOpen() }, openDelay) } } - function close () : void { - if (closeHandle) { // 之前的 close 事件需要被清除 - clearTimeout(closeHandle.current) - closeHandle.current = null - } - if (openHandle.current) { // 鼠标快进快出,则不显示 - clearTimeout(openHandle.current) - openHandle.current = null - } + + function close(): void { + // 之前的 close 事件需要被清除 + clearClose() + // 鼠标快进快出,则不显示 + clearOpen() + closeHandle.current = setTimeout(() => { onClose() - closeHandle.current = null + clearClose() }, closeDelay) } + return [open, close] } diff --git a/gatsby-theme-oi-wiki/src/components/Link/index.tsx b/gatsby-theme-oi-wiki/src/components/Link/index.tsx index d617bc36..54c26f25 100644 --- a/gatsby-theme-oi-wiki/src/components/Link/index.tsx +++ b/gatsby-theme-oi-wiki/src/components/Link/index.tsx @@ -6,10 +6,10 @@ import LinkTooltip from './LinkTooltip' import path from 'path' import clsx from 'clsx' import { GatsbyLinkProps } from 'gatsby-link' -import smoothScrollTo from "../../lib/smoothScroll"; -import { useSetting } from "../../lib/useSetting"; -import { useMediaQuery, useTheme } from "@material-ui/core"; -import { OnClickHandler } from "../../types/common"; +import smoothScrollTo from '../../lib/smoothScroll' +import { useSetting } from '../../lib/useSetting' +import { useMediaQuery, useTheme } from '@material-ui/core' +import { OnClickHandler } from '../../types/common' const MD_EXPR = /\.(md|markdown|mdtext|mdx)/g const NO_SLASH_EXPR = /[^/]$/ diff --git a/gatsby-theme-oi-wiki/src/components/Link/utils.ts b/gatsby-theme-oi-wiki/src/components/Link/utils.ts new file mode 100644 index 00000000..c66a1c4d --- /dev/null +++ b/gatsby-theme-oi-wiki/src/components/Link/utils.ts @@ -0,0 +1,21 @@ +export type Position = Pick + +/** + * 获取元素的绝对位置坐标(相对于浏览器视区左上角) + */ +export const getElementViewPosition = (el: HTMLElement): Position => { + const rect = el.getBoundingClientRect() + return { + x: rect.x || rect.top, + y: rect.y || rect.bottom, + } +} + +export type Size = Pick + +export function getElementSize(el: HTMLElement): Size { + return { + width: el.offsetWidth, + height: el.offsetHeight, + } +} diff --git a/gatsby-theme-oi-wiki/src/components/Link/utils.tsx b/gatsby-theme-oi-wiki/src/components/Link/utils.tsx deleted file mode 100644 index 2bd51a64..00000000 --- a/gatsby-theme-oi-wiki/src/components/Link/utils.tsx +++ /dev/null @@ -1,59 +0,0 @@ -export type Position = { - x: number, - y: number, -} -/** - * 获取元素的绝对位置坐标(像对于浏览器视区左上角) - * - * (https://www.hangge.com/blog/cache/detail_2260.html) - * - * @export - * @param {*} el - * @return {Position} - */ -export function getElementViewPosition (el: any): Position { - // 计算x坐标 - let actualLeft = el.offsetLeft - let current = el.offsetParent - let elScrollLeft - while (current !== null) { - actualLeft += (current.offsetLeft + current.clientLeft) - current = current.offsetParent - } - if (document.compatMode === 'BackCompat') { - elScrollLeft = document.body.scrollLeft - } else { - elScrollLeft = document.documentElement.scrollLeft - } - const left = actualLeft - elScrollLeft - // 计算y坐标 - let actualTop = el.offsetTop - current = el.offsetParent - let elScrollTop - while (current !== null) { - actualTop += (current.offsetTop + current.clientTop) - current = current.offsetParent - } - if (document.compatMode === 'BackCompat') { - elScrollTop = document.body.scrollTop - } else { - elScrollTop = document.documentElement.scrollTop - } - const right = actualTop - elScrollTop - return { - x: left, - y: right, - } -} - -export type Size = { - width: number, - height: number, -} - -export function getElementSize (el: HTMLElement): Size { - return { - width: el.offsetWidth, - height: el.offsetHeight, - } -} diff --git a/gatsby-theme-oi-wiki/src/components/Mdx.tsx b/gatsby-theme-oi-wiki/src/components/Mdx.tsx new file mode 100644 index 00000000..45b9b843 --- /dev/null +++ b/gatsby-theme-oi-wiki/src/components/Mdx.tsx @@ -0,0 +1,109 @@ +import React, { useCallback, useEffect } from 'react' +import Mark from 'mark.js' +import clsx from 'clsx' + +import MDRenderer from '../lib/MDRenderer' +import Details from './Details' +import Summary from './Summary' +import { SmartLink, SmartLinkProps } from './Link' +import SEO from './Seo' +import StyledLayout from './StyledLayout' +import { DeepRequiredNonNull, DeepWriteable, Nullable } from '../types/common' +import { ReactiveHastComponents } from '../lib/reactiveHast' + +export interface MdxProps { + data: DeepWriteable> + location: Location & { state: Nullable<{ searchKey: string }> }; +} + +const Mdx: React.FC = ({ data: { mdx }, location }) => { + const title = mdx.fields.slug === '/' ? '' : mdx.frontmatter.title + const description = mdx.frontmatter.description || mdx.excerpt + const authors = mdx.frontmatter.author || '' + const tags = mdx.frontmatter.tags || [] + const noMeta = mdx.frontmatter.noMeta || false + const noComment = mdx.frontmatter.noComment || false + const noEdit = false + const toc = mdx.toc || null + const relativePath = mdx.parent.relativePath || '' + const modifiedTime = mdx.parent.modifiedTime || '' + const wordCount = mdx.wordCount.words || 0 + const datePublished = mdx.parent.birthTime || '' + const dateModified = mdx.parent.changeTime || '' + const isIndex = mdx.fields.isIndex + + const highlightNode = useCallback((tagName: string, isHighlight: boolean): void => { + if (location.state?.searchKey) { + const mainNodes = document.getElementsByTagName('main') + const nodes = mainNodes[0].querySelectorAll(tagName) + const instance = new Mark(nodes) + instance[isHighlight ? 'mark' : 'unmark']( + location.state.searchKey, + { + exclude: ['span'], + }) + } + }, [location.state?.searchKey]) + + useEffect(() => { + if (location.state?.searchKey) { + highlightNode('h1', true) + highlightNode('h2', true) + highlightNode('h3', true) + highlightNode('p', true) + setTimeout( + () => { + highlightNode('h1', false) + highlightNode('h2', false) + highlightNode('h3', false) + highlightNode('p', false) + }, 5000) + } + }, [highlightNode, location.state?.searchKey]) + + const LinkGetter: React.FC = (props) => + + + const InlineCode: React.FC<{ className: string, [key: string]: any }> = (props) => { + const { className, children, ...others } = props + return {children} + } + + const myComponents = { + details: Details, + summary: Summary, + a: LinkGetter, + inlinecode: InlineCode, + } as ReactiveHastComponents + + const isWIP = wordCount === 0 || (tags?.findIndex((x: string) => x === 'WIP') >= 0) + + return ( + + + + + ) +} + +export default Mdx diff --git a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/NavBtnGroup.tsx b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/NavBtnGroup.tsx index 03203705..e12c3cf1 100644 --- a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/NavBtnGroup.tsx +++ b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/NavBtnGroup.tsx @@ -1,11 +1,12 @@ import React from 'react' -import { Tooltip, IconButton, makeStyles } from '@material-ui/core' +import { IconButton, IconButtonProps, makeStyles, Tooltip } from '@material-ui/core' import { + GitHub as GitHubIcon, LibraryBooks as LibraryBooksIcon, LocalOffer as LocalOfferIcon, - GitHub as GitHubIcon, Settings as SettingsIcon, + SvgIconComponent, } from '@material-ui/icons' const useStyles = makeStyles({ @@ -15,32 +16,36 @@ const useStyles = makeStyles({ }, }) -function NavBtn ({ title, href, Icon, ...restProps }) { +interface NavBtnProps extends IconButtonProps<'a', React.AnchorHTMLAttributes> { + title: string; + Icon: SvgIconComponent; +} + +const NavBtn: React.FC = (props) => { const classes = useStyles() - return ( - - - - - - ) + const { title, href, Icon, ...restProps } = props + return + + + + } const OIWikiGithub = 'https://github.com/OI-wiki/OI-wiki' -export default function NavBtnGroup () { - return ( - <> - - - - - - ) +const NavBtnGroup: React.FC = () => { + return <> + + + + + } + +export default NavBtnGroup diff --git a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/index.tsx b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/index.tsx index 3e622d3f..b3029e1a 100644 --- a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/index.tsx +++ b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/index.tsx @@ -15,8 +15,9 @@ import { flattenObject } from './utils' import { useSetting } from '../../lib/useSetting' import NavBtnGroup from './NavBtnGroup' import Search from '../Search' +import { StrIndexObj } from '../../types/common' -const getTabIDFromLocation = (location: string, pathList: string[]): number => { +const getTabIDFromLocation = (location: string, pathList: StrIndexObj): number => { const locationTrimmed = trimTrailingSlash(location) for (const v of Object.entries(pathList)) { if (Object.values(flattenObject(v[1])).map(v => trimTrailingSlash(v as string)).indexOf(locationTrimmed) > -1) return +v[0] diff --git a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/utils.tsx b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/utils.tsx index bf8ce37d..894a50db 100644 --- a/gatsby-theme-oi-wiki/src/components/NavAndDrawer/utils.tsx +++ b/gatsby-theme-oi-wiki/src/components/NavAndDrawer/utils.tsx @@ -1,25 +1,25 @@ +import { StrIndexObj } from '../../types/common' + /** * Flatten JS object (keys and values) to a single depth object * (ref: https://stackoverflow.com/a/53739792) - * - * @param {*} ob - * @return {*} {Record} */ -export function flattenObject (ob:any) :Record { - const toReturn = {} +export function flattenObject(ob: StrIndexObj): StrIndexObj { + const toReturn: StrIndexObj = {} for (const i in ob) { - if (!Object.prototype.hasOwnProperty.call(ob, i)) continue - - if ((typeof ob[i]) === 'object' && ob[i] !== null) { - const flatObject = flattenObject(ob[i]) - for (const x in flatObject) { - if (!Object.prototype.hasOwnProperty.call(flatObject, x)) continue + if (Object.prototype.hasOwnProperty.call(ob, i)) { + if ((typeof ob[i]) === 'object' && ob[i] !== null) { + const flatObject = flattenObject(ob[i]) - toReturn[i + '.' + x] = flatObject[x] + for (const x in flatObject) { + if (Object.prototype.hasOwnProperty.call(flatObject, x)) { + toReturn[i + '.' + x] = flatObject[x] + } + } + } else { + toReturn[i] = ob[i] } - } else { - toReturn[i] = ob[i] } } return toReturn diff --git a/gatsby-theme-oi-wiki/src/components/Search/ResultList.tsx b/gatsby-theme-oi-wiki/src/components/Search/ResultList.tsx index 3c24bda8..418d76a3 100644 --- a/gatsby-theme-oi-wiki/src/components/Search/ResultList.tsx +++ b/gatsby-theme-oi-wiki/src/components/Search/ResultList.tsx @@ -20,8 +20,11 @@ export interface SearchResultListProps { const SearchResultList: React.FC = props => { const { result, isFirstRun, searchKey, classes } = props const resultCount = result.length - return resultCount !== 0 - ? (<> + return resultCount === 0 + ? (isFirstRun.current + ? <> + : 没有找到符合条件的结果) + : (<> 共找到 {resultCount} 条搜索结果: @@ -64,11 +67,6 @@ const SearchResultList: React.FC = props => { )} ) - : (isFirstRun.current - ? <> - : ( - 没有找到符合条件的结果 - )) } export default SearchResultList diff --git a/gatsby-theme-oi-wiki/src/components/Search/hooks.ts b/gatsby-theme-oi-wiki/src/components/Search/hooks.ts index cd8e8ae7..47f88470 100644 --- a/gatsby-theme-oi-wiki/src/components/Search/hooks.ts +++ b/gatsby-theme-oi-wiki/src/components/Search/hooks.ts @@ -35,8 +35,8 @@ const useWindowDimensions = (): WindowDimensions => { useEffect(() => { const handleResize = (): void => { setWindowDimensions({ - ...windowDimensions, width: window.innerWidth, + height: window.innerHeight, }) } diff --git a/gatsby-theme-oi-wiki/src/components/Search/index.tsx b/gatsby-theme-oi-wiki/src/components/Search/index.tsx index 9067e048..7e7b0dc1 100644 --- a/gatsby-theme-oi-wiki/src/components/Search/index.tsx +++ b/gatsby-theme-oi-wiki/src/components/Search/index.tsx @@ -13,12 +13,9 @@ import { useDebounce, useWindowDimensions } from './hooks' * 从 API 获取搜索数据 * @param str 要搜索的内容 */ -const fetchResult = (str: string): Promise => fetch( - `https://search.oi-wiki.org:8443/?s=${encodeURIComponent(str)}`, - { - // credentials: "same-origin" - }, -).then((response) => response.json()) +const fetchResult = (str: string): Promise => + fetch(`https://search.oi-wiki.org:8443/?s=${encodeURIComponent(str)}`) + .then((response) => response.json()) const Search: React.FC = () => { const [searchKey, setSearchKey] = useState('') @@ -53,11 +50,7 @@ const Search: React.FC = () => {
@@ -124,9 +117,6 @@ const Search: React.FC = () => { onChange={(ev) => { setSearchKey(ev.target.value) }} - // onFocus={() => { - // setOpen(true) - // }} classes={{ root: classes.smallScreenInputRoot, input: classes.inputInput, diff --git a/gatsby-theme-oi-wiki/src/components/Seo.tsx b/gatsby-theme-oi-wiki/src/components/Seo.tsx index 21b849a1..b121e477 100644 --- a/gatsby-theme-oi-wiki/src/components/Seo.tsx +++ b/gatsby-theme-oi-wiki/src/components/Seo.tsx @@ -1,116 +1,104 @@ import React from 'react' import { Helmet } from 'react-helmet' import { useLocation } from '@reach/router' -import { useStaticQuery, graphql } from 'gatsby' +import { graphql, useStaticQuery } from 'gatsby' +import { Article, WithContext } from 'schema-dts' +import { DeepRequiredNonNull } from '../types/common' -interface Props { +interface SEOProps { title: string; description: string; - image: string; article: boolean; author: string; tags: string[]; dateModified: string; datePublished: string; + image?: string } -const SEO: React.FC = (props: Props) => { +const SEO: React.FC = (props: SEOProps) => { + const { pathname } = useLocation() + const { site: { siteMetadata } } = useStaticQuery(graphql` + query SEO { + site { + siteMetadata { + defaultTitle: title + defaultDescription: description + siteUrl + defaultAuthor: author + } + } + } + `) as DeepRequiredNonNull + const { title = null, description = null, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - image = null, article = false, - author = null, + author = '', tags = null, dateModified, datePublished, } = props - const { pathname } = useLocation() - const { site } = useStaticQuery(query) - - const { - defaultTitle, - defaultDescription, - siteUrl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - defaultAuthor, - } = site.siteMetadata const seo = { - title: title || defaultTitle, - description: description || defaultDescription, - // image: `${siteUrl}${image}`, + title: title || siteMetadata.defaultTitle, + description: description || siteMetadata.defaultDescription, image: 'https://cdn.jsdelivr.net/npm/oicdn@0.0.2/wordArt.webp', - url: `${siteUrl}${pathname}`, + url: `${siteMetadata.siteUrl}${pathname}`, author: author && author.split(','), tags: tags, } - const schemaMarkUp = { + const schemaMarkUp: WithContext
= { + '@context': 'https://schema.org', + '@type': 'Article', headline: seo.title, - image: ['https://cdn.jsdelivr.net/npm/oicdn@0.0.2/wordArt.webp'], + image: [seo.image], datePublished: datePublished, dateModified: dateModified, mainEntityOfPage: seo.url, author: { + '@type': 'Person', name: author, }, publisher: { + '@type': 'Organization', name: 'OI Wiki', logo: { - url: 'https://cdn.jsdelivr.net/npm/oicdn@0.0.2/wordArt.webp', + '@type': 'ImageObject', + url: seo.image, }, }, } - schemaMarkUp['@context'] = 'https://schema.org' - schemaMarkUp['@type'] = 'Article' - schemaMarkUp.author['@type'] = 'Person' - schemaMarkUp.publisher['@type'] = 'Organization' - schemaMarkUp.publisher.logo['@type'] = 'ImageObject' return ( - - - + + + - {seo.url && } + {seo.url && } - {article && } - {seo.tags && seo.tags.map((tag) => )} - {seo.author && seo.author.map((author) => )} + {article && } + {seo.tags && seo.tags.map((tag) => )} + {seo.author && seo.author.map((author) => )} - {seo.title && } + {seo.title && } - {seo.description && } + {seo.description && } - {seo.image && } + {seo.image && } - + - {seo.title && } + {seo.title && } - {seo.description && ( - - )} + {seo.description && } - {seo.image && } + {seo.image && } {schemaMarkUp && } ) } export default SEO - -const query = graphql` - query SEO { - site { - siteMetadata { - defaultTitle: title - defaultDescription: description - siteUrl - defaultAuthor: author - } - } - } -` diff --git a/gatsby-theme-oi-wiki/src/components/Sidebar.tsx b/gatsby-theme-oi-wiki/src/components/Sidebar.tsx index 9a0483cd..5ad85d02 100644 --- a/gatsby-theme-oi-wiki/src/components/Sidebar.tsx +++ b/gatsby-theme-oi-wiki/src/components/Sidebar.tsx @@ -8,7 +8,7 @@ import { Link } from 'gatsby' import trimTrailingSlash from '../lib/trailingSlash' const useStyles = makeStyles((theme) => ({ - listitem: { + listItem: { color: theme.palette.text.primary, lineHeight: 1.2, '&:hover': { @@ -17,7 +17,7 @@ const useStyles = makeStyles((theme) => ({ paddingTop: 5, paddingBottom: 5, }, - oplistitem: { + opListItem: { lineHeight: 1.2, paddingTop: 5, paddingBottom: 5, @@ -32,94 +32,115 @@ enum NodeType { Leaf, NonLeaf } -type PathListType = Array | Array> - -type PropsType = { - pathList: PathListType, - pathname: string -} - interface PathListLeafNode { - name: string, - type: NodeType - path: string + name: string; + type: NodeType.Leaf; + path: string; } interface PathListNonLeafNode { - name: string, - type: NodeType, - children: Array + name: string; + type: NodeType.NonLeaf; + children: Array; } type PathListNode = PathListLeafNode | PathListNonLeafNode type TypedPathList = Array -function Item (node: PathListNode, padding: number, pathname: string): [React.ReactElement, boolean] { +interface LeafItemProps extends PathListLeafNode { + padding: number; + selected: boolean; +} + +interface NonLeafItemProps extends Omit { + padding: number; + childItems: React.ReactElement[]; + isOpen: boolean; +} + +type PathListType = Array | Array> + +export interface SidebarProps { + pathList: PathListType, + pathname: string +} + +const NonLeafItem: React.FC = (props) => { const classes = useStyles() - const name = node.name + const { name, padding, childItems, isOpen } = props + const [open, setOpen] = useState(isOpen) + + return
+ setOpen(!open)} + className={classes.opListItem} + style={{ paddingLeft: `${padding}px` }} + > + + {name} + + } + /> + {open ? : } + + + {childItems} + +
+} + +const LeafItem: React.FC = (props) => { + const classes = useStyles() + const { name, path, padding, selected } = props + + return + + + {name} + + } + /> + + +} + +function Item(node: PathListNode, padding: number, pathname: string): [React.ReactElement, boolean] { if (node.type === NodeType.Leaf) { - const url = (node as PathListLeafNode).path + const selected = trimTrailingSlash(node.path) === trimTrailingSlash(pathname) return [ - - - - {name} - - } - /> - - , - trimTrailingSlash(url) === trimTrailingSlash(pathname), + , + selected, ] - } - // array - const children = (node as PathListNonLeafNode).children - const listItemsResult = children.map((item) => - Item(item, padding + 16, pathname), - ) + } else { + const children = node.children + let isOpen = false + const items: NonLeafItemProps['childItems'] = [] - let shouldOpen = false - for (const [, i] of listItemsResult) { - shouldOpen = shouldOpen || i - } + children.forEach((v) => { + const [item, selected] = Item(v, padding + 16, pathname) + if (selected) isOpen = selected + items.push(item) + }) - // eslint-disable-next-line - const [open, setOpen] = useState(shouldOpen) - const listItems = listItemsResult.map(([v]) => v) - return [ -
- setOpen(!open)} - className={classes.oplistitem} - style={{ paddingLeft: `${padding}px` }} - > - - {name} - - } - /> - {open ? : } - - - {listItems} - -
, - shouldOpen, - ] + return [ + , + isOpen, + ] + } } -function getTypedPathList (pathList: PathListType): TypedPathList { +function getTypedPathList(pathList: PathListType): TypedPathList { const resArray: TypedPathList = [] for (const i of pathList) { const [[name, a]] = Object.entries(i) @@ -132,11 +153,13 @@ function getTypedPathList (pathList: PathListType): TypedPathList { return resArray } -const Sidebar: React.FC = (props) => { +const Sidebar: React.FC = (props) => { const classes = useStyles() const pathList = props.pathList const typedPathList = getTypedPathList(pathList) - const res = typedPathList.map((item) => Item(item, 16, props.pathname)).map(([x]) => x) + const res = typedPathList + .map((item) => Item(item, 16, props.pathname)) + .map(([x]) => x) return ( {res} @@ -144,5 +167,5 @@ const Sidebar: React.FC = (props) => { ) } -export default React.memo(Sidebar, (prev, next) => prev.pathname === next.pathname) // 只比较 pathname,而不比较 pathList,考虑到当 pathList 不同时,pathname 也一定不同,因此这样比较可以节省计算量 +export default React.memo(Sidebar, (prev, next) => prev.pathname === next.pathname) diff --git a/gatsby-theme-oi-wiki/src/components/SmallScreenMenu.tsx b/gatsby-theme-oi-wiki/src/components/SmallScreenMenu.tsx index 0b264010..13b44b62 100644 --- a/gatsby-theme-oi-wiki/src/components/SmallScreenMenu.tsx +++ b/gatsby-theme-oi-wiki/src/components/SmallScreenMenu.tsx @@ -1,6 +1,6 @@ import { makeStyles } from '@material-ui/core/styles' -import { Menu, MenuItem, IconButton, Hidden, ListItemIcon } from '@material-ui/core' +import { Hidden, IconButton, ListItemIcon, Menu, MenuItem } from '@material-ui/core' import SettingsIcon from '@material-ui/icons/Settings' import LibraryBooksIcon from '@material-ui/icons/LibraryBooks' @@ -19,17 +19,12 @@ const useStyles = makeStyles((theme) => ({ }, })) -const SmallScreenMenu: React.FC = function (props) { - const OIWikiGithub = 'https://github.com/OI-wiki/OI-wiki' - - const classes = useStyles({ - - }) - +const SmallScreenMenu: React.FC = () => { + const classes = useStyles() const [anchorEl, setAnchorEl] = React.useState(null) + const OIWikiGithub = 'https://github.com/OI-wiki/OI-wiki' const handleClick = React.useCallback((event): void => { - // event.stopPropagation(); setAnchorEl(event.currentTarget) }, []) @@ -39,39 +34,44 @@ const SmallScreenMenu: React.FC = function (props) { return ( - handleClick(e)} disableRipple disableFocusRipple> - + handleClick(e)} + disableRipple={true} + disableFocusRipple={true}> + children} TransitionProps={{ timeout: 0 }} classes={{ list: classes.sideMenu }} > - + - 设置 + 设置 - + - 标签 + 标签 - + - 目录 + 目录 + - + - GitHub + GitHub + ) } diff --git a/gatsby-theme-oi-wiki/src/components/StyledLayout.tsx b/gatsby-theme-oi-wiki/src/components/StyledLayout.tsx index 6509017e..5004e1b8 100644 --- a/gatsby-theme-oi-wiki/src/components/StyledLayout.tsx +++ b/gatsby-theme-oi-wiki/src/components/StyledLayout.tsx @@ -106,7 +106,7 @@ const MyLayout: React.FC = (props) => { toc = null, noMeta = false, noComment = false, - noEdit = false, + noEdit = true, noToc = !props.toc?.items, overflow = false, isWIP = false, diff --git a/gatsby-theme-oi-wiki/src/components/Summary.tsx b/gatsby-theme-oi-wiki/src/components/Summary.tsx index 93577b2c..c01baec3 100644 --- a/gatsby-theme-oi-wiki/src/components/Summary.tsx +++ b/gatsby-theme-oi-wiki/src/components/Summary.tsx @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { AccordionSummary } from '@material-ui/core' +import { AccordionSummary, AccordionSummaryProps } from '@material-ui/core' import blue from '@material-ui/core/colors/blue' import { makeStyles } from '@material-ui/core/styles' import EditIcon from '@material-ui/icons/Edit' @@ -34,22 +33,21 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 'bold', }, })) -interface Props{ + +export interface SummaryProps extends AccordionSummaryProps { className: string; - children: string; - [props: string]: any; } -const Summary: React.FC = (props) => { - const { className = null, children } = props +const Summary: React.FC = (props) => { + const { children, ...others } = props const classes = useStyles() return ( } + expandIcon={} aria-controls="expand" - {...props} + {...others} > ({ paddingRight: theme.spacing(2.5), minWidth: '0', '&:hover': { - // eslint-disable-next-line - //@ts-ignore color: theme.palette.tab.colorOnHover, opacity: '1', }, @@ -23,45 +21,43 @@ const useIndicatorStyles = makeStyles(() => ({ }, })) -interface Props{ +interface NavTabsProps { tabID: number; - pathList: any; + pathList: Array | Array>>)>; } -const NavTabs: React.FC = (props) => { +const NavTabs: React.FC = (props) => { const classes = useStyles() const indicatorClasses = useIndicatorStyles() - const { tabID, pathList } = props const newTabs = [] + for (const curTab of pathList.values()) { const curTitle = Object.keys(curTab)[0] - const curLocation = (typeof Object.values(curTab)[0] === 'string') - ? Object.values(curTab)[0] + const values = Object.values(curTab)[0] + const curLocation = (typeof values === 'string') /* - 测试: /test/ */ - : Object.values(Object.values(curTab)[0][0])[0] + ? values /* - 测试: - 测试: /test/ */ - + : Object.values(values[0])[0] newTabs.push({ title: curTitle, link: curLocation }) } - const state = (() => { - return tabID - })() + const [value, setValue] = React.useState(tabID) - const [value, setValue] = React.useState(state) - const handleChange = function (event, newValue) : void { - setValue(newValue) - } return ( - + { + setValue(newValue) + }}> {newTabs.map(({ title, link }) => ( { time: number | Date | string -} & Partial +} -const Time: React.FC = (props) => { +const Time: React.FC = (props) => { const { time, defaultShowRelative, updateInterval } = { ...defaultProps, ...props } const [relativeMode, setRelativeMode] = useState(defaultShowRelative) + const [now, setNow] = useState(Date.now()) const timestamp = +new Date(time) + const toggle = (): void => { setRelativeMode(!relativeMode) } - const [now, setNow] = useState(+(new Date())) + useEffect(() => { - const t = setInterval(() => { setNow(+(new Date())) }, updateInterval) - return () => { clearInterval(t) } + const t = setInterval(() => { + setNow(Date.now()) + }, updateInterval) + + return () => { + clearInterval(t) + } }, [updateInterval]) - return {relativeMode ? timeDifference(+now, timestamp) : new Date(timestamp).toLocaleString()} + + return + {relativeMode ? timeDifference(now, timestamp) : new Date(timestamp).toLocaleString()} + } export default Time diff --git a/gatsby-theme-oi-wiki/src/components/Title.tsx b/gatsby-theme-oi-wiki/src/components/Title.tsx index 72a24531..1a390804 100644 --- a/gatsby-theme-oi-wiki/src/components/Title.tsx +++ b/gatsby-theme-oi-wiki/src/components/Title.tsx @@ -62,7 +62,6 @@ const Title: React.FC = (props) => { } - ) } diff --git a/gatsby-theme-oi-wiki/src/components/doc.js b/gatsby-theme-oi-wiki/src/components/doc.js deleted file mode 100644 index 25922433..00000000 --- a/gatsby-theme-oi-wiki/src/components/doc.js +++ /dev/null @@ -1,105 +0,0 @@ -import MDRenderer from '../lib/MDRenderer' -import React, { useEffect } from 'react' -import Mark from 'mark.js' -import Details from './Details.tsx' -import Summary from './Summary.tsx' -import { SmartLink } from './Link' -import SEO from './Seo' -import clsx from 'clsx' -import StyledLayout from './StyledLayout' - -function Mdx ({ data: { mdx }, pageContext: { lastModified }, location }) { - // console.log(mdx); - // const headingTitle = mdx.headings[0] && mdx.headings[0].value - const title = mdx.slug === '/' ? null : mdx.frontmatter.title - const description = mdx.frontmatter.description || mdx.excerpt - const authors = mdx.frontmatter.author || null - const tags = mdx.frontmatter.tags || null - const noMeta = mdx.frontmatter.noMeta === 'true' || false - const noComment = mdx.frontmatter.noComment === 'true' || false - const noEdit = mdx.frontmatter.noEdit === 'true' || false - const toc = mdx.toc || null - const relativePath = mdx.parent.relativePath || '' - const modifiedTime = lastModified || '' - const wordCount = mdx.wordCount.words || 0 - const datePublished = mdx.parent.birthTime || '' - const dateModified = mdx.parent.changeTime || '' - const isIndex = mdx.fields.isIndex - - const highlightNode = (tagName, isHighlight) => { - const mainNodes = document.getElementsByTagName('main') - const nodes = mainNodes[0].getElementsByTagName(tagName) - const children = [...nodes] - children.forEach((node) => { - const instance = new Mark(node) - instance[isHighlight ? 'mark' : 'unmark']( - location.state.searchKey, - { - exclude: ['span'], - }) - }) - } - useEffect(() => { - if (location?.state?.searchKey) { - highlightNode('h1', true) - highlightNode('h2', true) - highlightNode('h3', true) - highlightNode('p', true) - setTimeout( - () => { - highlightNode('h1', false) - highlightNode('h2', false) - highlightNode('h3', false) - highlightNode('p', false) - }, 5000) - } - }, []) - - function LinkGetter () { - return function TooltipLink (props) { - return - } - } - - function InlineCode ({ className, children, ...props }) { - return {children} - } - - const myComponents = { - details: Details, - summary: Summary, - a: LinkGetter(), - inlineCode: InlineCode, - inlinecode: InlineCode, - } - - const isWIP = wordCount === 0 || (tags?.findIndex(x => x === 'WIP') >= 0) - return ( - - - - - ) -} - -export default Mdx diff --git a/gatsby-theme-oi-wiki/src/lib/MDRenderer.js b/gatsby-theme-oi-wiki/src/lib/MDRenderer.js deleted file mode 100644 index 5a3ab03b..00000000 --- a/gatsby-theme-oi-wiki/src/lib/MDRenderer.js +++ /dev/null @@ -1,8 +0,0 @@ -import reactiveHast from './reactive-hast' -import React from 'react' - -function MDRenderer ({ components, htmlAst }) { - return reactiveHast({ ...htmlAst, tagName: 'div' }, components ?? {}) -} - -export default React.memo(MDRenderer, () => true) diff --git a/gatsby-theme-oi-wiki/src/lib/MDRenderer.tsx b/gatsby-theme-oi-wiki/src/lib/MDRenderer.tsx new file mode 100644 index 00000000..c7ae0610 --- /dev/null +++ b/gatsby-theme-oi-wiki/src/lib/MDRenderer.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import reactiveHast, { ReactiveHastComponents, ReactiveHastHtmlAst } from './reactiveHast' + +export interface MDRendererProps { + components: ReactiveHastComponents; + htmlAst: ReactiveHastHtmlAst; +} + +const MDRenderer: React.FC = (props) => { + const { components, htmlAst } = props + return reactiveHast({ + ...htmlAst, + tagName: 'div', + }, components ?? {}) as ReturnType +} + +export default React.memo(MDRenderer, () => true) diff --git a/gatsby-theme-oi-wiki/src/lib/reactive-hast.js b/gatsby-theme-oi-wiki/src/lib/reactive-hast.js deleted file mode 100644 index 66a8eede..00000000 --- a/gatsby-theme-oi-wiki/src/lib/reactive-hast.js +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react' - -// modified on https://github.com/pveyes/htmr - -const attributeMap = { - for: 'htmlFor', - class: 'className', - acceptcharset: 'acceptCharset', - accesskey: 'accessKey', - allowfullscreen: 'allowFullScreen', - allowtransparency: 'allowTransparency', - autocomplete: 'autoComplete', - autofocus: 'autoFocus', - autoplay: 'autoPlay', - cellpadding: 'cellPadding', - cellspacing: 'cellSpacing', - charset: 'charSet', - classid: 'classID', - classname: 'className', - colspan: 'colSpan', - contenteditable: 'contentEditable', - contextmenu: 'contextMenu', - crossorigin: 'crossOrigin', - datetime: 'dateTime', - enctype: 'encType', - formaction: 'formAction', - formenctype: 'formEncType', - formmethod: 'formMethod', - formnovalidate: 'formNoValidate', - formtarget: 'formTarget', - frameborder: 'frameBorder', - hreflang: 'hrefLang', - htmlfor: 'htmlFor', - httpequiv: 'httpEquiv', - inputmode: 'inputMode', - keyparams: 'keyParams', - keytype: 'keyType', - marginheight: 'marginHeight', - marginwidth: 'marginWidth', - maxlength: 'maxLength', - mediagroup: 'mediaGroup', - minlength: 'minLength', - novalidate: 'noValidate', - radiogroup: 'radioGroup', - readonly: 'readOnly', - rowspan: 'rowSpan', - spellcheck: 'spellCheck', - srcdoc: 'srcDoc', - srclang: 'srcLang', - srcset: 'srcSet', - tabindex: 'tabIndex', - usemap: 'useMap', - viewbox: 'viewBox', -} - -/** - * convert camel case to hypen - * - * example: - * - colorProfile -> color-profile - * - * @param {*} str - * @return {*} - */ -function camelCaseToHypen (str) { - return str.replace(/[A-Z]/g, (match) => { - return '-' + match.toLowerCase() - }) -} - -/** - * convert hypen and colon to camel case - * - * example: - * - color-profile -> colorProfile - * - xlink:role -> xlinkRole - * - * @param {*} str - * @return {*} - */ -function hypenColonToCamelCase (str) { - return str.replace(/(-|:)(.)/g, (match, symbol, char) => { - return char.toUpperCase() - }) -} - -export function convertStyle (styleStr) { - function convertValue (value) { - // value can be converted to pixel automatically by converting it to number - if (/^\d+$/.test(value)) { - return Number(value) - } - - return value.replace(/'/g, '"') - } - - function convertProperty (prop) { - if (/^-ms-/.test(prop)) { - // eslint-disable-next-line no-param-reassign - prop = prop.substr(1) - } - - return hypenColonToCamelCase(prop) - } - - const style = {} - - styleStr - .split(';') - .filter(style => { - const declaration = style.trim() - return declaration !== '' - }) - .forEach(declaration => { - const rules = declaration.split(':') - if (rules.length > 1) { - const prop = convertProperty(rules[0].trim()) - // To handle url: attribute on style - const val = convertValue( - rules - .slice(1) - .join(':') - .trim(), - ) - style[prop] = val - } - }) - - return style -} - -function mapAttribute (tagName, attrs = {}, preserveAttributes) { - return Object.keys(attrs).reduce((result, attr) => { - // ignore inline event attribute - if (/^on.*/.test(attr)) { - return result - } - - // Allow preserving non-standard attribute, e.g: `ng-if` - function isPreserved (att) { - return !!preserveAttributes.filter(at => { - if (at instanceof RegExp) { - return at.test(att) - } - return at === att - }).length - } - - // Convert attribute to camelCase except data-* and aria-* attribute - // https://facebook.github.io/react/docs/dom-elements.html - let attributeName = attr - if (!/^(data|aria)-/.test(attr)) { - if (isPreserved(attr) === false) { - attributeName = hypenColonToCamelCase(attr) - } - } - - // Convert camelCase data and aria attribute to hypen case - if (/^(data|aria)[A-Z]/.test(attr)) { - if (isPreserved(attr) === false) { - attributeName = camelCaseToHypen(attr) - } - } - - const name = attributeMap[attributeName] || attributeName - if (name === 'style') { - // if there's an attribute called style, this means that the value must be exists - // even if it's an empty string - result[name] = convertStyle(attrs.style) - } else if (name === 'className') { - // React treats MathJAX elements as HTML custom tags - // Thus its className will keep as is in the final DOM. - // Rename it to `class` here - const attrName = tagName.startsWith('mjx-') ? 'class' : 'className' - result[attrName] = attrs.className.join(' ') - } else { - const value = attrs[attr] - // Convert attribute value to boolean attribute if needed - // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes - const isBooleanAttribute = value === '' || String(value).toLowerCase() === attributeName.toLowerCase() - result[name] = isBooleanAttribute ? true : value - } - - return result - }, {}) -} - -const dangerouslySetChildren = ['style'] - -export default function reactiveHast (ast, components, index = 1) { - if (ast.type === 'text') { - // const val = ast.value.replace(/\\/g,""); - return ast.value - } - - if (ast.type === 'comment') { - return <> - } - - const props = { key: index.toString(), ...mapAttribute(ast.tagName, ast.properties, []) } - - const tag = components[ast.tagName] ?? ast.tagName - - if (dangerouslySetChildren.indexOf(ast.tagName) > -1) { - const childNode = ast.children[0] - props.dangerouslySetInnerHTML = { - __html: childNode.value.trim(), - } - return React.createElement(tag, props, null) - } - - const children = ast.children.map((el, i) => reactiveHast(el, components, i)) - - return React.createElement(tag, props, ...children.length ? [children] : []) -} diff --git a/gatsby-theme-oi-wiki/src/lib/reactiveHast.ts b/gatsby-theme-oi-wiki/src/lib/reactiveHast.ts new file mode 100644 index 00000000..cfe87d27 --- /dev/null +++ b/gatsby-theme-oi-wiki/src/lib/reactiveHast.ts @@ -0,0 +1,203 @@ +import React from 'react' +import { StrIndexObj } from '../types/common' + +// modified on https://github.com/pveyes/htmr + +const attributeMap = { + for: 'htmlFor', + class: 'className', + acceptcharset: 'acceptCharset', + accesskey: 'accessKey', + allowfullscreen: 'allowFullScreen', + allowtransparency: 'allowTransparency', + autocomplete: 'autoComplete', + autofocus: 'autoFocus', + autoplay: 'autoPlay', + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing', + charset: 'charSet', + classid: 'classID', + classname: 'className', + colspan: 'colSpan', + contenteditable: 'contentEditable', + contextmenu: 'contextMenu', + crossorigin: 'crossOrigin', + datetime: 'dateTime', + enctype: 'encType', + formaction: 'formAction', + formenctype: 'formEncType', + formmethod: 'formMethod', + formnovalidate: 'formNoValidate', + formtarget: 'formTarget', + frameborder: 'frameBorder', + hreflang: 'hrefLang', + htmlfor: 'htmlFor', + httpequiv: 'httpEquiv', + inputmode: 'inputMode', + keyparams: 'keyParams', + keytype: 'keyType', + marginheight: 'marginHeight', + marginwidth: 'marginWidth', + maxlength: 'maxLength', + mediagroup: 'mediaGroup', + minlength: 'minLength', + novalidate: 'noValidate', + radiogroup: 'radioGroup', + readonly: 'readOnly', + rowspan: 'rowSpan', + spellcheck: 'spellCheck', + srcdoc: 'srcDoc', + srclang: 'srcLang', + srcset: 'srcSet', + tabindex: 'tabIndex', + usemap: 'useMap', + viewbox: 'viewBox', +} + +/** + * convert camel case to kebab case + * + * example: + * - colorProfile -> color-profile + */ +function camelCase2KebabCase(str: string): string { + return str.replace(/[A-Z]/g, (match) => '-' + match.toLowerCase()) +} + +/** + * convert kebab and colon to camel case + * + * example: + * - color-profile -> colorProfile + * - xlink:role -> xlinkRole + */ +function kebabColon2CamelCase(str: string): string { + return str.replace(/([-:])(.)/g, (_match, _symbol, char) => { + return char.toUpperCase() + }) +} + +type StyleObj = StrIndexObj + +export function convertStyle(styleStr: string): StyleObj { + function convertValue(value: string): number | string { + // value can be converted to pixel automatically by converting it to number + if (/^\d+$/.test(value)) { + return Number(value) + } else { + return value.replace(/'/g, '"') + } + } + + function convertProperty(prop: string): string { + if (/^-ms-/.test(prop)) { + prop = prop.substring(1) + } + return kebabColon2CamelCase(prop) + } + + const style: StyleObj = {} + + styleStr + .split(';') + .filter(style => style.trim() !== '') + .forEach(declaration => { + const rules = declaration.split(':') + if (rules.length > 1) { + const prop = convertProperty(rules[0].trim()) + // To handle url: attribute on style + style[prop] = convertValue(rules.slice(1).join(':').trim()) + } + }) + + return style +} + +type Attrs = + StrIndexObj + | StyleObj + | StrIndexObj + | StrIndexObj & { dangerouslySetInnerHTML: StrIndexObj } + +function mapAttribute(tagName: string, attrs: Attrs | StrIndexObj = {}, preserveAttributes: Array): Attrs { + // Allow preserving non-standard attribute, e.g: `ng-if` + const isPreserved = function (att: string): boolean { + return preserveAttributes.filter(at => at instanceof RegExp ? at.test(att) : at === att).length > 0 + } + + return Object.keys(attrs).reduce((result, attr) => { + // ignore inline event attribute + if (!/^on.*/.test(attr)) { + let attributeName = attr + + if (!isPreserved(attr)) { + // Convert attribute to camelCase except data-* and aria-* attribute + // https://facebook.github.io/react/docs/dom-elements.html + if (!/^(data|aria)-/.test(attr)) { + attributeName = kebabColon2CamelCase(attr) + } + // Convert camelCase data and aria attribute to kebab case + if (/^(data|aria)[A-Z]/.test(attr)) { + attributeName = camelCase2KebabCase(attr) + } + } + + const name = attributeMap[attributeName as keyof typeof attributeMap] || attributeName + if (name === 'style') { + // if there's an attribute called style, this means that the value must be exists + // even if it's an empty string + result[name] = convertStyle(attrs.style as string) + } else if (name === 'className') { + // React treats MathJAX elements as HTML custom tags + // Thus its className will keep as is in the final DOM. + // Rename it to `class` here + const attrName = tagName.startsWith('mjx-') ? 'class' : 'className' + result[attrName] = (attrs.className as string[]).join(' ') + } else { + // Convert attribute value to boolean attribute if needed + // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes + const value = attrs[attr] + const isBooleanAttribute = value === '' || String(value).toLowerCase() === attributeName.toLowerCase() + result[name] = isBooleanAttribute ? 'true' : value as string | number + } + } + + return result + }, {} as Attrs) +} + +export type ReactiveHastComponents = StrIndexObj>; +export type ReactiveHastHtmlAst = { + tagName: string; + type: string; + value: ReturnType | string; + properties: Attrs; + children?: (Omit & { value: string })[] +} + +const dangerouslySetChildren = ['style'] + +function reactiveHast(ast: ReactiveHastHtmlAst, components: ReactiveHastComponents, index = 1, needKey = false): ReactiveHastHtmlAst['value'] { + if (ast.type === 'text') { + return ast.value + } else if (ast.type === 'comment') { + return React.createElement(React.Fragment, null) + } else { + const props = mapAttribute(ast.tagName, ast.properties, []) + const tag = components[ast.tagName] ?? ast.tagName + + if (dangerouslySetChildren.indexOf(ast.tagName) > -1) { + const childNode = ast.children?.[0]; + (props as any).dangerouslySetInnerHTML = { + __html: childNode?.value.trim(), + } + return React.createElement(tag, props, null) + } else { + if (needKey && !props['key']) props['key'] = `${ast.tagName}-${index.toString()}` + const children = ast.children?.map((el, i) => reactiveHast(el, components, i, true)) + return React.createElement(tag, props, ...children?.length ? [children] : []) + } + } +} + +export default reactiveHast diff --git a/gatsby-theme-oi-wiki/src/lib/trailingSlash.ts b/gatsby-theme-oi-wiki/src/lib/trailingSlash.ts index 7d614d71..8df9c5bd 100644 --- a/gatsby-theme-oi-wiki/src/lib/trailingSlash.ts +++ b/gatsby-theme-oi-wiki/src/lib/trailingSlash.ts @@ -1,3 +1,3 @@ -export default function trimTrailingSlash (str?: string): string { - return str?.replace(/\/$/, '') +export default function trimTrailingSlash(str?: string): string { + return str?.replace(/\/$/, '') ?? '' } diff --git a/gatsby-theme-oi-wiki/src/lib/useSetting.tsx b/gatsby-theme-oi-wiki/src/lib/useSetting.ts similarity index 83% rename from gatsby-theme-oi-wiki/src/lib/useSetting.tsx rename to gatsby-theme-oi-wiki/src/lib/useSetting.ts index bf4e2d48..e7218dcd 100644 --- a/gatsby-theme-oi-wiki/src/lib/useSetting.tsx +++ b/gatsby-theme-oi-wiki/src/lib/useSetting.ts @@ -11,8 +11,10 @@ export interface Settings { smoothScroll: boolean; } theme: { - primary: LabeledPaletteColor|null; // null: auto - secondary: string; // id + /** null: auto */ + primary: LabeledPaletteColor | null; + /** id */ + secondary: string; fallbackMonoFont: boolean } } @@ -35,17 +37,15 @@ const defaultSettings: Settings = { // https://stackoverflow.com/questions/41980195/recursive-partialt-in-typescript // 第一个回答会让 linter unhappy, 所以魔改了一下 type RecursivePartial = { - [P in keyof T]?: - T[P] extends (infer U)[] ? RecursivePartial[] : RecursivePartial + [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] : RecursivePartial }; export const useSetting = (): [Settings, (s: RecursivePartial) => void] => { const [settings, setSettings] = usePersistedState('settings')(defaultSettings) const updateSetting = (newSettings: RecursivePartial): void => { - const finalSettings = merge.all([defaultSettings, settings, newSettings]) + const finalSettings = merge.all([defaultSettings, settings, newSettings]) as Settings setSettings(finalSettings) - // eslint-disable-next-line dot-notation window !== undefined && window['onthemechange'](finalSettings) } return [settings, updateSetting] diff --git a/gatsby-theme-oi-wiki/src/pages/404.tsx b/gatsby-theme-oi-wiki/src/pages/404.tsx index 3a05575f..d46d5c6e 100644 --- a/gatsby-theme-oi-wiki/src/pages/404.tsx +++ b/gatsby-theme-oi-wiki/src/pages/404.tsx @@ -1,4 +1,3 @@ -// Components import React from 'react' import StyledLayout from '../components/StyledLayout' diff --git a/gatsby-theme-oi-wiki/src/pages/pages.js b/gatsby-theme-oi-wiki/src/pages/pages.tsx similarity index 50% rename from gatsby-theme-oi-wiki/src/pages/pages.js rename to gatsby-theme-oi-wiki/src/pages/pages.tsx index d536f813..0ff982a3 100644 --- a/gatsby-theme-oi-wiki/src/pages/pages.js +++ b/gatsby-theme-oi-wiki/src/pages/pages.tsx @@ -1,23 +1,34 @@ import { Card, CardActions, CardContent, Grid, TextField, Typography, useMediaQuery } from '@material-ui/core' - import { useTheme } from '@material-ui/core/styles' import Autocomplete from '@material-ui/lab/Autocomplete' import match from 'autosuggest-highlight/match' import parse from 'autosuggest-highlight/parse' import times from 'lodash/times' -import { graphql } from 'gatsby' +import { graphql, useStaticQuery } from 'gatsby' import React, { useState } from 'react' import { SmartLink } from '../components/Link' -import Tags from '../components/Tags.tsx' +import Tags from '../components/Tags' import StyledLayout from '../components/StyledLayout' +import { DeepRequiredNonNull, DeepWriteable } from '../types/common' + +interface PageItemInfo { + id: string; + frontmatter: Pick; + fields: Pick; +} -function PageItem (props) { +interface PageItemProps extends DeepWriteable> { + linkComponent: React.ElementType +} + +const PageItem: React.FC = (props) => { const { id, frontmatter: { title, tags }, fields: { slug: link }, } = props + return ( @@ -34,39 +45,47 @@ function PageItem (props) { ) } -function matchTags (pageTags, selectedTags) { +function matchTags(pageTags: string[], selectedTags: string[]): boolean { if (selectedTags.length === 0) return true - if (!pageTags) return false - const matchTag = (tags, selected) => { - return tags.includes(selected) - } - const res = selectedTags.map((selected) => matchTag(pageTags, selected)) - return res.every((v) => v === true) + else if (!pageTags) return false + else return selectedTags.every((v) => pageTags.includes(v)) } -function Column ({ items, linkComponent }) { - return ( - - {items.map(x => )} - - ) +interface ColumnProps { + items: Omit[]; + linkComponent: PageItemProps['linkComponent']; +} + +const Column: React.FC = ({ items, linkComponent }) => ( + + {items.map(x => )} + +) + +interface GridItemsProps { + filteredItems: ColumnProps['items']; + linkComponent: PageItemProps['linkComponent']; } -function GridItems (props) { +const GridItems: React.FC = (props) => { const theme = useTheme() const { filteredItems } = props - const upXL = useMediaQuery(theme.breakpoints.up('xl')) - const upLG = useMediaQuery(theme.breakpoints.up('lg')) - const upMD = useMediaQuery(theme.breakpoints.up('md')) - const upSmall = useMediaQuery(theme.breakpoints.up('sm')) - const columnCount = upXL ? 5 : upLG ? 4 : upMD ? 3 : upSmall ? 2 : 1 + const upStatus = theme.breakpoints.up + const upList = [ + useMediaQuery(upStatus('xl')), + useMediaQuery(upStatus('lg')), + useMediaQuery(upStatus('md')), + useMediaQuery(upStatus('sm')), + true, + ] + const columnCount = 5 - upList.indexOf(true) return ( <> {times(columnCount).map(i => idx % columnCount === i)} + items={filteredItems.filter((_, idx) => idx % columnCount === i)} linkComponent={props.linkComponent} />, )} @@ -74,19 +93,38 @@ function GridItems (props) { ) } -function BlogIndex (props) { - const { - location, - data: { - allMarkdownRemark: { edges, group }, - }, - } = props +export interface BlogIndexProps { + location: Location; +} + +const BlogIndex: React.FC = (props) => { + const { allMarkdownRemark: { edges, group } } = useStaticQuery(graphql` + query BlogIndex { + allMarkdownRemark { + edges { + node { + id + frontmatter { + title + tags + } + fields { + slug + } + } + } + group(field: frontmatter___tags) { + fieldValue + totalCount + } + } + }`) as DeepWriteable> + + const { location } = props const articles = edges.map(x => x.node) const tags = group.map(({ fieldValue }) => fieldValue) - const [selectedTags, setSelectedTags] = useState([]) - const filteredItems = articles - .map((x) => matchTags(x.frontmatter.tags, selectedTags) && x) - .filter((x) => x !== false) + const [selectedTags, setSelectedTags] = useState([]) + const filteredItems = articles.filter((v) => !!v.frontmatter.title && matchTags(v.frontmatter.tags, selectedTags)) return ( - + { setSelectedTags(v) }} - multiple + multiple={true} + disableCloseOnSelect={true} + filterSelectedOptions={true} options={tags} - disableCloseOnSelect - filterSelectedOptions getOptionLabel={(option) => option} renderOption={(option, { inputValue }) => { const matches = match(option, inputValue) @@ -131,7 +169,7 @@ function BlogIndex (props) { /> - + @@ -140,26 +178,4 @@ function BlogIndex (props) { ) } -export const pageQuery = graphql` - query blogIndex { - allMarkdownRemark { - edges { - node { - id - frontmatter { - title - tags - } - fields { - slug - } - } - } - group(field: frontmatter___tags) { - fieldValue - totalCount - } - } - } -` export default BlogIndex diff --git a/gatsby-theme-oi-wiki/src/pages/settings.tsx b/gatsby-theme-oi-wiki/src/pages/settings.tsx index dfb83cf6..847ee5fa 100644 --- a/gatsby-theme-oi-wiki/src/pages/settings.tsx +++ b/gatsby-theme-oi-wiki/src/pages/settings.tsx @@ -11,12 +11,12 @@ import { } from '@material-ui/core' import React from 'react' import colors, { LabeledPaletteColor } from '../styles/colors' -import { useSetting } from '../lib/useSetting' +import { Settings, useSetting } from '../lib/useSetting' import StyledLayout from '../components/StyledLayout' const useStyles = makeStyles((theme) => ({ - root: (props: LabeledPaletteColor) => ({ - background: props ? props.main : undefined, + root: (props: LabeledPaletteColor | Record) => ({ + background: props?.main, color: props ? props.contrastText : theme.palette.background.default, margin: '1em 1.2em 1em 0', padding: 0, @@ -25,7 +25,7 @@ const useStyles = makeStyles((theme) => ({ border: '.1em solid', borderColor: theme.palette.divider, '&:hover': { - background: props ? props.dark : undefined, + background: props?.dark, }, }), label: { @@ -39,18 +39,19 @@ const useStyles = makeStyles((theme) => ({ }, })) -interface ColorButtonProp { +interface ColorButtonProps { data: LabeledPaletteColor - onClick?: (props: LabeledPaletteColor) => any + onClick?: (props: LabeledPaletteColor) => void } -const ColorButton: React.FC = (props: ColorButtonProp) => { - const classes = useStyles(props.data.main === 'auto' ? undefined : props.data) +const ColorButton: React.FC = (props) => { + const { data, onClick } = props + const classes = useStyles(data.main === 'auto' ? {} : data) return ( @@ -65,7 +66,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => const { location } = props const [settings, updateSetting] = useSetting() - const onNavColorBtnClick = (c: LabeledPaletteColor) => { + const onNavColorBtnClick = (c: LabeledPaletteColor): void => { updateSetting({ theme: { primary: c.main === 'auto' ? null : c, @@ -73,7 +74,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => }) } - const onSecondaryColorBtnClick = (c: LabeledPaletteColor) => { + const onSecondaryColorBtnClick = (c: LabeledPaletteColor): void => { if (c.main === 'auto') throw new Error('invalid color') updateSetting({ theme: { @@ -100,7 +101,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => onChange={(e) => { updateSetting({ darkMode: { - type: (e.target.value as unknown as ('user-preference' | 'always-on' | 'always-off')), + type: (e.target.value as Settings['darkMode']['type']), }, }) }} @@ -118,6 +119,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => { updateSetting({ animation: { @@ -125,7 +127,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => }, }) } - } name="animation-smooth-scroll" + } /> } label="使用平滑滚动" @@ -140,6 +142,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => { updateSetting({ theme: { @@ -147,7 +150,7 @@ const SettingsPage: React.FC = (props: SettingsPageProps) => }, }) } - } name="monofont" + } /> } label="使用浏览器默认字体" diff --git a/gatsby-theme-oi-wiki/src/pages/tags.js b/gatsby-theme-oi-wiki/src/pages/tags.js deleted file mode 100644 index b6a38834..00000000 --- a/gatsby-theme-oi-wiki/src/pages/tags.js +++ /dev/null @@ -1,85 +0,0 @@ -import Chip from '@material-ui/core/Chip' -import { makeStyles } from '@material-ui/core/styles' -import { graphql } from 'gatsby' -import kebabCase from 'lodash/kebabCase' -import PropTypes from 'prop-types' -import React from 'react' -import StyledLayout from '../components/StyledLayout' - -const useStyles = makeStyles((theme) => ({ - chip: { - margin: theme.spacing(0.5), - }, -})) - -function sortTags (a, b) { - return b.totalCount - a.totalCount -} - -const TagsPage = ({ - data: { - allMarkdownRemark: { group }, - site: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - siteMetadata: { title }, - }, - }, - location, -}) => { - group.sort(sortTags) - // console.log(group) - const classes = useStyles() - return ( - -
- {group.map((tag) => ( - - ))} -
-
- ) -} - -TagsPage.propTypes = { - data: PropTypes.shape({ - allMarkdownRemark: PropTypes.shape({ - group: PropTypes.arrayOf( - PropTypes.shape({ - fieldValue: PropTypes.string.isRequired, - totalCount: PropTypes.number.isRequired, - }).isRequired, - ), - }), - site: PropTypes.shape({ - siteMetadata: PropTypes.shape({ - title: PropTypes.string.isRequired, - }), - }), - }), -} - -export default TagsPage - -export const pageQuery = graphql` - query { - site { - siteMetadata { - title - } - } - allMarkdownRemark(limit: 2000) { - group(field: frontmatter___tags) { - fieldValue - totalCount - } - } - } -` diff --git a/gatsby-theme-oi-wiki/src/pages/tags.tsx b/gatsby-theme-oi-wiki/src/pages/tags.tsx new file mode 100644 index 00000000..c72eb405 --- /dev/null +++ b/gatsby-theme-oi-wiki/src/pages/tags.tsx @@ -0,0 +1,62 @@ +import Chip from '@material-ui/core/Chip' +import { makeStyles } from '@material-ui/core/styles' +import { graphql, useStaticQuery } from 'gatsby' +import kebabCase from 'lodash/kebabCase' +import React from 'react' +import StyledLayout from '../components/StyledLayout' +import { DeepRequiredNonNull, DeepWriteable } from '../types/common' + +const useStyles = makeStyles((theme) => ({ + chip: { + margin: theme.spacing(0.5), + }, +})) + +export interface TagsPageProps { + location: Location +} + +const TagsPage: React.FC = (props) => { + const { + allMarkdownRemark: { group }, + } = useStaticQuery(graphql` + query Tags { + site { + siteMetadata { + title + } + } + allMarkdownRemark(limit: 2000) { + group(field: frontmatter___tags) { + fieldValue + totalCount + } + } + } + `) as DeepWriteable> + + const classes = useStyles() + const { location } = props + + group.sort((a, b) => b.totalCount - a.totalCount) + + return ( + +
+ {group.map((tag) => ( + + ))} +
+
+ ) +} + +export default TagsPage diff --git a/gatsby-theme-oi-wiki/src/templates/doc.js b/gatsby-theme-oi-wiki/src/templates/doc.js index a37636e4..4df5b4c0 100644 --- a/gatsby-theme-oi-wiki/src/templates/doc.js +++ b/gatsby-theme-oi-wiki/src/templates/doc.js @@ -1,14 +1,13 @@ import { graphql } from 'gatsby' import React from 'react' -import Doc from '../components/doc' +import Mdx from '../components/Mdx' -export default function MdxDoc (props) { - // console.log(data) - return +export default function MdxDoc({ data, location }) { + return } export const query = graphql` - query($id: String!) { + query Doc($id: String!) { mdx: markdownRemark(id: { eq: $id }) { id wordCount { diff --git a/gatsby-theme-oi-wiki/src/types/common.ts b/gatsby-theme-oi-wiki/src/types/common.ts index f7f9dfd8..494ed2b1 100644 --- a/gatsby-theme-oi-wiki/src/types/common.ts +++ b/gatsby-theme-oi-wiki/src/types/common.ts @@ -6,3 +6,9 @@ export type OnClickHandler = ((e: React.MouseEvent) => export type RequiredNonNull = { [P in keyof T]-?: NonNullable; } +export type DeepRequiredNonNull = { + [P in keyof T]-?: DeepRequiredNonNull>; +} +export type DeepWriteable = { + -readonly [P in keyof T]: DeepWriteable +}; diff --git a/yarn.lock b/yarn.lock index d7b9f310..486c5f53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,6 +2070,11 @@ resolved "https://registry.yarnpkg.com/@turist/time/-/time-0.0.1.tgz#57637d2a7d1860adb9f9cecbdcc966ce4f551d63" integrity sha512-M2BiThcbxMxSKX8W4z5u9jKZn6datnM3+FpEU+eYw0//l31E2xhqi7vTAuJ/Sf0P3yhp66SDJgPu3bRRpvrdQQ== +"@types/autosuggest-highlight@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/autosuggest-highlight/-/autosuggest-highlight-3.1.1.tgz#afdec910b84c06f77e9a7ed1a47ffeade4c0da6d" + integrity sha512-onPUHT+32TyI7ctiG7G/JtWlVkEBH2bw1onOWaea0L0dZmKNhP+KIOcrh7gnr2LTzD9/cjQlC7zJdv259dg0lg== + "@types/cacheable-request@^6.0.1": version "6.0.2" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" @@ -2206,6 +2211,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/jquery@*": + version "3.5.6" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0" + integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg== + dependencies: + "@types/sizzle" "*" + "@types/json-patch@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/json-patch/-/json-patch-0.0.30.tgz#7c562173216c50529e70126ceb8e7a533f865e9b" @@ -2228,6 +2240,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b" integrity sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg== +"@types/mark.js@^8.11.6": + version "8.11.6" + resolved "https://registry.yarnpkg.com/@types/mark.js/-/mark.js-8.11.6.tgz#28406c3aafc5be277de1d75840da6246172b180a" + integrity sha512-mhe0gRtOd0UmsnEgO862RTVF4761fbAB5CjdFLwzN7YAhdhefbH25KNQREa++N/L6g4YotygavvHT3UO/0xgnQ== + dependencies: + "@types/jquery" "*" + "@types/mathjax@^0.0.36": version "0.0.36" resolved "https://registry.yarnpkg.com/@types/mathjax/-/mathjax-0.0.36.tgz#18cf766f88ac0cd4e7ee8282b1286049bb6aa682" @@ -2358,6 +2377,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/sizzle@*": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + "@types/tmp@^0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.0.33.tgz#1073c4bc824754ae3d10cfab88ab0237ba964e4d" @@ -2368,6 +2392,13 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.5.tgz#fdd299f23205c3455af88ce618dd65c14cb73e22" integrity sha512-wnra4Vw9dopnuybR6HBywJ/URYpYrKLoepBTEtgfJup8Ahoi2zJECPP2cwiXp7btTvOT2CULv87aQRA4eZSP6g== +"@types/use-persisted-state@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@types/use-persisted-state/-/use-persisted-state-0.3.0.tgz#23370e89bd6c8468afdb05dde2d1bab898128229" + integrity sha512-ZT98QuckR95qM7W97lGVqc7fFS9TT6f3txp7R40fl0zxa5BLm3GG7j0i51G12h8DkoJxFAf2oQyYKU99h0pxFA== + dependencies: + "@types/react" "*" + "@types/vfile-message@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -13197,6 +13228,11 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +schema-dts@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-0.9.0.tgz#23d9f7111774ba253b6dfda0cea47a2b323a647d" + integrity sha512-awQ5s+OBV7w7GcvoZvG4F/IaZQWqniOmN1L5PL99dxKLIpjYiqWB8za+LIvuZ4S69oqfzRrpJhJk8Vr4mGEEQA== + schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"