Add more linting to web package (#343)

* Import React a lot

* Fix misc. linting concerns

* Turn on many recommended lints for `web` package
This commit is contained in:
Marshall Polaris 2022-05-26 14:41:24 -07:00 committed by GitHub
parent 5217270073
commit 420ea9e90e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 99 additions and 83 deletions

View File

@ -1,10 +1,21 @@
module.exports = { module.exports = {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['lodash'], plugins: ['lodash'],
extends: ['plugin:react-hooks/recommended', 'plugin:@next/next/recommended'], extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@next/next/recommended',
],
rules: { rules: {
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@next/next/no-img-element': 'off', '@next/next/no-img-element': 'off',
'@next/next/no-typos': 'off', '@next/next/no-typos': 'off',
'lodash/import-scope': [2, 'member'], 'lodash/import-scope': [2, 'member'],
}, },
env: {
browser: true,
node: true,
},
} }

View File

@ -1,4 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { Col } from './layout/col' import { Col } from './layout/col'

View File

@ -1,5 +1,5 @@
// Adapted from https://stackoverflow.com/a/50884055/1222351 // Adapted from https://stackoverflow.com/a/50884055/1222351
import { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
export function ClientRender(props: { children: React.ReactNode }) { export function ClientRender(props: { children: React.ReactNode }) {
const { children } = props const { children } = props

View File

@ -229,13 +229,14 @@ function QuickOutcomeView(props: {
case 'NUMERIC': case 'NUMERIC':
display = formatLargeNumber(getExpectedValue(contract as NumericContract)) display = formatLargeNumber(getExpectedValue(contract as NumericContract))
break break
case 'FREE_RESPONSE': case 'FREE_RESPONSE': {
const topAnswer = getTopAnswer(contract as FreeResponseContract) const topAnswer = getTopAnswer(contract as FreeResponseContract)
display = display =
topAnswer && topAnswer &&
formatPercent(getOutcomeProbability(contract, topAnswer.id)) formatPercent(getOutcomeProbability(contract, topAnswer.id))
break break
} }
}
return ( return (
<Col className={clsx('items-center text-3xl', textColor)}> <Col className={clsx('items-center text-3xl', textColor)}>

View File

@ -1,3 +1,4 @@
import React from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc' import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone' import timezone from 'dayjs/plugin/timezone'

View File

@ -343,7 +343,7 @@ function groupBetsAndComments(
// iterate through the bets and comment activity items and add them to the items in order of comment creation time: // iterate through the bets and comment activity items and add them to the items in order of comment creation time:
const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets] const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
let sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => { const sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
if (item.type === 'comment') { if (item.type === 'comment') {
return item.comment.createdTime return item.comment.createdTime
} else if (item.type === 'bet') { } else if (item.type === 'bet') {
@ -540,7 +540,7 @@ export function getSpecificContractActivityItems(
} }
) { ) {
const { mode } = options const { mode } = options
let items = [] as ActivityItem[] const items = [] as ActivityItem[]
switch (mode) { switch (mode) {
case 'bets': case 'bets':
@ -559,7 +559,7 @@ export function getSpecificContractActivityItems(
) )
break break
case 'comments': case 'comments': {
const nonFreeResponseComments = comments.filter((comment) => const nonFreeResponseComments = comments.filter((comment) =>
commentIsGeneralComment(comment, contract) commentIsGeneralComment(comment, contract)
) )
@ -585,6 +585,7 @@ export function getSpecificContractActivityItems(
), ),
}) })
break break
}
case 'free-response-comment-answer-groups': case 'free-response-comment-answer-groups':
items.push( items.push(
...getAnswerAndCommentInputGroups( ...getAnswerAndCommentInputGroups(

View File

@ -21,7 +21,7 @@ export function CopyLinkDateTimeComponent(props: {
event: React.MouseEvent<HTMLAnchorElement, MouseEvent> event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) { ) {
event.preventDefault() event.preventDefault()
let elementLocation = `https://${ENV_CONFIG.domain}${contractPath( const elementLocation = `https://${ENV_CONFIG.domain}${contractPath(
contract contract
)}#${elementId}` )}#${elementId}`

View File

@ -1,6 +1,8 @@
import { ReactNode } from 'react'
export const JoinSpans = (props: { export const JoinSpans = (props: {
children: any[] children: any[]
separator?: JSX.Element | string separator?: ReactNode
}) => { }) => {
const { separator } = props const { separator } = props
const children = props.children.filter((x) => !!x) const children = props.children.filter((x) => !!x)

View File

@ -1,9 +1,9 @@
import { Fragment } from 'react' import { Fragment, ReactNode } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
// From https://tailwindui.com/components/application-ui/overlays/modals // From https://tailwindui.com/components/application-ui/overlays/modals
export function Modal(props: { export function Modal(props: {
children: React.ReactNode children: ReactNode
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {

View File

@ -1,12 +1,12 @@
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react' import { ReactNode, useState } from 'react'
import { Row } from './row' import { Row } from './row'
type Tab = { type Tab = {
title: string title: string
tabIcon?: JSX.Element tabIcon?: ReactNode
content: JSX.Element content: ReactNode
// If set, change the url to this href when the tab is selected // If set, change the url to this href when the tab is selected
href?: string href?: string
} }

View File

@ -4,14 +4,14 @@ import { SiteLink } from './site-link'
// Return a JSX span, linkifying @username, #hashtags, and https://... // Return a JSX span, linkifying @username, #hashtags, and https://...
// TODO: Use a markdown parser instead of rolling our own here. // TODO: Use a markdown parser instead of rolling our own here.
export function Linkify(props: { text: string; gray?: boolean }) { export function Linkify(props: { text: string; gray?: boolean }) {
let { text, gray } = props const { text, gray } = props
// Replace "m1234" with "ϻ1234" // Replace "m1234" with "ϻ1234"
// const mRegex = /(\W|^)m(\d+)/g // const mRegex = /(\W|^)m(\d+)/g
// text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`) // text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
// Find instances of @username, #hashtag, and https://... // Find instances of @username, #hashtag, and https://...
const regex = const regex =
/(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#\/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#\/%=~_|])/gi /(?:^|\s)(?:[@#][a-z0-9_]+|https?:\/\/[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])/gi
const matches = text.match(regex) || [] const matches = text.match(regex) || []
const links = matches.map((match) => { const links = matches.map((match) => {
// Matches are in the form: " @username" or "https://example.com" // Matches are in the form: " @username" or "https://example.com"

View File

@ -17,7 +17,7 @@ import { Avatar } from '../avatar'
import clsx from 'clsx' import clsx from 'clsx'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
function getNavigation(username: String) { function getNavigation(username: string) {
return [ return [
{ name: 'Home', href: '/home', icon: HomeIcon }, { name: 'Home', href: '/home', icon: HomeIcon },
{ name: 'Activity', href: '/activity', icon: ChatAltIcon }, { name: 'Activity', href: '/activity', icon: ChatAltIcon },

View File

@ -25,7 +25,7 @@ import {
useHasCreatedContractToday, useHasCreatedContractToday,
} from 'web/hooks/use-has-created-contract-today' } from 'web/hooks/use-has-created-contract-today'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
// Create an icon from the url of an image // Create an icon from the url of an image
function IconFromUrl(url: string): React.ComponentType<{ className?: string }> { function IconFromUrl(url: string): React.ComponentType<{ className?: string }> {
@ -130,7 +130,7 @@ export default function Sidebar(props: { className?: string }) {
const nextUtcResetTime = getUtcFreeMarketResetTime(false) const nextUtcResetTime = getUtcFreeMarketResetTime(false)
const interval = setInterval(() => { const interval = setInterval(() => {
const now = new Date().getTime() const now = new Date().getTime()
let timeUntil = nextUtcResetTime - now const timeUntil = nextUtcResetTime - now
const hoursUntil = timeUntil / 1000 / 60 / 60 const hoursUntil = timeUntil / 1000 / 60 / 60
const minutesUntil = Math.floor((hoursUntil * 60) % 60) const minutesUntil = Math.floor((hoursUntil * 60) % 60)
const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60) const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)

View File

@ -1,5 +1,6 @@
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Spacer } from './layout/spacer' import { Spacer } from './layout/spacer'

View File

@ -1,4 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { ReactNode } from 'react'
import { Answer } from 'common/answer' import { Answer } from 'common/answer'
import { getProbability } from 'common/calculate' import { getProbability } from 'common/calculate'
import { getValueFromBucket } from 'common/calculate-dpm' import { getValueFromBucket } from 'common/calculate-dpm'
@ -156,7 +157,7 @@ export function AnswerLabel(props: {
function FreeResponseAnswerToolTip(props: { function FreeResponseAnswerToolTip(props: {
text: string text: string
children?: React.ReactNode children?: ReactNode
}) { }) {
const { text } = props const { text } = props
return ( return (

View File

@ -1,4 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { ReactNode } from 'react'
import { BottomNavBar } from './nav/nav-bar' import { BottomNavBar } from './nav/nav-bar'
import Sidebar from './nav/sidebar' import Sidebar from './nav/sidebar'
import { Toaster } from 'react-hot-toast' import { Toaster } from 'react-hot-toast'
@ -6,7 +7,7 @@ import { Toaster } from 'react-hot-toast'
export function Page(props: { export function Page(props: {
margin?: boolean margin?: boolean
assertUser?: 'signed-in' | 'signed-out' assertUser?: 'signed-in' | 'signed-out'
rightSidebar?: React.ReactNode rightSidebar?: ReactNode
suspend?: boolean suspend?: boolean
children?: any children?: any
}) { }) {

View File

@ -1,4 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { ReactNode } from 'react'
import Link from 'next/link' import Link from 'next/link'
export const SiteLink = (props: { export const SiteLink = (props: {
@ -30,7 +31,7 @@ export const SiteLink = (props: {
) )
} }
function MaybeLink(props: { href: string; children: React.ReactNode }) { function MaybeLink(props: { href: string; children: ReactNode }) {
const { href, children } = props const { href, children } = props
return href.startsWith('http') ? ( return href.startsWith('http') ? (
<>{children}</> <>{children}</>

View File

@ -6,10 +6,10 @@ import fetch, { Headers, Response } from 'node-fetch'
function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) { function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
const result = new Headers() const result = new Headers()
for (let name of whitelist) { for (const name of whitelist) {
const v = req.headers[name.toLowerCase()] const v = req.headers[name.toLowerCase()]
if (Array.isArray(v)) { if (Array.isArray(v)) {
for (let vv of v) { for (const vv of v) {
result.append(name, vv) result.append(name, vv)
} }
} else if (v != null) { } else if (v != null) {
@ -23,7 +23,7 @@ function getProxiedRequestHeaders(req: NextApiRequest, whitelist: string[]) {
function getProxiedResponseHeaders(res: Response, whitelist: string[]) { function getProxiedResponseHeaders(res: Response, whitelist: string[]) {
const result: { [k: string]: string } = {} const result: { [k: string]: string } = {}
for (let name of whitelist) { for (const name of whitelist) {
const v = res.headers.get(name) const v = res.headers.get(name)
if (v != null) { if (v != null) {
result[name] = v result[name] = v

View File

@ -9,13 +9,15 @@ export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG)
export const db = getFirestore() export const db = getFirestore()
export const functions = getFunctions() export const functions = getFunctions()
const EMULATORS_STARTED = 'EMULATORS_STARTED' declare global {
/* eslint-disable-next-line no-var */
var EMULATORS_STARTED: boolean
}
function startEmulators() { function startEmulators() {
// I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee // I don't like this but this is the only way to reconnect to the emulators without error, see: https://stackoverflow.com/questions/65066963/firebase-firestore-emulator-error-host-has-been-set-in-both-settings-and-usee
// @ts-ignore if (!global.EMULATORS_STARTED) {
if (!global[EMULATORS_STARTED]) { global.EMULATORS_STARTED = true
// @ts-ignore
global[EMULATORS_STARTED] = true
connectFirestoreEmulator(db, 'localhost', 8080) connectFirestoreEmulator(db, 'localhost', 8080)
connectFunctionsEmulator(functions, 'localhost', 5001) connectFunctionsEmulator(functions, 'localhost', 5001)
} }

View File

@ -21,7 +21,7 @@ export function copyToClipboard(text: string) {
document.queryCommandSupported('copy') document.queryCommandSupported('copy')
) { ) {
console.log('copy 3') console.log('copy 3')
var textarea = document.createElement('textarea') const textarea = document.createElement('textarea')
textarea.textContent = text textarea.textContent = text
textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge. textarea.style.position = 'fixed' // Prevent scrolling to bottom of page in Microsoft Edge.
document.body.appendChild(textarea) document.body.appendChild(textarea)

View File

@ -13,12 +13,12 @@ function firstLine(msg: string) {
function printBuildInfo() { function printBuildInfo() {
// These are undefined if e.g. dev server // These are undefined if e.g. dev server
if (process.env.NEXT_PUBLIC_VERCEL_ENV) { if (process.env.NEXT_PUBLIC_VERCEL_ENV) {
let env = process.env.NEXT_PUBLIC_VERCEL_ENV const env = process.env.NEXT_PUBLIC_VERCEL_ENV
let msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE const msg = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_MESSAGE
let owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER const owner = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER
let repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG const repo = process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG
let sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA const sha = process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA
let url = `https://github.com/${owner}/${repo}/commit/${sha}` const url = `https://github.com/${owner}/${repo}/commit/${sha}`
console.info(`Build: ${env} / ${firstLine(msg || '???')} / ${url}`) console.info(`Build: ${env} / ${firstLine(msg || '???')} / ${url}`)
} }
} }

View File

@ -19,28 +19,22 @@ function avatarHtml(avatarUrl: string) {
} }
function UsersTable() { function UsersTable() {
let users = useUsers() const users = useUsers()
let privateUsers = usePrivateUsers() const privateUsers = usePrivateUsers()
// Map private users by user id // Map private users by user id
const privateUsersById = mapKeys(privateUsers, 'id') const privateUsersById = mapKeys(privateUsers, 'id')
console.log('private users by id', privateUsersById) console.log('private users by id', privateUsersById)
// For each user, set their email from the PrivateUser // For each user, set their email from the PrivateUser
users = users.map((user) => { const fullUsers = users
// @ts-ignore .map((user) => {
user.email = privateUsersById[user.id]?.email return { email: privateUsersById[user.id]?.email, ...user }
return user
}) })
.sort((a, b) => b.createdTime - a.createdTime)
// Sort users by createdTime descending, by default
users = users.sort((a, b) => b.createdTime - a.createdTime)
function exportCsv() { function exportCsv() {
const csv = users const csv = fullUsers.map((u) => [u.email, u.name].join(', ')).join('\n')
// @ts-ignore
.map((u) => [u.email, u.name].join(', '))
.join('\n')
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }) const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const a = document.createElement('a') const a = document.createElement('a')
@ -108,13 +102,14 @@ function UsersTable() {
} }
function ContractsTable() { function ContractsTable() {
let contracts = useContracts() ?? [] const contracts = useContracts() ?? []
// Sort users by createdTime descending, by default // Sort users by createdTime descending, by default
contracts.sort((a, b) => b.createdTime - a.createdTime) const displayContracts = contracts
.sort((a, b) => b.createdTime - a.createdTime)
.map((contract) => {
// Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs // Render a clickable question. See https://gridjs.io/docs/examples/react-cells for docs
contracts.map((contract) => { const questionLink = r(
// @ts-ignore
contract.questionLink = r(
<div className="w-60"> <div className="w-60">
<a <a
className="hover:underline hover:decoration-indigo-400 hover:decoration-2" className="hover:underline hover:decoration-indigo-400 hover:decoration-2"
@ -124,11 +119,12 @@ function ContractsTable() {
</a> </a>
</div> </div>
) )
return { questionLink, ...contract }
}) })
return ( return (
<Grid <Grid
data={contracts} data={displayContracts}
columns={[ columns={[
{ {
id: 'creatorUsername', id: 'creatorUsername',

View File

@ -1,6 +1,6 @@
import { sortBy, sumBy, uniqBy } from 'lodash' import { sortBy, sumBy, uniqBy } from 'lodash'
import clsx from 'clsx' import clsx from 'clsx'
import { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'

View File

@ -63,7 +63,7 @@ export default function Folds(props: {
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
// Copied from contracts-list.tsx; extract if we copy this again // Copied from contracts-list.tsx; extract if we copy this again
const queryWords = query.toLowerCase().split(' ') const queryWords = query.toLowerCase().split(' ')
function check(corpus: String) { function check(corpus: string) {
return queryWords.every((word) => corpus.toLowerCase().includes(word)) return queryWords.every((word) => corpus.toLowerCase().includes(word))
} }

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { RefreshIcon } from '@heroicons/react/outline' import { RefreshIcon } from '@heroicons/react/outline'
import Router from 'next/router' import Router from 'next/router'
@ -21,7 +21,7 @@ import Textarea from 'react-expanding-textarea'
function EditUserField(props: { function EditUserField(props: {
user: User user: User
field: 'bio' | 'bannerUrl' | 'twitterHandle' | 'discordHandle' field: 'bio' | 'website' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
label: string label: string
}) { }) {
const { user, field, label } = props const { user, field, label } = props
@ -220,18 +220,15 @@ export default function ProfilePage() {
}} }}
/> />
{[ {(
[
['bio', 'Bio'], ['bio', 'Bio'],
['website', 'Website URL'], ['website', 'Website URL'],
['twitterHandle', 'Twitter'], ['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'], ['discordHandle', 'Discord'],
].map(([field, label]) => ( ] as const
<EditUserField ).map(([field, label]) => (
user={user} <EditUserField user={user} field={field} label={label} />
// @ts-ignore
field={field}
label={label}
/>
))} ))}
</> </>
)} )}