Set up eslint for wardrobe-2020
Ok cool, I have just not been running any of this since moving out of impress-2020, but now that we're doing serious JS work in here it's time to turn it back on!! 1. Install eslint and the plugins we use 2. Set up a `yarn lint` command 3. Set up a git hook via husky to lint on pre-commit 4. Fix/disable all the lint errors!
This commit is contained in:
parent
629706a182
commit
494f82601f
15 changed files with 1760 additions and 86 deletions
45
.eslintrc.json
Normal file
45
.eslintrc.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-a11y"],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"process": true // For process.env["NODE_ENV"]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allow": ["debug", "info", "warn", "error"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/first": "off",
|
||||||
|
"import/no-webpack-loader-syntax": "off",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"varsIgnorePattern": "^unused",
|
||||||
|
"argsIgnorePattern": "^_+$|^e$"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/no-unescaped-entities": ["error", { "forbid": [">", "}"] }],
|
||||||
|
// We have some React.forwardRefs that trigger this, not sure how to improve
|
||||||
|
"react/display-name": "off",
|
||||||
|
"react/prop-types": "off"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint --max-warnings=0 --fix
|
|
@ -5,6 +5,8 @@ import { AppProvider, ItemPageOutfitPreview } from "./wardrobe-2020";
|
||||||
|
|
||||||
const rootNode = document.querySelector("#outfit-preview-root");
|
const rootNode = document.querySelector("#outfit-preview-root");
|
||||||
const itemId = rootNode.getAttribute("data-item-id");
|
const itemId = rootNode.getAttribute("data-item-id");
|
||||||
|
// TODO: Use the new React 18 APIs instead!
|
||||||
|
// eslint-disable-next-line react/no-deprecated
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<ItemPageOutfitPreview itemId={itemId} />
|
<ItemPageOutfitPreview itemId={itemId} />
|
||||||
|
|
|
@ -4,6 +4,8 @@ import ReactDOM from "react-dom";
|
||||||
import { AppProvider, WardrobePage } from "./wardrobe-2020";
|
import { AppProvider, WardrobePage } from "./wardrobe-2020";
|
||||||
|
|
||||||
const rootNode = document.querySelector("#wardrobe-2020-root");
|
const rootNode = document.querySelector("#wardrobe-2020-root");
|
||||||
|
// TODO: Use the new React 18 APIs instead!
|
||||||
|
// eslint-disable-next-line react/no-deprecated
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AppProvider>
|
<AppProvider>
|
||||||
<WardrobePage />
|
<WardrobePage />
|
||||||
|
|
|
@ -285,7 +285,7 @@ function ItemActionButton({ icon, label, to, onClick }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkOrButton({ href, component = Button, ...props }) {
|
function LinkOrButton({ href, component, ...props }) {
|
||||||
const ButtonComponent = component;
|
const ButtonComponent = component;
|
||||||
if (href != null) {
|
if (href != null) {
|
||||||
return <ButtonComponent as="a" href={href} {...props} />;
|
return <ButtonComponent as="a" href={href} {...props} />;
|
||||||
|
|
|
@ -227,6 +227,8 @@ function SearchResultItem({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// We're wrapping the control inside the label, which works just fine!
|
||||||
|
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||||
<label>
|
<label>
|
||||||
<VisuallyHidden
|
<VisuallyHidden
|
||||||
as="input"
|
as="input"
|
||||||
|
|
|
@ -242,6 +242,8 @@ function SearchToolbar({
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
background={background}
|
background={background}
|
||||||
|
// TODO: How to improve a11y here?
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { useToast } from "@chakra-ui/react";
|
||||||
import { emptySearchQuery } from "./SearchToolbar";
|
import { emptySearchQuery } from "./SearchToolbar";
|
||||||
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
||||||
import SearchFooter from "./SearchFooter";
|
import SearchFooter from "./SearchFooter";
|
||||||
import SupportOnly from "./support/SupportOnly";
|
|
||||||
import useOutfitSaving from "./useOutfitSaving";
|
import useOutfitSaving from "./useOutfitSaving";
|
||||||
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
||||||
import WardrobePageLayout from "./WardrobePageLayout";
|
import WardrobePageLayout from "./WardrobePageLayout";
|
||||||
|
@ -111,36 +110,4 @@ function WardrobePage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* SavedOutfitMetaTags renders the meta tags that we use to render pretty
|
|
||||||
* share cards for social media for saved outfits!
|
|
||||||
*/
|
|
||||||
function SavedOutfitMetaTags({ outfitState }) {
|
|
||||||
const updatedAtTimestamp = Math.floor(
|
|
||||||
new Date(outfitState.updatedAt).getTime() / 1000,
|
|
||||||
);
|
|
||||||
const imageUrl =
|
|
||||||
`https://impress-outfit-images.openneo.net/outfits` +
|
|
||||||
`/${encodeURIComponent(outfitState.id)}` +
|
|
||||||
`/v/${encodeURIComponent(updatedAtTimestamp)}` +
|
|
||||||
`/600.png`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content={outfitState.name || "Untitled outfit"}
|
|
||||||
/>
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:image" content={imageUrl} />
|
|
||||||
<meta property="og:url" content={outfitState.url} />
|
|
||||||
<meta property="og:site_name" content="Dress to Impress" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="A custom Neopets outfit, designed on Dress to Impress!"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WardrobePage;
|
export default WardrobePage;
|
||||||
|
|
|
@ -96,7 +96,7 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
|
||||||
// It's important that this callback _doesn't_ change when the outfit
|
// It's important that this callback _doesn't_ change when the outfit
|
||||||
// changes, so that the auto-save effect is only responding to the
|
// changes, so that the auto-save effect is only responding to the
|
||||||
// debounced state!
|
// debounced state!
|
||||||
[saveOutfitMutation.mutateAsync, pathname, navigate, toast],
|
[saveOutfitMutation, pathname, navigate, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveOutfit = React.useCallback(
|
const saveOutfit = React.useCallback(
|
||||||
|
|
|
@ -250,13 +250,9 @@ function useOutfitState() {
|
||||||
// Keep the URL up-to-date.
|
// Keep the URL up-to-date.
|
||||||
const path = buildOutfitPath(outfitState);
|
const path = buildOutfitPath(outfitState);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.debug(
|
console.debug(`[useOutfitState] Navigating to latest outfit path:`, path);
|
||||||
`[useOutfitState] Navigating to latest outfit path:`,
|
|
||||||
path,
|
|
||||||
outfitState,
|
|
||||||
);
|
|
||||||
navigate(path, { replace: true });
|
navigate(path, { replace: true });
|
||||||
}, [path]);
|
}, [path, navigate]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading: outfitLoading || itemsLoading,
|
loading: outfitLoading || itemsLoading,
|
||||||
|
@ -393,6 +389,9 @@ function useParseOutfitUrl() {
|
||||||
// stable object!
|
// stable object!
|
||||||
const memoizedOutfitState = React.useMemo(
|
const memoizedOutfitState = React.useMemo(
|
||||||
() => readOutfitStateFromSearchParams(location.pathname, mergedParams),
|
() => readOutfitStateFromSearchParams(location.pathname, mergedParams),
|
||||||
|
// TODO: This hook is reliable as-is, I think… but is there a simpler way
|
||||||
|
// to make it obvious that it is?
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[location.pathname, mergedParams.toString()],
|
[location.pathname, mergedParams.toString()],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||||
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
|
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
|
||||||
import { setContext } from "@apollo/client/link/context";
|
|
||||||
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
||||||
|
|
||||||
// Use Apollo's error messages in development.
|
// Use Apollo's error messages in development.
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Button, Flex, Select } from "@chakra-ui/react";
|
import { Box, Button, Flex, Select } from "@chakra-ui/react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
|
|
||||||
function PaginationToolbar({
|
function PaginationToolbar({
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -71,42 +70,6 @@ function PaginationToolbar({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRouterPagination(totalCount, numPerPage) {
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const currentOffset = parseInt(searchParams.get("offset")) || 0;
|
|
||||||
|
|
||||||
const currentPageIndex = Math.floor(currentOffset / numPerPage);
|
|
||||||
const currentPageNumber = currentPageIndex + 1;
|
|
||||||
const numTotalPages = totalCount ? Math.ceil(totalCount / numPerPage) : null;
|
|
||||||
|
|
||||||
const buildPageUrl = React.useCallback(
|
|
||||||
(newPageNumber) => {
|
|
||||||
setSearchParams((newParams) => {
|
|
||||||
const newPageIndex = newPageNumber - 1;
|
|
||||||
const newOffset = newPageIndex * numPerPage;
|
|
||||||
newParams.set("offset", newOffset);
|
|
||||||
return newParams;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[query, numPerPage],
|
|
||||||
);
|
|
||||||
|
|
||||||
const goToPageNumber = React.useCallback(
|
|
||||||
(newPageNumber) => {
|
|
||||||
pushHistory(buildPageUrl(newPageNumber));
|
|
||||||
},
|
|
||||||
[buildPageUrl, pushHistory],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
numTotalPages,
|
|
||||||
currentPageNumber,
|
|
||||||
goToPageNumber,
|
|
||||||
buildPageUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkOrButton({ href, ...props }) {
|
function LinkOrButton({ href, ...props }) {
|
||||||
if (href != null) {
|
if (href != null) {
|
||||||
return <Button as="a" href={href} {...props} />;
|
return <Button as="a" href={href} {...props} />;
|
||||||
|
|
|
@ -454,11 +454,10 @@ export function MajorErrorMessage({ error = null, variant = "unexpected" }) {
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={ErrorGrundoImg}
|
src={ErrorGrundoImg}
|
||||||
srcset={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`}
|
srcSet={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`}
|
||||||
alt="Distressed Grundo programmer"
|
alt="Distressed Grundo programmer"
|
||||||
width={100}
|
width={100}
|
||||||
height={100}
|
height={100}
|
||||||
layout="fixed"
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
17
package.json
17
package.json
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "app",
|
"name": "impress",
|
||||||
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.6.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@chakra-ui/icons": "^1.0.4",
|
"@chakra-ui/icons": "^1.0.4",
|
||||||
|
@ -27,11 +28,21 @@
|
||||||
"tweenjs": "^1.0.2"
|
"tweenjs": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^3.0.3"
|
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||||
|
"@typescript-eslint/parser": "^6.9.1",
|
||||||
|
"eslint": "^8.52.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --asset-names=[name]-[hash].digested --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text",
|
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --asset-names=[name]-[hash].digested --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text",
|
||||||
"build:dev": "yarn build --public-path=/dev-assets",
|
"build:dev": "yarn build --public-path=/dev-assets",
|
||||||
"dev": "yarn build:dev --watch"
|
"dev": "yarn build:dev --watch",
|
||||||
|
"lint": "eslint app/javascript",
|
||||||
|
"prepare": "husky install"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue