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 = {
parser: '@typescript-eslint/parser',
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: {
'@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-typos': 'off',
'lodash/import-scope': [2, 'member'],
},
env: {
browser: true,
node: true,
},
}

View File

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

View File

@ -1,5 +1,5 @@
// 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 }) {
const { children } = props

View File

@ -229,12 +229,13 @@ function QuickOutcomeView(props: {
case 'NUMERIC':
display = formatLargeNumber(getExpectedValue(contract as NumericContract))
break
case 'FREE_RESPONSE':
case 'FREE_RESPONSE': {
const topAnswer = getTopAnswer(contract as FreeResponseContract)
display =
topAnswer &&
formatPercent(getOutcomeProbability(contract, topAnswer.id))
break
}
}
return (

View File

@ -1,3 +1,4 @@
import React from 'react'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
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:
const unorderedBetsAndComments = [...commentsWithoutBets, ...groupedBets]
let sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
const sortedBetsAndComments = sortBy(unorderedBetsAndComments, (item) => {
if (item.type === 'comment') {
return item.comment.createdTime
} else if (item.type === 'bet') {
@ -540,7 +540,7 @@ export function getSpecificContractActivityItems(
}
) {
const { mode } = options
let items = [] as ActivityItem[]
const items = [] as ActivityItem[]
switch (mode) {
case 'bets':
@ -559,7 +559,7 @@ export function getSpecificContractActivityItems(
)
break
case 'comments':
case 'comments': {
const nonFreeResponseComments = comments.filter((comment) =>
commentIsGeneralComment(comment, contract)
)
@ -585,6 +585,7 @@ export function getSpecificContractActivityItems(
),
})
break
}
case 'free-response-comment-answer-groups':
items.push(
...getAnswerAndCommentInputGroups(

View File

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

View File

@ -1,6 +1,8 @@
import { ReactNode } from 'react'
export const JoinSpans = (props: {
children: any[]
separator?: JSX.Element | string
separator?: ReactNode
}) => {
const { separator } = props
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'
// From https://tailwindui.com/components/application-ui/overlays/modals
export function Modal(props: {
children: React.ReactNode
children: ReactNode
open: boolean
setOpen: (open: boolean) => void
}) {

View File

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

View File

@ -4,14 +4,14 @@ import { SiteLink } from './site-link'
// Return a JSX span, linkifying @username, #hashtags, and https://...
// TODO: Use a markdown parser instead of rolling our own here.
export function Linkify(props: { text: string; gray?: boolean }) {
let { text, gray } = props
const { text, gray } = props
// Replace "m1234" with "ϻ1234"
// const mRegex = /(\W|^)m(\d+)/g
// text = text.replace(mRegex, (_, pre, num) => `${pre}ϻ${num}`)
// Find instances of @username, #hashtag, and https://...
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 links = matches.map((match) => {
// 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 { useRouter } from 'next/router'
function getNavigation(username: String) {
function getNavigation(username: string) {
return [
{ name: 'Home', href: '/home', icon: HomeIcon },
{ name: 'Activity', href: '/activity', icon: ChatAltIcon },

View File

@ -25,7 +25,7 @@ import {
useHasCreatedContractToday,
} from 'web/hooks/use-has-created-contract-today'
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
function IconFromUrl(url: string): React.ComponentType<{ className?: string }> {
@ -130,7 +130,7 @@ export default function Sidebar(props: { className?: string }) {
const nextUtcResetTime = getUtcFreeMarketResetTime(false)
const interval = setInterval(() => {
const now = new Date().getTime()
let timeUntil = nextUtcResetTime - now
const timeUntil = nextUtcResetTime - now
const hoursUntil = timeUntil / 1000 / 60 / 60
const minutesUntil = Math.floor((hoursUntil * 60) % 60)
const secondsUntil = Math.floor((hoursUntil * 60 * 60) % 60)

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import clsx from 'clsx'
import { ReactNode } from 'react'
import Link from 'next/link'
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
return href.startsWith('http') ? (
<>{children}</>

View File

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

View File

@ -9,13 +9,15 @@ export const app = getApps().length ? getApp() : initializeApp(FIREBASE_CONFIG)
export const db = getFirestore()
export const functions = getFunctions()
const EMULATORS_STARTED = 'EMULATORS_STARTED'
declare global {
/* eslint-disable-next-line no-var */
var EMULATORS_STARTED: boolean
}
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
// @ts-ignore
if (!global[EMULATORS_STARTED]) {
// @ts-ignore
global[EMULATORS_STARTED] = true
if (!global.EMULATORS_STARTED) {
global.EMULATORS_STARTED = true
connectFirestoreEmulator(db, 'localhost', 8080)
connectFunctionsEmulator(functions, 'localhost', 5001)
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { sortBy, sumBy, uniqBy } from 'lodash'
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 { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page'

View File

@ -63,7 +63,7 @@ export default function Folds(props: {
const [query, setQuery] = useState('')
// Copied from contracts-list.tsx; extract if we copy this again
const queryWords = query.toLowerCase().split(' ')
function check(corpus: String) {
function check(corpus: string) {
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 Router from 'next/router'
@ -21,7 +21,7 @@ import Textarea from 'react-expanding-textarea'
function EditUserField(props: {
user: User
field: 'bio' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
field: 'bio' | 'website' | 'bannerUrl' | 'twitterHandle' | 'discordHandle'
label: string
}) {
const { user, field, label } = props
@ -220,18 +220,15 @@ export default function ProfilePage() {
}}
/>
{[
['bio', 'Bio'],
['website', 'Website URL'],
['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'],
].map(([field, label]) => (
<EditUserField
user={user}
// @ts-ignore
field={field}
label={label}
/>
{(
[
['bio', 'Bio'],
['website', 'Website URL'],
['twitterHandle', 'Twitter'],
['discordHandle', 'Discord'],
] as const
).map(([field, label]) => (
<EditUserField user={user} field={field} label={label} />
))}
</>
)}