diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
new file mode 100644
index 00000000..c6fee294
--- /dev/null
+++ b/.github/workflows/web.yml
@@ -0,0 +1,27 @@
+name: Web CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Use Node.js 18
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ cache: npm
+ cache-dependency-path: '**/package-lock.json'
+ - run: npm ci
+ working-directory: web
+ - run: npm run lint
+ working-directory: web
+ - run: npm run build --if-present
+ working-directory: web
+ - run: npm run e2e:headless
+ working-directory: web
diff --git a/web/.prettierrc.js b/.prettierrc.cjs
similarity index 97%
rename from web/.prettierrc.js
rename to .prettierrc.cjs
index 4f412a82..d3bc6afd 100644
--- a/web/.prettierrc.js
+++ b/.prettierrc.cjs
@@ -4,4 +4,4 @@ module.exports = {
singleQuote: true,
tabWidth: 2,
useTabs: false,
-};
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 42877cbf..43b002ca 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,16 @@ _Patches submitted in issues, email, or elsewhere may be ignored._
## Coding Style
-Code should be formatted using `clang-format`. By configuring Conky with `cmake -DCHECK_CODE_QUALITY=ON`, you will be able to run `make clang-format` to automatically format code. If code in your PR is not formatted according to [`.clang-format`](.clang-format), the checks will not pass.
+Code should be formatted using `clang-format`. By configuring Conky with `cmake -DCHECK_CODE_QUALITY=ON`, you will be able to run `make clang-format` to automatically format code.
+
+## Git hooks
+
+To make life easier, you can use
+[lefthook](https://github.com/evilmartians/lefthook) to handle some basic
+linting with a pre-commit hook, as defined in [lefthook.yml](lefthook.yml).
+Follow the [installation guide for
+lefhook](https://github.com/evilmartians/lefthook/blob/master/docs/install.md),
+then run `lefthook install` to enable the hooks.
## Unit Testing
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 00000000..5e43d592
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,19 @@
+pre-commit:
+ commands:
+ web-linter:
+ parallel: true
+ run: |
+ npx eslint --fix {staged_files} \
+ && git add {staged_files}
+ root: web/
+ glob: '*.{ts,tsx,js,jsx}'
+ cpp-linter:
+ run: |
+ clang-format -i {staged_files} \
+ && git add {staged_files}
+ glob: '*.{c,cc,cxx,h,cpp}'
+ misc-linter:
+ run: |
+ npx prettier --write {staged_files} \
+ && git add {staged_files}
+ glob: '*.{md,json,yml,yaml}'
diff --git a/web/.eslintrc.json b/web/.eslintrc.json
index 852747d8..bf62089a 100644
--- a/web/.eslintrc.json
+++ b/web/.eslintrc.json
@@ -1,7 +1,16 @@
{
- "extends": ["next/core-web-vitals", "prettier"],
- "settings": {
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "next/core-web-vitals",
+ "prettier"
+ ],
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "root": true,
+ "settings": {
"mdx/code-blocks": true,
- "mdx/language-mapper": {}
+ "mdx/language-mapper": {},
+ "sort-imports": "error"
}
}
diff --git a/web/.gitignore b/web/.gitignore
index 8e8eb7eb..88fa91b8 100644
--- a/web/.gitignore
+++ b/web/.gitignore
@@ -39,3 +39,6 @@ yarn-error.log*
# IDEs
.vscode/
+
+# Cypress videos
+cypress/videos
diff --git a/web/components/CustomLink.tsx b/web/components/CustomLink.tsx
deleted file mode 100644
index 38f15118..00000000
--- a/web/components/CustomLink.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import Link from 'next/link'
-import { Url } from 'url'
-
-interface CustomLinkProps {
- as: Url
- href: Url
-}
-
-export default function CustomLink({
- as,
- href,
- ...otherProps
-}: CustomLinkProps) {
- return (
- <>
-
-
-
- >
- )
-}
diff --git a/web/components/Docs.tsx b/web/components/Docs.tsx
index b58e8d33..83da901a 100644
--- a/web/components/Docs.tsx
+++ b/web/components/Docs.tsx
@@ -32,9 +32,7 @@ export default function Docs({ docs, braces, assign }: DocsProps) {
diff --git a/web/components/Header.tsx b/web/components/Header.tsx
index 696adf6a..1f89f528 100644
--- a/web/components/Header.tsx
+++ b/web/components/Header.tsx
@@ -13,7 +13,8 @@ type HeaderProps = {
import * as React from 'react'
import Search from './Search'
-import { SearchIndex } from '../utils/search'
+import { SearchIndex, SearchItem } from '../utils/search'
+import Fuse from 'fuse.js'
interface NavLinkProps {
href: string
@@ -26,12 +27,11 @@ const NavLink: React.FunctionComponent
= (props) => {
? 'bg-rose-100 dark:bg-rose-900'
: ''
return (
-
-
- {props.name}
-
+
+ {props.name}
)
}
@@ -43,14 +43,21 @@ export default function Header({
searchIndex,
}: HeaderProps) {
const router = useRouter()
+ const fuse = React.useMemo(() => {
+ const options: Fuse.IFuseOptions = {}
+ return new Fuse(
+ searchIndex.list,
+ options,
+ Fuse.parseIndex(searchIndex.index)
+ )
+ }, [searchIndex])
+
return (
{router.asPath != '/' && (
@@ -61,7 +68,7 @@ export default function Header({
)}
-
+
diff --git a/web/components/Layout.tsx b/web/components/Layout.tsx
index 6b4e7ccd..b6959b00 100644
--- a/web/components/Layout.tsx
+++ b/web/components/Layout.tsx
@@ -34,7 +34,7 @@ export default function Layout({ children, searchIndex }: LayoutProps) {
}, [darkMode])
useEffect(() => {
- var darkQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const darkQuery = window.matchMedia('(prefers-color-scheme: dark)')
darkQuery.onchange = (e) => {
if (e.matches) {
diff --git a/web/components/LineChart.tsx b/web/components/LineChart.tsx
index e0772aa9..de675c4c 100644
--- a/web/components/LineChart.tsx
+++ b/web/components/LineChart.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
+import { useEffect, useMemo, useRef } from 'react'
import * as d3 from 'd3'
import { random } from 'colord'
@@ -40,12 +40,12 @@ function fetchData() {
const data = JSON.parse(storedData)
return data || []
}
-var data = fetchData()
+const data = fetchData()
export const LineChart = ({ width, height, darkMode }: LineChartProps) => {
const timerRef = useRef()
const svgRef = useRef(null)
- const [stroke, setStroke] = useState(random())
+ const stroke = useMemo(() => random(), [])
useEffect(() => {
const doIt = (data: DataPoint[]) => {
@@ -59,7 +59,7 @@ export const LineChart = ({ width, height, darkMode }: LineChartProps) => {
.range([height, 0])
// X axis
- const [xMin, xMax] = d3.extent(data, (d) => d.x)
+ const [, xMax] = d3.extent(data, (d) => d.x)
const xScale = d3
.scaleLinear()
.domain([(xMax || 0) - width, xMax || 0])
@@ -83,7 +83,7 @@ export const LineChart = ({ width, height, darkMode }: LineChartProps) => {
.attr('x', (d) => xScale(d.x))
.attr('y', (d) => yScale(d.y))
.attr('height', (d) => yScale((max || 0) - d.y))
- .attr('width', (d) => 1),
+ .attr('width', () => 1),
(update) =>
update
.transition()
@@ -95,7 +95,7 @@ export const LineChart = ({ width, height, darkMode }: LineChartProps) => {
.attr('x', (d) => xScale(d.x))
.attr('y', (d) => yScale(d.y))
.attr('height', (d) => yScale((max || 0) - d.y))
- .attr('width', (d) => 1),
+ .attr('width', () => 1),
(exit) => exit.call((d) => d.transition().remove())
)
diff --git a/web/components/Search.tsx b/web/components/Search.tsx
index ba94c22a..8313fccf 100644
--- a/web/components/Search.tsx
+++ b/web/components/Search.tsx
@@ -1,12 +1,12 @@
import Fuse from 'fuse.js'
import React, { Fragment, useCallback, useEffect, useState } from 'react'
import { Search as SearchIcon } from 'react-feather'
-import { SearchIndex, SearchItem } from '../utils/search'
+import { SearchItem } from '../utils/search'
import { Dialog, Transition, Combobox } from '@headlessui/react'
import { useRouter } from 'next/router'
export interface SearchProps {
- index: SearchIndex
+ fuse: Fuse
}
interface SearchResultProps {
@@ -57,23 +57,13 @@ const SearchResult: React.FunctionComponent = (props) => {
)
}
-const Search: React.FunctionComponent = (props) => {
+const Search: React.FunctionComponent = ({ fuse }) => {
const router = useRouter()
const [searchText, setSearchText] = useState('')
- const [selected, setSelected] = useState<
- Fuse.FuseResult | undefined
- >(undefined)
const [searchResults, setSearchResults] = useState<
Fuse.FuseResult[]
>([])
- const [fuse, setFuse] = useState(() => {
- const options: Fuse.IFuseOptions = {}
- return new Fuse(
- props.index.list,
- options,
- Fuse.parseIndex(props.index.index)
- )
- })
+
const [isOpen, setIsOpen] = useState(false)
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
@@ -159,7 +149,7 @@ const Search: React.FunctionComponent = (props) => {
-
+
diff --git a/web/components/ThemeSwitcher.tsx b/web/components/ThemeSwitcher.tsx
index d84f7f1f..a56e7753 100644
--- a/web/components/ThemeSwitcher.tsx
+++ b/web/components/ThemeSwitcher.tsx
@@ -1,5 +1,3 @@
-import { Dispatch, SetStateAction } from 'react'
-
const sunIcon = (