diff --git a/web/.eslintrc.js b/web/.eslintrc.js index f864ffa2..6c34b521 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -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, + }, } diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 84a29b6d..a31957cb 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -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' diff --git a/web/components/client-render.tsx b/web/components/client-render.tsx index a58c90ff..d26d2301 100644 --- a/web/components/client-render.tsx +++ b/web/components/client-render.tsx @@ -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 diff --git a/web/components/contract/quick-bet.tsx b/web/components/contract/quick-bet.tsx index d7b37834..c4b1ec16 100644 --- a/web/components/contract/quick-bet.tsx +++ b/web/components/contract/quick-bet.tsx @@ -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 ( diff --git a/web/components/datetime-tooltip.tsx b/web/components/datetime-tooltip.tsx index 69c4521e..7f7a9b45 100644 --- a/web/components/datetime-tooltip.tsx +++ b/web/components/datetime-tooltip.tsx @@ -1,3 +1,4 @@ +import React from 'react' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' import timezone from 'dayjs/plugin/timezone' diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index c958a892..b8d99598 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -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( diff --git a/web/components/feed/copy-link-date-time.tsx b/web/components/feed/copy-link-date-time.tsx index 354996a4..60395801 100644 --- a/web/components/feed/copy-link-date-time.tsx +++ b/web/components/feed/copy-link-date-time.tsx @@ -21,7 +21,7 @@ export function CopyLinkDateTimeComponent(props: { event: React.MouseEvent ) { event.preventDefault() - let elementLocation = `https://${ENV_CONFIG.domain}${contractPath( + const elementLocation = `https://${ENV_CONFIG.domain}${contractPath( contract )}#${elementId}` diff --git a/web/components/join-spans.tsx b/web/components/join-spans.tsx index e6947ee8..c11b4eed 100644 --- a/web/components/join-spans.tsx +++ b/web/components/join-spans.tsx @@ -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) diff --git a/web/components/layout/modal.tsx b/web/components/layout/modal.tsx index 6c6b1af4..d61a38dd 100644 --- a/web/components/layout/modal.tsx +++ b/web/components/layout/modal.tsx @@ -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 }) { diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index fc9dd775..eeab17f9 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -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 } diff --git a/web/components/linkify.tsx b/web/components/linkify.tsx index 3a5f2a18..f33b2bf5 100644 --- a/web/components/linkify.tsx +++ b/web/components/linkify.tsx @@ -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" diff --git a/web/components/nav/nav-bar.tsx b/web/components/nav/nav-bar.tsx index 0cb885f5..40a5aacd 100644 --- a/web/components/nav/nav-bar.tsx +++ b/web/components/nav/nav-bar.tsx @@ -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 }, diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 1948aae2..962593ed 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -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) diff --git a/web/components/number-input.tsx b/web/components/number-input.tsx index a5adb3f8..c40fadeb 100644 --- a/web/components/number-input.tsx +++ b/web/components/number-input.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx' +import React from 'react' import { Col } from './layout/col' import { Spacer } from './layout/spacer' diff --git a/web/components/outcome-label.tsx b/web/components/outcome-label.tsx index 394fb6ae..7cbfa144 100644 --- a/web/components/outcome-label.tsx +++ b/web/components/outcome-label.tsx @@ -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 ( diff --git a/web/components/page.tsx b/web/components/page.tsx index faefb718..64830181 100644 --- a/web/components/page.tsx +++ b/web/components/page.tsx @@ -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 }) { diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index 86fc6c5c..cff47ea5 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -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} diff --git a/web/lib/api/proxy.ts b/web/lib/api/proxy.ts index 6fa66873..0a386730 100644 --- a/web/lib/api/proxy.ts +++ b/web/lib/api/proxy.ts @@ -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 diff --git a/web/lib/firebase/init.ts b/web/lib/firebase/init.ts index 379f7cb6..12f3d832 100644 --- a/web/lib/firebase/init.ts +++ b/web/lib/firebase/init.ts @@ -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) } diff --git a/web/lib/util/copy.ts b/web/lib/util/copy.ts index 47dc4dab..66314612 100644 --- a/web/lib/util/copy.ts +++ b/web/lib/util/copy.ts @@ -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) diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 55618e4c..a833c933 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -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}`) } } diff --git a/web/pages/admin.tsx b/web/pages/admin.tsx index db230e74..e916eb6f 100644 --- a/web/pages/admin.tsx +++ b/web/pages/admin.tsx @@ -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( -
- - {contract.question} - -
- ) - }) + 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( +
+ + {contract.question} + +
+ ) + return { questionLink, ...contract } + }) return ( corpus.toLowerCase().includes(word)) } diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx index ea206f99..ac06eaf2 100644 --- a/web/pages/profile.tsx +++ b/web/pages/profile.tsx @@ -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]) => ( - + {( + [ + ['bio', 'Bio'], + ['website', 'Website URL'], + ['twitterHandle', 'Twitter'], + ['discordHandle', 'Discord'], + ] as const + ).map(([field, label]) => ( + ))} )}