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 itemId = rootNode.getAttribute("data-item-id");
|
||||
// TODO: Use the new React 18 APIs instead!
|
||||
// eslint-disable-next-line react/no-deprecated
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<ItemPageOutfitPreview itemId={itemId} />
|
||||
|
|
|
@ -4,6 +4,8 @@ import ReactDOM from "react-dom";
|
|||
import { AppProvider, WardrobePage } from "./wardrobe-2020";
|
||||
|
||||
const rootNode = document.querySelector("#wardrobe-2020-root");
|
||||
// TODO: Use the new React 18 APIs instead!
|
||||
// eslint-disable-next-line react/no-deprecated
|
||||
ReactDOM.render(
|
||||
<AppProvider>
|
||||
<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;
|
||||
if (href != null) {
|
||||
return <ButtonComponent as="a" href={href} {...props} />;
|
||||
|
|
|
@ -227,6 +227,8 @@ function SearchResultItem({
|
|||
);
|
||||
|
||||
return (
|
||||
// We're wrapping the control inside the label, which works just fine!
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
|
|
|
@ -242,6 +242,8 @@ function SearchToolbar({
|
|||
)}
|
||||
<Input
|
||||
background={background}
|
||||
// TODO: How to improve a11y here?
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
{...inputProps}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useToast } from "@chakra-ui/react";
|
|||
import { emptySearchQuery } from "./SearchToolbar";
|
||||
import ItemsAndSearchPanels from "./ItemsAndSearchPanels";
|
||||
import SearchFooter from "./SearchFooter";
|
||||
import SupportOnly from "./support/SupportOnly";
|
||||
import useOutfitSaving from "./useOutfitSaving";
|
||||
import useOutfitState, { OutfitStateContext } from "./useOutfitState";
|
||||
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;
|
||||
|
|
|
@ -96,7 +96,7 @@ function useOutfitSaving(outfitState, dispatchToOutfit) {
|
|||
// It's important that this callback _doesn't_ change when the outfit
|
||||
// changes, so that the auto-save effect is only responding to the
|
||||
// debounced state!
|
||||
[saveOutfitMutation.mutateAsync, pathname, navigate, toast],
|
||||
[saveOutfitMutation, pathname, navigate, toast],
|
||||
);
|
||||
|
||||
const saveOutfit = React.useCallback(
|
||||
|
|
|
@ -250,13 +250,9 @@ function useOutfitState() {
|
|||
// Keep the URL up-to-date.
|
||||
const path = buildOutfitPath(outfitState);
|
||||
React.useEffect(() => {
|
||||
console.debug(
|
||||
`[useOutfitState] Navigating to latest outfit path:`,
|
||||
path,
|
||||
outfitState,
|
||||
);
|
||||
console.debug(`[useOutfitState] Navigating to latest outfit path:`, path);
|
||||
navigate(path, { replace: true });
|
||||
}, [path]);
|
||||
}, [path, navigate]);
|
||||
|
||||
return {
|
||||
loading: outfitLoading || itemsLoading,
|
||||
|
@ -393,6 +389,9 @@ function useParseOutfitUrl() {
|
|||
// stable object!
|
||||
const memoizedOutfitState = React.useMemo(
|
||||
() => 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()],
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
|
||||
import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev";
|
||||
import { setContext } from "@apollo/client/link/context";
|
||||
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
|
||||
|
||||
// Use Apollo's error messages in development.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { Box, Button, Flex, Select } from "@chakra-ui/react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
function PaginationToolbar({
|
||||
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 }) {
|
||||
if (href != null) {
|
||||
return <Button as="a" href={href} {...props} />;
|
||||
|
|
|
@ -454,11 +454,10 @@ export function MajorErrorMessage({ error = null, variant = "unexpected" }) {
|
|||
>
|
||||
<img
|
||||
src={ErrorGrundoImg}
|
||||
srcset={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`}
|
||||
srcSet={`${ErrorGrundoImg}, ${ErrorGrundoImg2x} 2x`}
|
||||
alt="Distressed Grundo programmer"
|
||||
width={100}
|
||||
height={100}
|
||||
layout="fixed"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
17
package.json
17
package.json
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "app",
|
||||
"name": "impress",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@chakra-ui/icons": "^1.0.4",
|
||||
|
@ -27,11 +28,21 @@
|
|||
"tweenjs": "^1.0.2"
|
||||
},
|
||||
"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": {
|
||||
"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",
|
||||
"dev": "yarn build:dev --watch"
|
||||
"dev": "yarn build:dev --watch",
|
||||
"lint": "eslint app/javascript",
|
||||
"prepare": "husky install"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue