From 7ba19c274bb28cfb6116bdc453f830b003916750 Mon Sep 17 00:00:00 2001 From: Barak Gila Date: Tue, 27 Sep 2022 10:02:03 -0700 Subject: [PATCH 01/13] basic sprig integration with possible page URL events (#932) * basic sprig integration with possible page URL events * iteration 0 * iteration 1 * run prettier; attempt to remove expect error * readd expect error messages * typescript comment fixes * add identify * remove package-lock.json * extract to separate file * fix linting * fix lint * fix lint * fix missing config --- common/envs/dev.ts | 1 + common/envs/prod.ts | 2 ++ web/lib/service/analytics.ts | 4 ++++ web/lib/service/sprig.ts | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 web/lib/service/sprig.ts diff --git a/common/envs/dev.ts b/common/envs/dev.ts index 96ec4dc2..ff3fd37d 100644 --- a/common/envs/dev.ts +++ b/common/envs/dev.ts @@ -18,4 +18,5 @@ export const DEV_CONFIG: EnvConfig = { amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', // this is Phil's deployment twitchBotEndpoint: 'https://king-prawn-app-5btyw.ondigitalocean.app', + sprigEnvironmentId: 'Tu7kRZPm7daP', } diff --git a/common/envs/prod.ts b/common/envs/prod.ts index 3014f4e3..d0469d84 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -3,6 +3,7 @@ export type EnvConfig = { firebaseConfig: FirebaseConfig amplitudeApiKey?: string twitchBotEndpoint?: string + sprigEnvironmentId?: string // IDs for v2 cloud functions -- find these by deploying a cloud function and // examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app @@ -56,6 +57,7 @@ type FirebaseConfig = { export const PROD_CONFIG: EnvConfig = { domain: 'manifold.markets', amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15', + sprigEnvironmentId: 'sQcrq9TDqkib', firebaseConfig: { apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', diff --git a/web/lib/service/analytics.ts b/web/lib/service/analytics.ts index 3ac58055..780d9ba4 100644 --- a/web/lib/service/analytics.ts +++ b/web/lib/service/analytics.ts @@ -6,6 +6,8 @@ import { Identify, } from '@amplitude/analytics-browser' +import * as Sprig from 'web/lib/service/sprig' + import { ENV_CONFIG } from 'common/envs/constants' init(ENV_CONFIG.amplitudeApiKey ?? '', undefined, { includeReferrer: true }) @@ -33,10 +35,12 @@ export const withTracking = export async function identifyUser(userId: string) { setUserId(userId) + Sprig.setUserId(userId) } export async function setUserProperty(property: string, value: string) { const identifyObj = new Identify() identifyObj.set(property, value) await identify(identifyObj) + Sprig.setAttributes({ [property]: value }) } diff --git a/web/lib/service/sprig.ts b/web/lib/service/sprig.ts new file mode 100644 index 00000000..ee6052b7 --- /dev/null +++ b/web/lib/service/sprig.ts @@ -0,0 +1,33 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +// Integrate Sprig + +import { ENV_CONFIG } from 'common/envs/constants' + +try { + ;(function (l, e, a, p) { + if (window.Sprig) return + window.Sprig = function (...args) { + S._queue.push(args) + } + const S = window.Sprig + S.appId = a + S._queue = [] + window.UserLeap = S + a = l.createElement('script') + a.async = 1 + a.src = e + '?id=' + S.appId + p = l.getElementsByTagName('script')[0] + p.parentNode.insertBefore(a, p) + })(document, 'https://cdn.sprig.com/shim.js', ENV_CONFIG.sprigEnvironmentId) +} catch (error) { + console.log('Error initializing Sprig, please complain to Barak', error) +} + +export function setUserId(userId: string): void { + window.Sprig.setUserId(userId) +} + +export function setAttributes(attributes: Record): void { + window.Sprig.setAttributes(attributes) +} From 723d9dbece2924c7091721d5be4f46d6fe6349ca Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Tue, 27 Sep 2022 12:09:54 -0500 Subject: [PATCH 02/13] Better bet summary (#936) * show position, expected value, profit instead of "invested" * move bet summary outside trades on market page * refactor * pass in userbets * hide only if no bets; show invested on desktop * various --- web/components/bet-summary.tsx | 120 +++++++++++++++++ web/components/bets-list.tsx | 155 +--------------------- web/components/contract/contract-tabs.tsx | 64 ++++----- web/components/profit-badge.tsx | 28 ++++ web/pages/[username]/[contractSlug].tsx | 17 ++- web/pages/home/index.tsx | 2 +- 6 files changed, 192 insertions(+), 194 deletions(-) create mode 100644 web/components/bet-summary.tsx create mode 100644 web/components/profit-badge.tsx diff --git a/web/components/bet-summary.tsx b/web/components/bet-summary.tsx new file mode 100644 index 00000000..aa64da43 --- /dev/null +++ b/web/components/bet-summary.tsx @@ -0,0 +1,120 @@ +import { sumBy } from 'lodash' +import clsx from 'clsx' + +import { Bet } from 'web/lib/firebase/bets' +import { formatMoney, formatWithCommas } from 'common/util/format' +import { Col } from './layout/col' +import { Contract } from 'web/lib/firebase/contracts' +import { Row } from './layout/row' +import { YesLabel, NoLabel } from './outcome-label' +import { + calculatePayout, + getContractBetMetrics, + getProbability, +} from 'common/calculate' +import { InfoTooltip } from './info-tooltip' +import { ProfitBadge } from './profit-badge' + +export function BetsSummary(props: { + contract: Contract + userBets: Bet[] + className?: string +}) { + const { contract, className } = props + const { resolution, outcomeType } = contract + const isBinary = outcomeType === 'BINARY' + + const bets = props.userBets.filter((b) => !b.isAnte) + const { profitPercent, payout, profit, invested } = getContractBetMetrics( + contract, + bets + ) + + const excludeSales = bets.filter((b) => !b.isSold && !b.sale) + const yesWinnings = sumBy(excludeSales, (bet) => + calculatePayout(contract, bet, 'YES') + ) + const noWinnings = sumBy(excludeSales, (bet) => + calculatePayout(contract, bet, 'NO') + ) + + const position = yesWinnings - noWinnings + + const prob = isBinary ? getProbability(contract) : 0 + const expectation = prob * yesWinnings + (1 - prob) * noWinnings + + if (bets.length === 0) return <> + + return ( + + + {resolution ? ( + +
Payout
+
+ {formatMoney(payout)}{' '} + +
+ + ) : isBinary ? ( + +
+ Position{' '} + +
+
+ {position > 1e-7 ? ( + <> + {formatWithCommas(position)} + + ) : position < -1e-7 ? ( + <> + {formatWithCommas(-position)} + + ) : ( + '——' + )} +
+ + ) : ( + +
+ Expectation{''} + +
+
{formatMoney(payout)}
+ + )} + + +
+ Invested{' '} + +
+
{formatMoney(invested)}
+ + + {isBinary && !resolution && ( + +
+ Expectation{' '} + +
+
{formatMoney(expectation)}
+ + )} + + +
+ Profit{' '} + +
+
+ {formatMoney(profit)} + +
+ +
+ + ) +} diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index dbb2db56..5a95f22f 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -22,7 +22,7 @@ import { import { Row } from './layout/row' import { sellBet } from 'web/lib/firebase/api' import { ConfirmationButton } from './confirmation-button' -import { OutcomeLabel, YesLabel, NoLabel } from './outcome-label' +import { OutcomeLabel } from './outcome-label' import { LoadingIndicator } from './loading-indicator' import { SiteLink } from './site-link' import { @@ -38,14 +38,14 @@ import { NumericContract } from 'common/contract' import { formatNumericProbability } from 'common/pseudo-numeric' import { useUser } from 'web/hooks/use-user' import { useUserBets } from 'web/hooks/use-user-bets' -import { SellSharesModal } from './sell-modal' import { useUnfilledBets } from 'web/hooks/use-bets' import { LimitBet } from 'common/bet' -import { floatingEqual } from 'common/util/math' import { Pagination } from './pagination' import { LimitOrderTable } from './limit-bets' import { UserLink } from 'web/components/user-link' import { useUserBetContracts } from 'web/hooks/use-contracts' +import { BetsSummary } from './bet-summary' +import { ProfitBadge } from './profit-badge' type BetSort = 'newest' | 'profit' | 'closeTime' | 'value' type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all' @@ -337,8 +337,7 @@ function ContractBets(props: { {contract.mechanism === 'cpmm-1' && limitBets.length > 0 && ( @@ -364,125 +363,6 @@ function ContractBets(props: { ) } -export function BetsSummary(props: { - contract: Contract - bets: Bet[] - isYourBets: boolean - className?: string -}) { - const { contract, isYourBets, className } = props - const { resolution, closeTime, outcomeType, mechanism } = contract - const isBinary = outcomeType === 'BINARY' - const isPseudoNumeric = outcomeType === 'PSEUDO_NUMERIC' - const isCpmm = mechanism === 'cpmm-1' - const isClosed = closeTime && Date.now() > closeTime - - const bets = props.bets.filter((b) => !b.isAnte) - const { hasShares, invested, profitPercent, payout, profit, totalShares } = - getContractBetMetrics(contract, bets) - - const excludeSales = bets.filter((b) => !b.isSold && !b.sale) - const yesWinnings = sumBy(excludeSales, (bet) => - calculatePayout(contract, bet, 'YES') - ) - const noWinnings = sumBy(excludeSales, (bet) => - calculatePayout(contract, bet, 'NO') - ) - - const [showSellModal, setShowSellModal] = useState(false) - const user = useUser() - - const sharesOutcome = floatingEqual(totalShares.YES ?? 0, 0) - ? floatingEqual(totalShares.NO ?? 0, 0) - ? undefined - : 'NO' - : 'YES' - - const canSell = - isYourBets && - isCpmm && - (isBinary || isPseudoNumeric) && - !isClosed && - !resolution && - hasShares && - sharesOutcome && - user - - return ( - - - -
- Invested -
-
{formatMoney(invested)}
- - -
Profit
-
- {formatMoney(profit)} -
- - {canSell && ( - <> - - {showSellModal && ( - - )} - - )} -
- - {resolution ? ( - -
Payout
-
- {formatMoney(payout)}{' '} - -
- - ) : isBinary ? ( - <> - -
- Payout if -
-
- {formatMoney(yesWinnings)} -
- - -
- Payout if -
-
{formatMoney(noWinnings)}
- - - ) : ( - -
- Expected value -
-
{formatMoney(payout)}
- - )} -
- - ) -} - export function ContractBetsTable(props: { contract: Contract bets: Bet[] @@ -750,30 +630,3 @@ function SellButton(props: { ) } - -export function ProfitBadge(props: { - profitPercent: number - round?: boolean - className?: string -}) { - const { profitPercent, round, className } = props - if (!profitPercent) return null - const colors = - profitPercent > 0 - ? 'bg-green-100 text-green-800' - : 'bg-red-100 text-red-800' - - return ( - - {(profitPercent > 0 ? '+' : '') + - profitPercent.toFixed(round ? 0 : 1) + - '%'} - - ) -} diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index 17471796..bd3204ed 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -9,7 +9,7 @@ import { groupBy, sortBy } from 'lodash' import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { PAST_BETS } from 'common/user' -import { ContractBetsTable, BetsSummary } from '../bets-list' +import { ContractBetsTable } from '../bets-list' import { Spacer } from '../layout/spacer' import { Tabs } from '../layout/tabs' import { Col } from '../layout/col' @@ -17,59 +17,45 @@ import { LoadingIndicator } from 'web/components/loading-indicator' import { useComments } from 'web/hooks/use-comments' import { useLiquidity } from 'web/hooks/use-liquidity' import { useTipTxns } from 'web/hooks/use-tip-txns' -import { useUser } from 'web/hooks/use-user' import { capitalize } from 'lodash' import { DEV_HOUSE_LIQUIDITY_PROVIDER_ID, HOUSE_LIQUIDITY_PROVIDER_ID, } from 'common/antes' -import { useIsMobile } from 'web/hooks/use-is-mobile' +import { buildArray } from 'common/util/array' -export function ContractTabs(props: { contract: Contract; bets: Bet[] }) { - const { contract, bets } = props - - const isMobile = useIsMobile() - const user = useUser() - const userBets = - user && bets.filter((bet) => !bet.isAnte && bet.userId === user.id) +export function ContractTabs(props: { + contract: Contract + bets: Bet[] + userBets: Bet[] +}) { + const { contract, bets, userBets } = props const yourTrades = (
- - +
) + const tabs = buildArray( + { + title: 'Comments', + content: , + }, + { + title: capitalize(PAST_BETS), + content: , + }, + userBets.length > 0 && { + title: 'Your trades', + content: yourTrades, + } + ) + return ( - , - }, - { - title: capitalize(PAST_BETS), - content: , - }, - ...(!user || !userBets?.length - ? [] - : [ - { - title: isMobile ? `You` : `Your ${PAST_BETS}`, - content: yourTrades, - }, - ]), - ]} - /> + ) } diff --git a/web/components/profit-badge.tsx b/web/components/profit-badge.tsx new file mode 100644 index 00000000..f82159e6 --- /dev/null +++ b/web/components/profit-badge.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx' + +export function ProfitBadge(props: { + profitPercent: number + round?: boolean + className?: string +}) { + const { profitPercent, round, className } = props + if (!profitPercent) return null + const colors = + profitPercent > 0 + ? 'bg-green-100 text-green-800' + : 'bg-red-100 text-red-800' + + return ( + + {(profitPercent > 0 ? '+' : '') + + profitPercent.toFixed(round ? 0 : 1) + + '%'} + + ) +} diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 38df2fbf..1dde2f95 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,5 +1,6 @@ import React, { memo, useEffect, useMemo, useState } from 'react' import { ArrowLeftIcon } from '@heroicons/react/outline' +import dayjs from 'dayjs' import { useContractWithPreload } from 'web/hooks/use-contract' import { ContractOverview } from 'web/components/contract/contract-overview' @@ -44,8 +45,7 @@ import { useAdmin } from 'web/hooks/use-admin' import { BetSignUpPrompt } from 'web/components/sign-up-prompt' import { PlayMoneyDisclaimer } from 'web/components/play-money-disclaimer' import BetButton from 'web/components/bet-button' - -import dayjs from 'dayjs' +import { BetsSummary } from 'web/components/bet-summary' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -167,6 +167,10 @@ export function ContractPageContent( [bets] ) + const userBets = user + ? bets.filter((bet) => !bet.isAnte && bet.userId === user.id) + : [] + const [showConfetti, setShowConfetti] = useState(false) useEffect(() => { @@ -248,7 +252,14 @@ export function ContractPageContent( )} - + + + + {!user ? ( diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx index b42b37bb..ba2851bf 100644 --- a/web/pages/home/index.tsx +++ b/web/pages/home/index.tsx @@ -33,7 +33,6 @@ import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' import { formatMoney } from 'common/util/format' import { useProbChanges } from 'web/hooks/use-prob-changes' -import { ProfitBadge } from 'web/components/bets-list' import { calculatePortfolioProfit } from 'common/calculate-metrics' import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal' import { ContractsGrid } from 'web/components/contract/contracts-grid' @@ -45,6 +44,7 @@ import { usePrefetch } from 'web/hooks/use-prefetch' import { Title } from 'web/components/title' import { CPMMBinaryContract } from 'common/contract' import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts' +import { ProfitBadge } from 'web/components/profit-badge' import { LoadingIndicator } from 'web/components/loading-indicator' export default function Home() { From 5e34b5a911314ff46369765a018ea43599e65567 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Tue, 27 Sep 2022 13:15:13 -0400 Subject: [PATCH 03/13] greyscale bet button if outcome is undefined --- web/components/bet-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 1b9bddbe..90918283 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -401,7 +401,7 @@ export function BuyPanel(props: { isSubmitting={isSubmitting} openModalButtonClass={clsx( 'btn mb-2 flex-1', - betDisabled + betDisabled || outcome === undefined ? 'btn-disabled bg-greyscale-2' : outcome === 'NO' ? 'border-none bg-red-400 hover:bg-red-500' From e2047210b74e3c7e772679e50128722ac1e1d9fa Mon Sep 17 00:00:00 2001 From: Barak Gila Date: Tue, 27 Sep 2022 12:13:11 -0700 Subject: [PATCH 04/13] add to queue rather than invoking sprig object directly, as it's still being setup (#940) --- web/lib/service/sprig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/lib/service/sprig.ts b/web/lib/service/sprig.ts index ee6052b7..9353020d 100644 --- a/web/lib/service/sprig.ts +++ b/web/lib/service/sprig.ts @@ -25,9 +25,9 @@ try { } export function setUserId(userId: string): void { - window.Sprig.setUserId(userId) + window.Sprig('setUserId', userId) } export function setAttributes(attributes: Record): void { - window.Sprig.setAttributes(attributes) + window.Sprig('setAttributes', attributes) } From 419c7ab636361238940c88c6dbcb53a33ccaf126 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Tue, 27 Sep 2022 17:16:48 -0400 Subject: [PATCH 05/13] Navigate to ?tab=portfolio --- web/components/nav/bottom-nav-bar.tsx | 4 +--- web/components/nav/profile-menu.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/web/components/nav/bottom-nav-bar.tsx b/web/components/nav/bottom-nav-bar.tsx index f906b21d..922e6646 100644 --- a/web/components/nav/bottom-nav-bar.tsx +++ b/web/components/nav/bottom-nav-bar.tsx @@ -20,8 +20,6 @@ import { useIsIframe } from 'web/hooks/use-is-iframe' import { trackCallback } from 'web/lib/service/analytics' import { User } from 'common/user' -import { PAST_BETS } from 'common/user' - function getNavigation() { return [ { name: 'Home', href: '/home', icon: HomeIcon }, @@ -42,7 +40,7 @@ const signedOutNavigation = [ export const userProfileItem = (user: User) => ({ name: formatMoney(user.balance), trackingEventName: 'profile', - href: `/${user.username}?tab=${PAST_BETS}`, + href: `/${user.username}?tab=portfolio`, icon: () => ( + Date: Tue, 27 Sep 2022 17:30:07 -0500 Subject: [PATCH 06/13] Date docs on Manifold (#941) * Date docs * Create date doc * Create and show a date market as well * Move url to date-docs * Date doc individual page * Add share button * Edit date docs * Layout * Add comments for create-post * Add comments and back nav * Fix urls * Tweaks --- common/post.ts | 8 ++ functions/src/api.ts | 2 +- functions/src/create-market.ts | 27 +++-- functions/src/create-post.ts | 39 +++++- web/components/site-link.tsx | 4 +- web/lib/firebase/posts.ts | 24 +++- web/pages/date-docs/[username].tsx | 159 ++++++++++++++++++++++++ web/pages/date-docs/create.tsx | 179 ++++++++++++++++++++++++++++ web/pages/date-docs/index.tsx | 72 +++++++++++ web/pages/post/[...slugs]/index.tsx | 19 +-- web/posts/post-comments.tsx | 2 - 11 files changed, 500 insertions(+), 35 deletions(-) create mode 100644 web/pages/date-docs/[username].tsx create mode 100644 web/pages/date-docs/create.tsx create mode 100644 web/pages/date-docs/index.tsx diff --git a/common/post.ts b/common/post.ts index 05eab685..13a90821 100644 --- a/common/post.ts +++ b/common/post.ts @@ -9,4 +9,12 @@ export type Post = { slug: string } +export type DateDoc = Post & { + bounty: number + birthday: number + photoUrl: string + type: 'date-doc' + contractSlug: string +} + export const MAX_POST_TITLE_LENGTH = 480 diff --git a/functions/src/api.ts b/functions/src/api.ts index 7440f16a..7134c8d8 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -14,7 +14,7 @@ import { export { APIError } from '../../common/api' type Output = Record -type AuthedUser = { +export type AuthedUser = { uid: string creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) } diff --git a/functions/src/create-market.ts b/functions/src/create-market.ts index 300d91f2..d1483ca4 100644 --- a/functions/src/create-market.ts +++ b/functions/src/create-market.ts @@ -16,7 +16,7 @@ import { slugify } from '../../common/util/slugify' import { randomString } from '../../common/util/random' import { chargeUser, getContract, isProd } from './utils' -import { APIError, newEndpoint, validate, zTimestamp } from './api' +import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api' import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy' import { @@ -92,7 +92,11 @@ const multipleChoiceSchema = z.object({ answers: z.string().trim().min(1).array().min(2), }) -export const createmarket = newEndpoint({}, async (req, auth) => { +export const createmarket = newEndpoint({}, (req, auth) => { + return createMarketHelper(req.body, auth) +}) + +export async function createMarketHelper(body: any, auth: AuthedUser) { const { question, description, @@ -101,16 +105,13 @@ export const createmarket = newEndpoint({}, async (req, auth) => { outcomeType, groupId, visibility = 'public', - } = validate(bodySchema, req.body) + } = validate(bodySchema, body) let min, max, initialProb, isLogScale, answers if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') { let initialValue - ;({ min, max, initialValue, isLogScale } = validate( - numericSchema, - req.body - )) + ;({ min, max, initialValue, isLogScale } = validate(numericSchema, body)) if (max - min <= 0.01 || initialValue <= min || initialValue >= max) throw new APIError(400, 'Invalid range.') @@ -126,11 +127,11 @@ export const createmarket = newEndpoint({}, async (req, auth) => { } if (outcomeType === 'BINARY') { - ;({ initialProb } = validate(binarySchema, req.body)) + ;({ initialProb } = validate(binarySchema, body)) } if (outcomeType === 'MULTIPLE_CHOICE') { - ;({ answers } = validate(multipleChoiceSchema, req.body)) + ;({ answers } = validate(multipleChoiceSchema, body)) } const userDoc = await firestore.collection('users').doc(auth.uid).get() @@ -186,17 +187,17 @@ export const createmarket = newEndpoint({}, async (req, auth) => { // convert string descriptions into JSONContent const newDescription = - typeof description === 'string' + !description || typeof description === 'string' ? { type: 'doc', content: [ { type: 'paragraph', - content: [{ type: 'text', text: description }], + content: [{ type: 'text', text: description || ' ' }], }, ], } - : description ?? {} + : description const contract = getNewContract( contractRef.id, @@ -323,7 +324,7 @@ export const createmarket = newEndpoint({}, async (req, auth) => { } return contract -}) +} const getSlug = async (question: string) => { const proposedSlug = slugify(question) diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index 113a34bd..a342dc05 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -7,6 +7,9 @@ import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post' import { APIError, newEndpoint, validate } from './api' import { JSONContent } from '@tiptap/core' import { z } from 'zod' +import { removeUndefinedProps } from '../../common/util/object' +import { createMarketHelper } from './create-market' +import { DAY_MS } from '../../common/util/time' const contentSchema: z.ZodType = z.lazy(() => z.intersection( @@ -35,11 +38,21 @@ const postSchema = z.object({ title: z.string().min(1).max(MAX_POST_TITLE_LENGTH), content: contentSchema, groupId: z.string().optional(), + + // Date doc fields: + bounty: z.number().optional(), + birthday: z.number().optional(), + photoUrl: z.string().optional(), + type: z.string().optional(), + question: z.string().optional(), }) export const createpost = newEndpoint({}, async (req, auth) => { const firestore = admin.firestore() - const { title, content, groupId } = validate(postSchema, req.body) + const { title, content, groupId, question, ...otherProps } = validate( + postSchema, + req.body + ) const creator = await getUser(auth.uid) if (!creator) @@ -51,14 +64,34 @@ export const createpost = newEndpoint({}, async (req, auth) => { const postRef = firestore.collection('posts').doc() - const post: Post = { + // If this is a date doc, create a market for it. + let contractSlug + if (question) { + const closeTime = Date.now() + DAY_MS * 30 * 3 + + const result = await createMarketHelper( + { + question, + closeTime, + outcomeType: 'BINARY', + visibility: 'unlisted', + initialProb: 50, + }, + auth + ) + contractSlug = result.slug + } + + const post: Post = removeUndefinedProps({ + ...otherProps, id: postRef.id, creatorId: creator.id, slug, title, createdTime: Date.now(), content: content, - } + contractSlug, + }) await postRef.create(post) if (groupId) { diff --git a/web/components/site-link.tsx b/web/components/site-link.tsx index f395e6a9..2b97f07d 100644 --- a/web/components/site-link.tsx +++ b/web/components/site-link.tsx @@ -6,13 +6,15 @@ export const linkClass = 'z-10 break-anywhere hover:underline hover:decoration-indigo-400 hover:decoration-2' export const SiteLink = (props: { - href: string + href: string | undefined children?: ReactNode onClick?: () => void className?: string }) => { const { href, children, onClick, className } = props + if (!href) return <>{children} + return ( ('posts') @@ -44,3 +45,22 @@ export async function listPosts(postIds?: string[]) { if (postIds === undefined) return [] return Promise.all(postIds.map(getPost)) } + +export async function getDateDocs() { + const q = query(posts, where('type', '==', 'date-doc')) + return getValues(q) +} + +export async function getDateDoc(username: string) { + const user = await getUserByUsername(username) + if (!user) return null + + const q = query( + posts, + where('type', '==', 'date-doc'), + where('creatorId', '==', user.id) + ) + const docs = await getValues(q) + const post = docs.length === 0 ? null : docs[0] + return { post, user } +} diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx new file mode 100644 index 00000000..d6ac37cd --- /dev/null +++ b/web/pages/date-docs/[username].tsx @@ -0,0 +1,159 @@ +import { getDateDoc } from 'web/lib/firebase/posts' +import { ArrowLeftIcon, LinkIcon } from '@heroicons/react/outline' +import { Page } from 'web/components/page' +import dayjs from 'dayjs' + +import { DateDoc } from 'common/post' +import { Content } from 'web/components/editor' +import { Col } from 'web/components/layout/col' +import { Row } from 'web/components/layout/row' +import { SiteLink } from 'web/components/site-link' +import { User } from 'web/lib/firebase/users' +import { DOMAIN } from 'common/envs/constants' +import Custom404 from '../404' +import { ShareIcon } from '@heroicons/react/solid' +import clsx from 'clsx' +import { Button } from 'web/components/button' +import { track } from '@amplitude/analytics-browser' +import toast from 'react-hot-toast' +import { copyToClipboard } from 'web/lib/util/copy' +import { useUser } from 'web/hooks/use-user' +import { PostCommentsActivity, RichEditPost } from '../post/[...slugs]' +import { usePost } from 'web/hooks/use-post' +import { useTipTxns } from 'web/hooks/use-tip-txns' +import { useCommentsOnPost } from 'web/hooks/use-comments' + +export async function getStaticProps(props: { params: { username: string } }) { + const { username } = props.params + const { user: creator, post } = (await getDateDoc(username)) ?? { + creator: null, + post: null, + } + + return { + props: { + creator, + post, + }, + revalidate: 5, // regenerate after five seconds + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: 'blocking' } +} + +export default function DateDocPageHelper(props: { + creator: User | null + post: DateDoc | null +}) { + const { creator, post } = props + + if (!creator || !post) return + + return +} + +function DateDocPage(props: { creator: User; post: DateDoc }) { + const { creator, post } = props + + const tips = useTipTxns({ postId: post.id }) + const comments = useCommentsOnPost(post.id) ?? [] + + return ( + + + + + + + + +
Add your endorsement of {creator.name}!
+ + + +
+ ) +} + +export function DateDocPost(props: { + dateDoc: DateDoc + creator: User + link?: boolean +}) { + const { dateDoc, creator, link } = props + const { content, birthday, photoUrl, contractSlug } = dateDoc + const { name, username } = creator + + const user = useUser() + const post = usePost(dateDoc.id) ?? dateDoc + + const age = dayjs().diff(birthday, 'year') + const shareUrl = `https://${DOMAIN}/date-docs/${username}` + const marketUrl = `https://${DOMAIN}/${username}/${contractSlug}` + + return ( + + + + +
+ {name}, {age} +
+ + + + +
+ {name} + +
+ {user && user.id === creator.id ? ( + + ) : ( + + )} +
+ +
+ + ) +} diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx new file mode 100644 index 00000000..5d72da42 --- /dev/null +++ b/web/pages/date-docs/create.tsx @@ -0,0 +1,179 @@ +import Router from 'next/router' +import { useEffect, useState } from 'react' +import Textarea from 'react-expanding-textarea' + +import { DateDoc } from 'common/post' +import { useTextEditor, TextEditor } from 'web/components/editor' +import { Page } from 'web/components/page' +import { Title } from 'web/components/title' +import { useUser } from 'web/hooks/use-user' +import { createPost } from 'web/lib/firebase/api' +import { Row } from 'web/components/layout/row' +import { Button } from 'web/components/button' +import dayjs from 'dayjs' +import { MINUTE_MS } from 'common/util/time' +import { Col } from 'web/components/layout/col' +import { uploadImage } from 'web/lib/firebase/storage' +import { LoadingIndicator } from 'web/components/loading-indicator' +import { MAX_QUESTION_LENGTH } from 'common/contract' + +export default function CreateDateDocPage() { + const user = useUser() + + useEffect(() => { + if (user === null) Router.push('/date') + }) + + const title = `${user?.name}'s Date Doc` + const [birthday, setBirthday] = useState(undefined) + const [photoUrl, setPhotoUrl] = useState('') + const [avatarLoading, setAvatarLoading] = useState(false) + const [question, setQuestion] = useState( + 'Will I find a partner in the next 3 months?' + ) + + const [isSubmitting, setIsSubmitting] = useState(false) + + const { editor, upload } = useTextEditor({ + disabled: isSubmitting, + }) + + const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined + const isValid = + user && + birthday && + photoUrl && + editor && + editor.isEmpty === false && + question + + const fileHandler = async (event: any) => { + if (!user) return + + const file = event.target.files[0] + + setAvatarLoading(true) + + await uploadImage(user.username, file) + .then(async (url) => { + setPhotoUrl(url) + setAvatarLoading(false) + }) + .catch(() => { + setAvatarLoading(false) + setPhotoUrl('') + }) + } + + async function saveDateDoc() { + if (!user || !editor || !birthdayTime) return + + const newPost: Omit< + DateDoc, + 'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug' + > & { question: string } = { + title, + content: editor.getJSON(), + bounty: 0, + birthday: birthdayTime, + photoUrl, + type: 'date-doc', + question, + } + + const result = await createPost(newPost) + + if (result.post) { + await Router.push(`/date-docs/${user.username}`) + } + } + + return ( + +
+
+ + + <Button + type="submit" + disabled={isSubmitting || !isValid || upload.isLoading} + onClick={async () => { + setIsSubmitting(true) + await saveDateDoc() + setIsSubmitting(false) + }} + color="blue" + > + {isSubmitting ? 'Publishing...' : 'Publish'} + </Button> + </Row> + + <Col className="gap-8"> + <Col className="max-w-[160px] justify-start gap-4"> + <div className="">Birthday</div> + <input + type={'date'} + className="input input-bordered" + onClick={(e) => e.stopPropagation()} + onChange={(e) => setBirthday(e.target.value)} + max={Math.round(Date.now() / MINUTE_MS) * MINUTE_MS} + disabled={isSubmitting} + value={birthday} + /> + </Col> + + <Col className="gap-4"> + <div className="">Photo</div> + <Row className="items-center gap-4"> + {avatarLoading ? ( + <LoadingIndicator /> + ) : ( + <> + {photoUrl && ( + <img + src={photoUrl} + width={80} + height={80} + className="flex h-[80px] w-[80px] items-center justify-center rounded-lg bg-gray-400 object-cover" + /> + )} + <input + className="text-sm text-gray-500" + type="file" + name="file" + accept="image/*" + onChange={fileHandler} + /> + </> + )} + </Row> + </Col> + + <Col className="gap-4"> + <div className=""> + Tell us about you! What are you looking for? + </div> + <TextEditor editor={editor} upload={upload} /> + </Col> + + <Col className="gap-4"> + <div className=""> + Finally, we'll create an (unlisted) prediction market! + </div> + + <Col className="gap-2"> + <Textarea + className="input input-bordered resize-none" + maxLength={MAX_QUESTION_LENGTH} + value={question} + onChange={(e) => setQuestion(e.target.value || '')} + /> + <div className="ml-2 text-gray-500">Cost: M$100</div> + </Col> + </Col> + </Col> + </div> + </div> + </Page> + ) +} diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx new file mode 100644 index 00000000..d2dd874c --- /dev/null +++ b/web/pages/date-docs/index.tsx @@ -0,0 +1,72 @@ +import { Page } from 'web/components/page' +import { PlusCircleIcon } from '@heroicons/react/outline' + +import { getDateDocs } from 'web/lib/firebase/posts' +import type { DateDoc } from 'common/post' +import { Title } from 'web/components/title' +import { Spacer } from 'web/components/layout/spacer' +import { Col } from 'web/components/layout/col' +import { useUser } from 'web/hooks/use-user' +import { Row } from 'web/components/layout/row' +import { Button } from 'web/components/button' +import { SiteLink } from 'web/components/site-link' +import { getUser, User } from 'web/lib/firebase/users' +import { DateDocPost } from './[username]' + +export async function getStaticProps() { + const dateDocs = await getDateDocs() + const docCreators = await Promise.all( + dateDocs.map((d) => getUser(d.creatorId)) + ) + + return { + props: { + dateDocs, + docCreators, + }, + + revalidate: 60, // regenerate after a minute + } +} + +export default function DatePage(props: { + dateDocs: DateDoc[] + docCreators: User[] +}) { + const { dateDocs, docCreators } = props + const user = useUser() + + const hasDoc = dateDocs.some((d) => d.creatorId === user?.id) + + return ( + <Page> + <div className="mx-auto w-full max-w-xl"> + <Row className="items-center justify-between"> + <Title className="!my-0 px-2 text-blue-500" text="Date docs" /> + {!hasDoc && ( + <SiteLink href="/date-docs/create" className="!no-underline"> + <Button className="flex flex-row gap-1" color="blue"> + <PlusCircleIcon + className={'h-5 w-5 flex-shrink-0 text-white'} + aria-hidden="true" + /> + New + </Button> + </SiteLink> + )} + </Row> + <Spacer h={6} /> + <Col className="gap-4"> + {dateDocs.map((dateDoc, i) => ( + <DateDocPost + key={dateDoc.id} + dateDoc={dateDoc} + creator={docCreators[i]} + link + /> + ))} + </Col> + </div> + </Page> + ) +} diff --git a/web/pages/post/[...slugs]/index.tsx b/web/pages/post/[...slugs]/index.tsx index 6cd4408f..b71b7cca 100644 --- a/web/pages/post/[...slugs]/index.tsx +++ b/web/pages/post/[...slugs]/index.tsx @@ -34,9 +34,9 @@ export async function getStaticProps(props: { params: { slugs: string[] } }) { return { props: { - post: post, - creator: creator, - comments: comments, + post, + creator, + comments, }, revalidate: 60, // regenerate after a minute @@ -117,12 +117,7 @@ export default function PostPage(props: { <Spacer h={4} /> <div className="rounded-lg bg-white px-6 py-4 sm:py-0"> - <PostCommentsActivity - post={post} - comments={comments} - tips={tips} - user={creator} - /> + <PostCommentsActivity post={post} comments={comments} tips={tips} /> </div> </div> </Page> @@ -133,9 +128,8 @@ export function PostCommentsActivity(props: { post: Post comments: PostComment[] tips: CommentTipMap - user: User | null | undefined }) { - const { post, comments, user, tips } = props + const { post, comments, tips } = props const commentsByUserId = groupBy(comments, (c) => c.userId) const commentsByParentId = groupBy(comments, (c) => c.replyToCommentId ?? '_') const topLevelComments = sortBy( @@ -149,7 +143,6 @@ export function PostCommentsActivity(props: { {topLevelComments.map((parent) => ( <PostCommentThread key={parent.id} - user={user} post={post} parentComment={parent} threadComments={sortBy( @@ -164,7 +157,7 @@ export function PostCommentsActivity(props: { ) } -function RichEditPost(props: { post: Post }) { +export function RichEditPost(props: { post: Post }) { const { post } = props const [editing, setEditing] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index b98887bb..f1d50a29 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -3,7 +3,6 @@ import { Editor } from '@tiptap/core' import clsx from 'clsx' import { PostComment } from 'common/comment' import { Post } from 'common/post' -import { User } from 'common/user' import { Dictionary } from 'lodash' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' @@ -21,7 +20,6 @@ import { createCommentOnPost } from 'web/lib/firebase/comments' import { firebaseLogin } from 'web/lib/firebase/users' export function PostCommentThread(props: { - user: User | null | undefined post: Post threadComments: PostComment[] tips: CommentTipMap From 80d4bffc95390e5455f088f4237b0a14c6718cf9 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:50:43 -0500 Subject: [PATCH 07/13] US Elections map (#943) * usa map * state election map * senate midterms * iframe * fix * /midterms * listen for updates --- web/components/usa-map/data.tsx | 302 ++++++++++++++++++ web/components/usa-map/state-election-map.tsx | 85 +++++ web/components/usa-map/usa-map.tsx | 106 ++++++ web/components/usa-map/usa-state.tsx | 34 ++ web/pages/midterms.tsx | 92 ++++++ 5 files changed, 619 insertions(+) create mode 100644 web/components/usa-map/data.tsx create mode 100644 web/components/usa-map/state-election-map.tsx create mode 100644 web/components/usa-map/usa-map.tsx create mode 100644 web/components/usa-map/usa-state.tsx create mode 100644 web/pages/midterms.tsx diff --git a/web/components/usa-map/data.tsx b/web/components/usa-map/data.tsx new file mode 100644 index 00000000..b198c4f0 --- /dev/null +++ b/web/components/usa-map/data.tsx @@ -0,0 +1,302 @@ +export const DATA = { + AK: { + dimensions: + 'M161.1,453.7 l-0.3,85.4 1.6,1 3.1,0.2 1.5,-1.1 h2.6 l0.2,2.9 7,6.8 0.5,2.6 3.4,-1.9 0.6,-0.2 0.3,-3.1 1.5,-1.6 1.1,-0.2 1.9,-1.5 3.1,2.1 0.6,2.9 1.9,1.1 1.1,2.4 3.9,1.8 3.4,6 2.7,3.9 2.3,2.7 1.5,3.7 5,1.8 5.2,2.1 1,4.4 0.5,3.1 -1,3.4 -1.8,2.3 -1.6,-0.8 -1.5,-3.1 -2.7,-1.5 -1.8,-1.1 -0.8,0.8 1.5,2.7 0.2,3.7 -1.1,0.5 -1.9,-1.9 -2.1,-1.3 0.5,1.6 1.3,1.8 -0.8,0.8 c0,0 -0.8,-0.3 -1.3,-1 -0.5,-0.6 -2.1,-3.4 -2.1,-3.4 l-1,-2.3 c0,0 -0.3,1.3 -1,1 -0.6,-0.3 -1.3,-1.5 -1.3,-1.5 l1.8,-1.9 -1.5,-1.5 v-5 h-0.8 l-0.8,3.4 -1.1,0.5 -1,-3.7 -0.6,-3.7 -0.8,-0.5 0.3,5.7 v1.1 l-1.5,-1.3 -3.6,-6 -2.1,-0.5 -0.6,-3.7 -1.6,-2.9 -1.6,-1.1 v-2.3 l2.1,-1.3 -0.5,-0.3 -2.6,0.6 -3.4,-2.4 -2.6,-2.9 -4.8,-2.6 -4,-2.6 1.3,-3.2 v-1.6 l-1.8,1.6 -2.9,1.1 -3.7,-1.1 -5.7,-2.4 h-5.5 l-0.6,0.5 -6.5,-3.9 -2.1,-0.3 -2.7,-5.8 -3.6,0.3 -3.6,1.5 0.5,4.5 1.1,-2.9 1,0.3 -1.5,4.4 3.2,-2.7 0.6,1.6 -3.9,4.4 -1.3,-0.3 -0.5,-1.9 -1.3,-0.8 -1.3,1.1 -2.7,-1.8 -3.1,2.1 -1.8,2.1 -3.4,2.1 -4.7,-0.2 -0.5,-2.1 3.7,-0.6 v-1.3 l-2.3,-0.6 1,-2.4 2.3,-3.9 v-1.8 l0.2,-0.8 4.4,-2.3 1,1.3 h2.7 l-1.3,-2.6 -3.7,-0.3 -5,2.7 -2.4,3.4 -1.8,2.6 -1.1,2.3 -4.2,1.5 -3.1,2.6 -0.3,1.6 2.3,1 0.8,2.1 -2.7,3.2 -6.5,4.2 -7.8,4.2 -2.1,1.1 -5.3,1.1 -5.3,2.3 1.8,1.3 -1.5,1.5 -0.5,1.1 -2.7,-1 -3.2,0.2 -0.8,2.3 h-1 l0.3,-2.4 -3.6,1.3 -2.9,1 -3.4,-1.3 -2.9,1.9 h-3.2 l-2.1,1.3 -1.6,0.8 -2.1,-0.3 -2.6,-1.1 -2.3,0.6 -1,1 -1.6,-1.1 v-1.9 l3.1,-1.3 6.3,0.6 4.4,-1.6 2.1,-2.1 2.9,-0.6 1.8,-0.8 2.7,0.2 1.6,1.3 1,-0.3 2.3,-2.7 3.1,-1 3.4,-0.6 1.3,-0.3 0.6,0.5 h0.8 l1.3,-3.7 4,-1.5 1.9,-3.7 2.3,-4.5 1.6,-1.5 0.3,-2.6 -1.6,1.3 -3.4,0.6 -0.6,-2.4 -1.3,-0.3 -1,1 -0.2,2.9 -1.5,-0.2 -1.5,-5.8 -1.3,1.3 -1.1,-0.5 -0.3,-1.9 -4,0.2 -2.1,1.1 -2.6,-0.3 1.5,-1.5 0.5,-2.6 -0.6,-1.9 1.5,-1 1.3,-0.2 -0.6,-1.8 v-4.4 l-1,-1 -0.8,1.5 h-6.1 l-1.5,-1.3 -0.6,-3.9 -2.1,-3.6 v-1 l2.1,-0.8 0.2,-2.1 1.1,-1.1 -0.8,-0.5 -1.3,0.5 -1.1,-2.7 1,-5 4.5,-3.2 2.6,-1.6 1.9,-3.7 2.7,-1.3 2.6,1.1 0.3,2.4 2.4,-0.3 3.2,-2.4 1.6,0.6 1,0.6 h1.6 l2.3,-1.3 0.8,-4.4 c0,0 0.3,-2.9 1,-3.4 0.6,-0.5 1,-1 1,-1 l-1.1,-1.9 -2.6,0.8 -3.2,0.8 -1.9,-0.5 -3.6,-1.8 -5,-0.2 -3.6,-3.7 0.5,-3.9 0.6,-2.4 -2.1,-1.8 -1.9,-3.7 0.5,-0.8 6.8,-0.5 h2.1 l1,1 h0.6 l-0.2,-1.6 3.9,-0.6 2.6,0.3 1.5,1.1 -1.5,2.1 -0.5,1.5 2.7,1.6 5,1.8 1.8,-1 -2.3,-4.4 -1,-3.2 1,-0.8 -3.4,-1.9 -0.5,-1.1 0.5,-1.6 -0.8,-3.9 -2.9,-4.7 -2.4,-4.2 2.9,-1.9 h3.2 l1.8,0.6 4.2,-0.2 3.7,-3.6 1.1,-3.1 3.7,-2.4 1.6,1 2.7,-0.6 3.7,-2.1 1.1,-0.2 1,0.8 4.5,-0.2 2.7,-3.1 h1.1 l3.6,2.4 1.9,2.1 -0.5,1.1 0.6,1.1 1.6,-1.6 3.9,0.3 0.3,3.7 1.9,1.5 7.1,0.6 6.3,4.2 1.5,-1 5.2,2.6 2.1,-0.6 1.9,-0.8 4.8,1.9z m-115.1,28.9 2.1,5.3 -0.2,1 -2.9,-0.3 -1.8,-4 -1.8,-1.5 h-2.4 l-0.2,-2.6 1.8,-2.4 1.1,2.4 1.5,1.5z m-2.6,33.5 3.7,0.8 3.7,1 0.8,1 -1.6,3.7 -3.1,-0.2 -3.4,-3.6z m-20.7,-14.1 1.1,2.6 1.1,1.6 -1.1,0.8 -2.1,-3.1 v-1.9z m-13.7,73.1 3.4,-2.3 3.4,-1 2.6,0.3 0.5,1.6 1.9,0.5 1.9,-1.9 -0.3,-1.6 2.7,-0.6 2.9,2.6 -1.1,1.8 -4.4,1.1 -2.7,-0.5 -3.7,-1.1 -4.4,1.5 -1.6,0.3z m48.9,-4.5 1.6,1.9 2.1,-1.6 -1.5,-1.3z m2.9,3 1.1,-2.3 2.1,0.3 -0.8,1.9 h-2.4z m23.6,-1.9 1.5,1.8 1,-1.1 -0.8,-1.9z m8.8,-12.5 1.1,5.8 2.9,0.8 5,-2.9 4.4,-2.6 -1.6,-2.4 0.5,-2.4 -2.1,1.3 -2.9,-0.8 1.6,-1.1 1.9,0.8 3.9,-1.8 0.5,-1.5 -2.4,-0.8 0.8,-1.9 -2.7,1.9 -4.7,3.6 -4.8,2.9z m42.3,-19.8 2.4,-1.5 -1,-1.8 -1.8,1z', + abbreviation: 'AK', + name: 'Alaska', + }, + HI: { + dimensions: + 'M233.1,519.3 l1.9,-3.6 2.3,-0.3 0.3,0.8 -2.1,3.1z m10.2,-3.7 6.1,2.6 2.1,-0.3 1.6,-3.9 -0.6,-3.4 -4.2,-0.5 -4,1.8z m30.7,10 3.7,5.5 2.4,-0.3 1.1,-0.5 1.5,1.3 3.7,-0.2 1,-1.5 -2.9,-1.8 -1.9,-3.7 -2.1,-3.6 -5.8,2.9z m20.2,8.9 1.3,-1.9 4.7,1 0.6,-0.5 6.1,0.6 -0.3,1.3 -2.6,1.5 -4.4,-0.3z m5.3,5.2 1.9,3.9 3.1,-1.1 0.3,-1.6 -1.6,-2.1 -3.7,-0.3z m7,-1.2 2.3,-2.9 4.7,2.4 4.4,1.1 4.4,2.7 v1.9 l-3.6,1.8 -4.8,1 -2.4,-1.5z m16.6,15.6 1.6,-1.3 3.4,1.6 7.6,3.6 3.4,2.1 1.6,2.4 1.9,4.4 4,2.6 -0.3,1.3 -3.9,3.2 -4.2,1.5 -1.5,-0.6 -3.1,1.8 -2.4,3.2 -2.3,2.9 -1.8,-0.2 -3.6,-2.6 -0.3,-4.5 0.6,-2.4 -1.6,-5.7 -2.1,-1.8 -0.2,-2.6 2.3,-1 2.1,-3.1 0.5,-1 -1.6,-1.8z', + abbreviation: 'HI', + name: 'Hawaii', + }, + AL: { + dimensions: + 'M628.5,466.4 l0.6,0.2 1.3,-2.7 1.5,-4.4 2.3,0.6 3.1,6 v1 l-2.7,1.9 2.7,0.3 5.2,-2.5 -0.3,-7.6 -2.5,-1.8 -2,-2 0.4,-4 10.5,-1.5 25.7,-2.9 6.7,-0.6 5.6,0.1 -0.5,-2.2 -1.5,-0.8 -0.9,-1.1 1,-2.6 -0.4,-5.2 -1.6,-4.5 0.8,-5.1 1.7,-4.8 -0.2,-1.7 -1.8,-0.7 -0.5,-3.6 -2.7,-3.4 -2,-6.5 -1.4,-6.7 -1.8,-5 -3.8,-16 -3.5,-7.9 -0.8,-5.6 0.1,-2.2 -9,0.8 -23.4,2.2 -12.2,0.8 -0.2,6.4 0.2,16.7 -0.7,31 -0.3,14.1 2.8,18.8 1.6,14.7z', + abbreviation: 'AL', + name: 'Alabama', + }, + AR: { + dimensions: + 'M587.3,346.1 l-6.4,-0.7 0.9,-3.1 3.1,-2.6 0.6,-2.3 -1.8,-2.9 -31.9,1.2 -23.3,0.7 -23.6,0.3 1.5,6.9 0.1,8.5 1.4,10.9 0.3,38.2 2.1,1.6 3,-1.2 2.9,1.2 0.4,10.1 25.2,-0.2 26.8,-0.8 0.9,-1.9 -0.3,-3.8 -1.7,-3.1 1.5,-1.4 -1.4,-2.2 0.7,-2.4 1.1,-5.9 2.7,-2.3 -0.8,-2.2 4,-5.6 2.5,-1.1 -0.1,-1.7 -0.5,-1.7 2.9,-5.8 2.5,-1.1 0.2,-3.3 2.1,-1.4 0.9,-4.1 -1.4,-4 4.2,-2.4 0.3,-2.1 1.2,-4.2 0.9,-3.1z', + abbreviation: 'AR', + name: 'Arkansas', + }, + AZ: { + dimensions: + 'M135.1,389.7 l-0.3,1.5 0.5,1 18.9,10.7 12.1,7.6 14.7,8.6 16.8,10 12.3,2.4 25.4,2.7 6,-39.6 7,-53.1 4.4,-31 -24.6,-3.6 -60.7,-11 -0.2,1.1 -2.6,16.5 -2.1,3.8 -2.8,-0.2 -1.2,-2.6 -2.6,-0.4 -1.2,-1.1 -1.1,0.1 -2.1,1.7 -0.3,6.8 -0.3,1.5 -0.5,12.5 -1.5,2.4 -0.4,3.3 2.8,5 1.1,5.5 0.7,1.1 1.1,0.9 -0.4,2.4 -1.7,1.2 -3.4,1.6 -1.6,1.8 -1.6,3.6 -0.5,4.9 -3,2.9 -1.9,0.9 -0.1,5.8 -0.6,1.6 0.5,0.8 3.9,0.4 -0.9,3 -1.7,2.4 -3.7,0.4z', + abbreviation: 'AZ', + name: 'Arizona', + }, + CA: { + dimensions: + 'M122.7,385.9 l-19.7,-2.7 -10,-1.5 -0.5,-1.8 v-9.4 l-0.3,-3.2 -2.6,-4.2 -0.8,-2.3 -3.9,-4.2 -2.9,-4.7 -2.7,-0.2 -3.2,-0.8 -0.3,-1 1.5,-0.6 -0.6,-3.2 -1.5,-2.1 -4.8,-0.8 -3.9,-2.1 -1.1,-2.3 -2.6,-4.8 -2.9,-3.1 h-2.9 l-3.9,-2.1 -4.5,-1.8 -4.2,-0.5 -2.4,-2.7 0.5,-1.9 1.8,-7.1 0.8,-1.9 v-2.4 l-1.6,-1 -0.5,-2.9 -1.5,-2.6 -3.4,-5.8 -1.3,-3.1 -1.5,-4.7 -1.6,-5.3 -3.2,-4.4 -0.5,-2.9 0.8,-3.9 h1.1 l2.1,-1.6 1.1,-3.6 -1,-2.7 -2.7,-0.5 -1.9,-2.6 -2.1,-3.7 -0.2,-8.2 0.6,-1.9 0.6,-2.3 0.5,-2.4 -5.7,-6.3 v-2.1 l0.3,-0.5 0.3,-3.2 -1.3,-4 -2.3,-4.8 -2.7,-4.5 -1.8,-3.9 1,-3.7 0.6,-5.8 1.8,-3.1 0.3,-6.5 -1.1,-3.6 -1.6,-4.2 -2.7,-4.2 0.8,-3.2 1.5,-4.2 1.8,-0.8 0.3,-1.1 3.1,-2.6 5.2,-11.8 0.2,-7.4 1.69,-4.9 38.69,11.8 25.6,6.6 -8,31.3 -8.67,33.1 12.63,19.2 42.16,62.3 17.1,26.1 -0.4,3.1 2.8,5.2 1.1,5.4 1,1.5 0.7,0.6 -0.2,1.4 -1.4,1 -3.4,1.6 -1.9,2.1 -1.7,3.9 -0.5,4.7 -2.6,2.5 -2.3,1.1 -0.1,6.2 -0.6,1.9 1,1.7 3,0.3 -0.4,1.6 -1.4,2 -3.9,0.6z m-73.9,-48.9 1.3,1.5 -0.2,1.3 -3.2,-0.1 -0.6,-1.2 -0.6,-1.5z m1.9,0 1.2,-0.6 3.6,2.1 3.1,1.2 -0.9,0.6 -4.5,-0.2 -1.6,-1.6z m20.7,19.8 1.8,2.3 0.8,1 1.5,0.6 0.6,-1.5 -1,-1.8 -2.7,-2 -1.1,0.2 v1.2z m-1.4,8.7 1.8,3.2 1.2,1.9 -1.5,0.2 -1.3,-1.2 c0,0 -0.7,-1.5 -0.7,-1.9 0,-0.4 0,-2.2 0,-2.2z', + abbreviation: 'CA', + name: 'California', + }, + CO: { + dimensions: + 'M380.2,235.5 l-36,-3.5 -79.1,-8.6 -2.2,22.1 -7,50.4 -1.9,13.7 34,3.9 37.5,4.4 34.7,3 14.3,0.6z', + abbreviation: 'CO', + name: 'Colorado', + }, + CT: { + dimensions: + 'M852,190.9 l3.6,-3.2 1.9,-2.1 0.8,0.6 2.7,-1.5 5.2,-1.1 7,-3.5 -0.6,-4.2 -0.8,-4.4 -1.6,-6 -4.3,1.1 -21.8,4.7 0.6,3.1 1.5,7.3 v8.3 l-0.9,2.1 1.7,2.2z', + abbreviation: 'CT', + name: 'Connecticut', + }, + DE: { + dimensions: + 'M834.4,247.2 l-1,0.5 -3.6,-2.4 -1.8,-4.7 -1.9,-3.6 -2.3,-1 -2.1,-3.6 0.5,-2 0.5,-2.3 0.1,-1.1 -0.6,0.1 -1.7,1 -2,1.7 -0.2,0.3 1.4,4.1 2.3,5.6 3.7,16.1 5,-0.3 6,-1.1z', + abbreviation: 'DE', + name: 'Delaware', + }, + FL: { + dimensions: + 'M750.2,445.2 l-5.2,-0.7 -0.7,0.8 1.5,4.4 -0.4,5.2 -4.1,-1 -0.2,-2.8 h-4.1 l-5.3,0.7 -32.4,1.9 -8.2,-0.3 -1.7,-1.7 -2.5,-4.2 h-5.9 l-6.6,0.5 -35.4,4.2 -0.3,2.8 1.6,1.6 2.9,2 0.3,8.4 3.3,-0.6 6,-2.1 6,-0.5 4.4,-0.6 7.6,1.8 8.1,3.9 1.6,1.5 2.9,1.1 1.6,1.9 0.3,2.7 3.2,-1.3 h3.9 l3.6,-1.9 3.7,-3.6 3.1,0.2 0.5,-1.1 -0.8,-1 0.2,-1.9 4,-0.8 h2.6 l2.9,1.5 4.2,1.5 2.4,3.7 2.7,1 1.1,3.4 3.4,1.6 1.6,2.6 1.9,0.6 5.2,1.3 1.3,3.1 3,3.7 v9.5 l-1.5,4.7 0.3,2.7 1.3,4.8 1.8,4 0.8,-0.5 1.5,-4.5 -2.6,-1 -0.3,-0.6 1.6,-0.6 4.5,1 0.2,1.6 -3.2,5.5 -2.1,2.4 3.6,3.7 2.6,3.1 2.9,5.3 2.9,3.9 2.1,5 1.8,0.3 1.6,-2.1 1.8,1.1 2.6,4 0.6,3.6 3.1,4.4 0.8,-1.3 3.9,0.3 3.6,2.3 3.4,5.2 0.8,3.4 0.3,2.9 1.1,1 1.3,0.5 2.4,-1 1.5,-1.6 3.9,-0.2 3.1,-1.5 2.7,-3.2 -0.5,-1.9 -0.3,-2.4 0.6,-1.9 -0.3,-1.9 2.4,-1.3 0.3,-3.4 -0.6,-1.8 -0.5,-12 -1.3,-7.6 -4.5,-8.2 -3.6,-5.8 -2.6,-5.3 -2.9,-2.9 -2.9,-7.4 0.7,-1.4 1.1,-1.3 -1.6,-2.9 -4,-3.7 -4.8,-5.5 -3.7,-6.3 -5.3,-9.4 -3.7,-9.7 -2.3,-7.3z m17.7,132.7 2.4,-0.6 1.3,-0.2 1.5,-2.3 2.3,-1.6 1.3,0.5 1.7,0.3 0.4,1.1 -3.5,1.2 -4.2,1.5 -2.3,1.2z m13.5,-5 1.2,1.1 2.7,-2.1 5.3,-4.2 3.7,-3.9 2.5,-6.6 1,-1.7 0.2,-3.4 -0.7,0.5 -1,2.8 -1.5,4.6 -3.2,5.3 -4.4,4.2 -3.4,1.9z', + abbreviation: 'FL', + name: 'Florida', + }, + GA: { + dimensions: + 'M750.2,444.2 l-5.6,-0.7 -1.4,1.6 1.6,4.7 -0.3,3.9 -2.2,-0.6 -0.2,-3 h-5.2 l-5.3,0.7 -32.3,1.9 -7.7,-0.3 -1.4,-1.2 -2.5,-4.3 -0.8,-3.3 -1.6,-0.9 -0.5,-0.5 0.9,-2.2 -0.4,-5.5 -1.6,-4.5 0.8,-4.9 1.7,-4.8 -0.2,-2.5 -1.9,-0.7 -0.4,-3.2 -2.8,-3.5 -1.9,-6.2 -1.5,-7 -1.7,-4.8 -3.8,-16 -3.5,-8 -0.8,-5.3 0.1,-2.3 3.3,-0.3 13.6,-1.6 18.6,-2 6.3,-1.1 0.5,1.4 -2.2,0.9 -0.9,2.2 0.4,2 1.4,1.6 4.3,2.7 3.2,-0.1 3.2,4.7 0.6,1.6 2.3,2.8 0.5,1.7 4.7,1.8 3,2.2 2.3,3 2.3,1.3 2,1.8 1.4,2.7 2.1,1.9 4.1,1.8 2.7,6 1.7,5.1 2.8,0.7 2.1,1.9 2,5.7 2.9,1.6 1.7,-0.8 0.4,1.2 -3.3,6.2 0.5,2.6 -1.5,4.2 -2.3,10 0.8,6.3z', + abbreviation: 'GA', + name: 'Georgia', + }, + IA: { + dimensions: + 'M556.8,183.6 l2.1,2.1 0.3,0.7 -2,3 0.3,4 2.6,4.1 3.1,1.6 2.4,0.3 0.9,1.8 0.2,2.4 2.5,1 0.9,1.1 0.5,1.6 3.8,3.3 0.6,1.9 -0.7,3 -1.7,3.7 -0.6,2.4 -2.1,1.6 -1.6,0.5 -5.7,1.5 -1.6,4.8 0.8,1.8 1.7,1.5 -0.2,3.5 -1.9,1.4 -0.7,1.8 v2.4 l-1.4,0.4 -1.7,1.4 -0.5,1.7 0.4,1.7 -1.3,1 -2.3,-2.7 -1.4,-2.8 -8.3,0.8 -10,0.6 -49.2,1.2 -1.6,-4.3 -0.4,-6.7 -1.4,-4.2 -0.7,-5.2 -2.2,-3.7 -1,-4.6 -2.7,-7.8 -1.1,-5.6 -1.4,-1.9 -1.3,-2.9 1.7,-3.8 1.2,-6.1 -2.7,-2.2 -0.3,-2.4 0.7,-2.4 1.8,-0.3 61.1,-0.6 21.2,-0.7z', + abbreviation: 'IA', + name: 'Iowa', + }, + ID: { + dimensions: + 'M175.3,27.63 l-4.8,17.41 -4.5,20.86 -3.4,16.22 -0.4,9.67 1.2,4.44 3.5,2.66 -0.2,3.91 -3.9,4.4 -4.5,6.6 -0.9,2.9 -1.2,1.1 -1.8,0.8 -4.3,5.3 -0.4,3.1 -0.4,1.1 0.6,1 2.6,-0.1 1.1,2.3 -2.4,5.8 -1.2,4.2 -8.8,35.3 20.7,4.5 39.5,7.9 34.8,6.1 4.9,-29.2 3.8,-24.1 -2.7,-2.4 -0.4,-2.6 -0.8,-1.1 -2.1,1 -0.7,2.6 -3.2,0.5 -3.9,-1.6 -3.8,0.1 -2.5,0.7 -3.4,-1.5 -2.4,0.2 -2.4,2 -2,-1.1 -0.7,-4 0.7,-2.9 -2.5,-2.9 -3.3,-2.6 -2.7,-13.1 -0.1,-4.7 -0.3,-0.1 -0.2,0.4 -5.1,3.5 -1.7,-0.2 -2.9,-3.4 -0.2,-3.1 7,-17.13 -0.4,-1.94 -3.4,-1.15 -0.6,-1.18 -2.6,-3.46 -4.6,-10.23 -3.2,-1.53 -2,-4.95 1.3,-4.63 -3.2,-7.58 4.4,-21.52z', + abbreviation: 'ID', + name: 'Idaho', + }, + IL: { + dimensions: + 'M618.7,214.3 l-0.8,-2.6 -1.3,-3.7 -1.6,-1.8 -1.5,-2.6 -0.4,-5.5 -15.9,1.8 -17.4,1 h-12.3 l0.2,2.1 2.2,0.9 1.1,1.4 0.4,1.4 3.9,3.4 0.7,2.4 -0.7,3.3 -1.7,3.7 -0.8,2.7 -2.4,1.9 -1.9,0.6 -5.2,1.3 -1.3,4.1 0.6,1.1 1.9,1.8 -0.2,4.3 -2.1,1.6 -0.5,1.3 v2.8 l-1.8,0.6 -1.4,1.2 -0.4,1.2 0.4,2 -1.6,1.3 -0.9,2.8 0.3,3.9 2.3,7 7,7.6 5.7,3.7 v4.4 l0.7,1.2 6.6,0.6 2.7,1.4 -0.7,3.5 -2.2,6.2 -0.8,3 2,3.7 6.4,5.3 4.8,0.8 2.2,5.1 2,3.4 -0.9,2.8 1.5,3.8 1.7,2.1 1.6,-0.3 1,-2.2 2.4,-1.7 2.8,-1 6.1,2.5 0.5,-0.2 v-1.1 l-1.2,-2.7 0.4,-2.8 2.4,-1.6 3.4,-1.2 -0.5,-1.3 -0.8,-2 1.2,-1.3 1,-2.7 v-4 l0.4,-4.9 2.5,-3 1.8,-3.8 2.5,-4 -0.5,-5.3 -1.8,-3.2 -0.3,-3.3 0.8,-5.3 -0.7,-7.2 -1.1,-15.8 -1.4,-15.3 -0.9,-11.7z', + abbreviation: 'IL', + name: 'Illinois', + }, + IN: { + dimensions: + 'M622.9,216.1 l1.5,1 1.1,-0.3 2.1,-1.9 2.5,-1.8 14.3,-1.1 18.4,-1.8 1.6,15.5 4.9,42.6 -0.6,2.9 1.3,1.6 0.2,1.3 -2.3,1.6 -3.6,1.7 -3.2,0.4 -0.5,4.8 -4.7,3.6 -2.9,4 0.2,2.4 -0.5,1.4 h-3.5 l-1.4,-1.7 -5.2,3 0.2,3.1 -0.9,0.2 -0.5,-0.9 -2.4,-1.7 -3.6,1.5 -1.4,2.9 -1.2,-0.6 -1.6,-1.8 -4.4,0.5 -5.7,1 -2.5,1.3 v-2.6 l0.4,-4.7 2.3,-2.9 1.8,-3.9 2.7,-4.2 -0.5,-5.8 -1.8,-3.1 -0.3,-3.2 0.8,-5.3 -0.7,-7.1 -0.9,-12.6 -2.5,-30.1z', + abbreviation: 'IN', + name: 'Indiana', + }, + KS: { + dimensions: + 'M485.9,259.5 l-43.8,-0.6 -40.6,-1.2 -21.7,-0.9 -4.3,64.8 24.3,1 44.7,2.1 46.3,0.6 12.6,-0.3 0.7,-35 -1.2,-11.1 -2.5,-2 -2.4,-3 -2.3,-3.6 0.6,-3 1.7,-1.4 v-2.1 l-0.8,-0.7 -2.6,-0.2 -3.5,-3.4z', + abbreviation: 'KS', + name: 'Kansas', + }, + KY: { + dimensions: + 'M607.2,331.8 l12.6,-0.7 0.1,-4.1 h4.3 l30.4,-3.2 45.1,-4.3 5.6,-3.6 3.9,-2.1 0.1,-1.9 6,-7.8 4.1,-3.6 2.1,-2.4 -3.3,-2 -2.5,-2.7 -3,-3.8 -0.5,-2.2 -2.6,-1.4 -0.9,-1.9 -0.2,-6.1 -2.6,-2 -1.9,-1.1 -0.5,-2.3 -1.3,0.2 -2,1.2 -2.5,2.7 -1.9,-1.7 -2.5,-0.5 -2.4,1.4 h-2.3 l-1.8,-2 -5.6,-0.1 -1.8,-4.5 -2.9,-1.5 -2.1,0.8 -4.2,0.2 -0.5,2.1 1.2,1.5 0.3,2.1 -2.8,2 -3.8,1.8 -2.6,0.4 -0.5,4.5 -4.9,3.6 -2.6,3.7 0.2,2.2 -0.9,2.3 -4.5,-0.1 -1.3,-1.3 -3.9,2.2 0.2,3.3 -2.4,0.6 -0.8,-1.4 -1.7,-1.2 -2.7,1.1 -1.8,3.5 -2.2,-1 -1.4,-1.6 -3.7,0.4 -5.6,1 -2.8,1.3 -1.2,3.4 -1,1 1.5,3.7 -4.2,1.4 -1.9,1.4 -0.4,2.2 1.2,2.4 v2.2 l-1.6,0.4 -6.1,-2.5 -2.3,0.9 -2,1.4 -0.8,1.8 1.7,2.4 -0.9,1.8 -0.1,3.3 -2.4,1.3 -2.1,1.7z', + abbreviation: 'KY', + name: 'Kentucky', + }, + LA: { + dimensions: + 'M526.9,485.9 l8.1,-0.3 10.3,3.6 6.5,1.1 3.7,-1.5 3.2,1.1 3.2,1 0.8,-2.1 -3.2,-1.1 -2.6,0.5 -2.7,-1.6 0.8,-1.5 3.1,-1 1.8,1.5 1.8,-1 3.2,0.6 1.5,2.4 0.3,2.3 4.5,0.3 1.8,1.8 -0.8,1.6 -1.3,0.8 1.6,1.6 8.4,3.6 3.6,-1.3 1,-2.4 2.6,-0.6 1.8,-1.5 1.3,1 0.8,2.9 -2.3,0.8 0.6,0.6 3.4,-1.3 2.3,-3.4 0.8,-0.5 -2.1,-0.3 0.8,-1.6 -0.2,-1.5 2.1,-0.5 1.1,-1.3 0.6,0.8 0.6,3.1 4.2,0.6 4,1.9 1,1.5 h2.9 l1.1,1 2.3,-3.1 v-1.5 h-1.3 l-3.4,-2.7 -5.8,-0.8 -3.2,-2.3 1.1,-2.7 2.3,0.3 0.2,-0.6 -1.8,-1 v-0.5 h3.2 l1.8,-3.1 -1.3,-1.9 -0.3,-2.7 -1.5,0.2 -1.9,2.1 -0.6,2.6 -3.1,-0.6 -1,-1.8 1.8,-1.9 1.9,-1.7 -2.2,-6.5 -3.4,-3.4 1,-7.3 -0.2,-0.5 -1.3,0.2 -33.1,1.4 -0.8,-2.4 0.8,-8.5 8.6,-14.8 -0.9,-2.6 1.4,-0.4 0.4,-2 -2.2,-2 0.1,-1.9 -2,-4.5 -0.4,-5.1 0.1,-0.7 -26.4,0.8 -25.2,0.1 0.4,9.7 0.7,9.5 0.5,3.7 2.6,4.5 0.9,4.4 4.3,6 0.3,3.1 0.6,0.8 -0.7,8.3 -2.8,4.6 1.2,2.4 -0.5,2.6 -0.8,7.3 -1.3,3 0.2,3.7z', + abbreviation: 'LA', + name: 'Louisiana', + }, + MA: { + dimensions: + 'M887.5,172.5 l-0.5,-2.3 0.8,-1.5 2.9,-1.5 0.8,3.1 -0.5,1.8 -2.4,1.5 v1 l1.9,-1.5 3.9,-4.5 3.9,-1.9 4.2,-1.5 -0.3,-2.4 -1,-2.9 -1.9,-2.4 -1.8,-0.8 -2.1,0.2 -0.5,0.5 1,1.3 1.5,-0.8 2.1,1.6 0.8,2.7 -1.8,1.8 -2.3,1 -3.6,-0.5 -3.9,-6 -2.3,-2.6 h-1.8 l-1.1,0.8 -1.9,-2.6 0.3,-1.5 2.4,-5.2 -2.9,-4.4 -3.7,1.8 -1.8,2.9 -18.3,4.7 -13.8,2.5 -0.6,10.6 0.7,4.9 22,-4.8 11.2,-2.8 2,1.6 3.4,4.3 2.9,4.7z m12.5,1.4 2.2,-0.7 0.5,-1.7 1,0.1 1,2.3 -1.3,0.5 -3.9,0.1z m-9.4,0.8 2.3,-2.6 h1.6 l1.8,1.5 -2.4,1 -2.2,1z', + abbreviation: 'MA', + name: 'Massachusetts', + }, + MD: { + dimensions: + 'M834.8,264.1 l1.7,-3.8 0.5,-4.8 -6.3,1.1 -5.8,0.3 -3.8,-16.8 -2.3,-5.5 -1.5,-4.6 -22.2,4.3 -37.6,7.6 2,10.4 4.8,-4.9 2.5,-0.7 1.4,-1.5 1.8,-2.7 1.6,0.7 2.6,-0.2 2.6,-2.1 2,-1.5 2.1,-0.6 1.5,1.1 2.7,1.4 1.9,1.8 1.3,1.4 4.8,1.6 -0.6,2.9 5.8,2.1 2.1,-2.6 3.7,2.5 -2.1,3.3 -0.7,3.3 -1.8,2.6 v2.1 l0.3,0.8 2,1.3 3.4,1.1 4.3,-0.1 3.1,1 2.1,0.3 1,-2.1 -1.5,-2.1 v-1.8 l-2.4,-2.1 -2.1,-5.5 1.3,-5.3 -0.2,-2.1 -1.3,-1.3 c0,0 1.5,-1.6 1.5,-2.3 0,-0.6 0.5,-2.1 0.5,-2.1 l1.9,-1.3 1.9,-1.6 0.5,1 -1.5,1.6 -1.3,3.7 0.3,1.1 1.8,0.3 0.5,5.5 -2.1,1 0.3,3.6 0.5,-0.2 1.1,-1.9 1.6,1.8 -1.6,1.3 -0.3,3.4 2.6,3.4 3.9,0.5 1.6,-0.8 3.2,4.2 1,0.4z m-14.5,0.2 1.1,2.5 0.2,1.8 1.1,1.9 c0,0 0.9,-0.9 0.9,-1.2 0,-0.3 -0.7,-3.1 -0.7,-3.1 l-0.7,-2.3z', + abbreviation: 'MD', + name: 'Maryland', + }, + ME: { + dimensions: + 'M865.8,91.9 l1.5,0.4 v-2.6 l0.8,-5.5 2.6,-4.7 1.5,-4 -1.9,-2.4 v-6 l0.8,-1 0.8,-2.7 -0.2,-1.5 -0.2,-4.8 1.8,-4.8 2.9,-8.9 2.1,-4.2 h1.3 l1.3,0.2 v1.1 l1.3,2.3 2.7,0.6 0.8,-0.8 v-1 l4,-2.9 1.8,-1.8 1.5,0.2 6,2.4 1.9,1 9.1,29.9 h6 l0.8,1.9 0.2,4.8 2.9,2.3 h0.8 l0.2,-0.5 -0.5,-1.1 2.8,-0.5 1.9,2.1 2.3,3.7 v1.9 l-2.1,4.7 -1.9,0.6 -3.4,3.1 -4.8,5.5 c0,0 -0.6,0 -1.3,0 -0.6,0 -1,-2.1 -1,-2.1 l-1.8,0.2 -1,1.5 -2.4,1.5 -1,1.5 1.6,1.5 -0.5,0.6 -0.5,2.7 -1.9,-0.2 v-1.6 l-0.3,-1.3 -1.5,0.3 -1.8,-3.2 -2.1,1.3 1.3,1.5 0.3,1.1 -0.8,1.3 0.3,3.1 0.2,1.6 -1.6,2.6 -2.9,0.5 -0.3,2.9 -5.3,3.1 -1.3,0.5 -1.6,-1.5 -3.1,3.6 1,3.2 -1.5,1.3 -0.2,4.4 -1.1,6.3 -2.2,-0.9 -0.5,-3.1 -4,-1.1 -0.2,-2.5 -11.7,-37.43z m36.5,15.6 1.5,-1.5 1.4,1.1 0.6,2.4 -1.7,0.9z m6.7,-5.9 1.8,1.9 c0,0 1.3,0.1 1.3,-0.2 0,-0.3 0.2,-2 0.2,-2 l0.9,-0.8 -0.8,-1.8 -2,0.7z', + abbreviation: 'ME', + name: 'Maine', + }, + MI: { + dimensions: + 'M644.5,211 l19.1,-1.9 0.2,1.1 9.9,-1.5 12,-1.7 0.1,-0.6 0.2,-1.5 2.1,-3.7 2,-1.7 -0.2,-5.1 1.6,-1.6 1.1,-0.3 0.2,-3.6 1.5,-3 1.1,0.6 0.2,0.6 0.8,0.2 1.9,-1 -0.4,-9.1 -3.2,-8.2 -2.3,-9.1 -2.4,-3.2 -2.6,-1.8 -1.6,1.1 -3.9,1.8 -1.9,5 -2.7,3.7 -1.1,0.6 -1.5,-0.6 c0,0 -2.6,-1.5 -2.4,-2.1 0.2,-0.6 0.5,-5 0.5,-5 l3.4,-1.3 0.8,-3.4 0.6,-2.6 2.4,-1.6 -0.3,-10 -1.6,-2.3 -1.3,-0.8 -0.8,-2.1 0.8,-0.8 1.6,0.3 0.2,-1.6 -2.6,-2.2 -1.3,-2.6 h-2.6 l-4.5,-1.5 -5.5,-3.4 h-2.7 l-0.6,0.6 -1,-0.5 -3.1,-2.3 -2.9,1.8 -2.9,2.3 0.3,3.6 1,0.3 2.1,0.5 0.5,0.8 -2.6,0.8 -2.6,0.3 -1.5,1.8 -0.3,2.1 0.3,1.6 0.3,5.5 -3.6,2.1 -0.6,-0.2 v-4.2 l1.3,-2.4 0.6,-2.4 -0.8,-0.8 -1.9,0.8 -1,4.2 -2.7,1.1 -1.8,1.9 -0.2,1 0.6,0.8 -0.6,2.6 -2.3,0.5 v1.1 l0.8,2.4 -1.1,6.1 -1.6,4 0.6,4.7 0.5,1.1 -0.8,2.4 -0.3,0.8 -0.3,2.7 3.6,6 2.9,6.5 1.5,4.8 -0.8,4.7 -1,6 -2.4,5.2 -0.3,2.7 -3.2,3.1z m-33.3,-72.4 -1.3,-1.1 -1.8,-10.4 -3.7,-1.3 -1.7,-2.3 -12.6,-2.8 -2.8,-1.1 -8.1,-2.2 -7.8,-1 -3.9,-5.3 0.7,-0.5 2.7,-0.8 3.6,-2.3 v-1 l0.6,-0.6 6,-1 2.4,-1.9 4.4,-2.1 0.2,-1.3 1.9,-2.9 1.8,-0.8 1.3,-1.8 2.3,-2.3 4.4,-2.4 4.7,-0.5 1.1,1.1 -0.3,1 -3.7,1 -1.5,3.1 -2.3,0.8 -0.5,2.4 -2.4,3.2 -0.3,2.6 0.8,0.5 1,-1.1 3.6,-2.9 1.3,1.3 h2.3 l3.2,1 1.5,1.1 1.5,3.1 2.7,2.7 3.9,-0.2 1.5,-1 1.6,1.3 1.6,0.5 1.3,-0.8 h1.1 l1.6,-1 4,-3.6 3.4,-1.1 6.6,-0.3 4.5,-1.9 2.6,-1.3 1.5,0.2 v5.7 l0.5,0.3 2.9,0.8 1.9,-0.5 6.1,-1.6 1.1,-1.1 1.5,0.5 v7 l3.2,3.1 1.3,0.6 1.3,1 -1.3,0.3 -0.8,-0.3 -3.7,-0.5 -2.1,0.6 -2.3,-0.2 -3.2,1.5 h-1.8 l-5.8,-1.3 -5.2,0.2 -1.9,2.6 -7,0.6 -2.4,0.8 -1.1,3.1 -1.3,1.1 -0.5,-0.2 -1.5,-1.6 -4.5,2.4 h-0.6 l-1.1,-1.6 -0.8,0.2 -1.9,4.4 -1,4 -3.2,6.9z m-29.6,-56.5 1.8,-2.1 2.2,-0.8 5.4,-3.9 2.3,-0.6 0.5,0.5 -5.1,5.1 -3.3,1.9 -2.1,0.9z m86.2,32.1 0.6,2.5 3.2,0.2 1.3,-1.2 c0,0 -0.1,-1.5 -0.4,-1.6 -0.3,-0.2 -1.6,-1.9 -1.6,-1.9 l-2.2,0.2 -1.6,0.2 -0.3,1.1z', + abbreviation: 'MI', + name: 'Michigan', + }, + MN: { + dimensions: + 'M464.6,66.79 l-0.6,3.91 v10.27 l1.6,5.03 1.9,3.32 0.5,9.93 1.8,13.45 1.8,7.3 0.4,6.4 v5.3 l-1.6,1.8 -1.8,1.3 v1.5 l0.9,1.7 4.1,3.5 0.7,3.2 v35.9 l60.3,-0.6 21.2,-0.7 -0.5,-6 -1.8,-2.1 -7.2,-4.6 -3.6,-5.3 -3.4,-0.9 -2,-2.8 h-3.2 l-3.5,-3.8 -0.5,-7 0.1,-3.9 1.5,-3 -0.7,-2.7 -2.8,-3.1 2.2,-6.1 5.4,-4 1.2,-1.4 -0.2,-8 0.2,-3 2.6,-3 3.8,-2.9 1.3,-0.2 4.5,-5 1.8,-0.8 2.3,-3.9 2.4,-3.6 3.1,-2.6 4.8,-2 9.2,-4.1 3.9,-1.8 0.6,-2.3 -4.4,0.4 -0.7,1.1 h-0.6 l-1.8,-3.1 -8.9,0.3 -1,0.8 h-1 l-0.5,-1.3 -0.8,-1.8 -2.6,0.5 -3.2,3.2 -1.6,0.8 h-3.1 l-2.6,-1 v-2.1 l-1.3,-0.2 -0.5,0.5 -2.6,-1.3 -0.5,-2.9 -1.5,0.5 -0.5,1 -2.4,-0.5 -5.3,-2.4 -3.9,-2.6 h-2.9 l-1.3,-1 -2.3,0.6 -1.1,1.1 -0.3,1.3 h-4.8 v-2.1 l-6.3,-0.3 -0.3,-1.5 h-4.8 l-1.6,-1.6 -1.5,-6.1 -0.8,-5.5 -1.9,-0.8 -2.3,-0.5 -0.6,0.2 -0.3,8.2 -30.1,-0.03z', + abbreviation: 'MN', + name: 'Minnesota', + }, + MO: { + dimensions: + 'M593.1,338.7 l0.5,-5.9 4.2,-3.4 1.9,-1 v-2.9 l0.7,-1.6 -1.1,-1.6 -2.4,0.3 -2.1,-2.5 -1.7,-4.5 0.9,-2.6 -2,-3.2 -1.8,-4.6 -4.6,-0.7 -6.8,-5.6 -2.2,-4.2 0.8,-3.3 2.2,-6 0.6,-3 -1.9,-1 -6.9,-0.6 -1.1,-1.9 v-4.1 l-5.3,-3.5 -7.2,-7.8 -2.3,-7.3 -0.5,-4.2 0.7,-2.4 -2.6,-3.1 -1.2,-2.4 -7.7,0.8 -10,0.6 -48.8,1.2 1.3,2.6 -0.1,2.2 2.3,3.6 3,3.9 3.1,3 2.6,0.2 1.4,1.1 v2.9 l-1.8,1.6 -0.5,2.3 2.1,3.2 2.4,3 2.6,2.1 1.3,11.6 -0.8,40 0.5,5.7 23.7,-0.2 23.3,-0.7 32.5,-1.3 2.2,3.7 -0.8,3.1 -3.1,2.5 -0.5,1.8 5.2,0.5 4.1,-1.1z', + abbreviation: 'MO', + name: 'Missouri', + }, + MS: { + dimensions: + 'M604.3,472.5 l2.6,-4.2 1.8,0.8 6.8,-1.9 2.1,0.3 1.5,0.8 h5.2 l0.4,-1.6 -1.7,-14.8 -2.8,-19 1,-45.1 -0.2,-16.7 0.2,-6.3 -4.8,0.3 -19.6,1.6 -13,0.4 -0.2,3.2 -2.8,1.3 -2.6,5.1 0.5,1.6 0.1,2.4 -2.9,1.1 -3.5,5.1 0.8,2.3 -3,2.5 -1,5.7 -0.6,1.9 1.6,2.5 -1.5,1.4 1.5,2.8 0.3,4.2 -1.2,2.5 -0.2,0.9 0.4,5 2,4.5 -0.1,1.7 2.3,2 -0.7,3.1 -0.9,0.3 0.6,1.9 -8.6,15 -0.8,8.2 0.5,1.5 24.2,-0.7 8.2,-0.7 1.9,-0.3 0.6,1.4 -1,7.1 3.3,3.3 2.2,6.4z', + abbreviation: 'MS', + name: 'Mississippi', + }, + MT: { + dimensions: + 'M361.1,70.77 l-5.3,57.13 -1.3,15.2 -59.1,-6.6 -49,-7.1 -1.4,11.2 -1.9,-1.7 -0.4,-2.5 -1.3,-1.9 -3.3,1.5 -0.7,2.5 -2.3,0.3 -3.8,-1.6 -4.1,0.1 -2.4,0.7 -3.2,-1.5 -3,0.2 -2.1,1.9 -0.9,-0.6 -0.7,-3.4 0.7,-3.2 -2.7,-3.2 -3.3,-2.5 -2.5,-12.6 -0.1,-5.3 -1.6,-0.8 -0.6,1 -4.5,3.2 -1.2,-0.1 -2.3,-2.8 -0.2,-2.8 7,-17.15 -0.6,-2.67 -3.5,-1.12 -0.4,-0.91 -2.7,-3.5 -4.6,-10.41 -3.2,-1.58 -1.8,-4.26 1.3,-4.63 -3.2,-7.57 4.4,-21.29 32.7,6.89 18.4,3.4 32.3,5.3 29.3,4 29.2,3.5 30.8,3.07z', + abbreviation: 'MT', + name: 'Montana', + }, + NC: { + dimensions: + 'M786.7,357.7 l-12.7,-7.7 -3.1,-0.8 -16.6,2.1 -1.6,-3 -2.8,-2.2 -16.7,0.5 -7.4,0.9 -9.2,4.5 -6.8,2.7 -6.5,1.2 -13.4,1.4 0.1,-4.1 1.7,-1.3 2.7,-0.7 0.7,-3.8 3.9,-2.5 3.9,-1.5 4.5,-3.7 4.4,-2.3 0.7,-3.2 4.1,-3.8 0.7,1 2.5,0.2 2.4,-3.6 1.7,-0.4 2.6,0.3 1.8,-4 2.5,-2.4 0.5,-1.8 0.1,-3.5 4.4,0.1 38.5,-5.6 57.5,-12.3 2,4.8 3.6,6.5 2.4,2.4 0.6,2.3 -2.4,0.2 0.8,0.6 -0.3,4.2 -2.6,1.3 -0.6,2.1 -1.3,2.9 -3.7,1.6 -2.4,-0.3 -1.5,-0.2 -1.6,-1.3 0.3,1.3 v1 h1.9 l0.8,1.3 -1.9,6.3 h4.2 l0.6,1.6 2.3,-2.3 1.3,-0.5 -1.9,3.6 -3.1,4.8 h-1.3 l-1.1,-0.5 -2.7,0.6 -5.2,2.4 -6.5,5.3 -3.4,4.7 -1.9,6.5 -0.5,2.4 -4.7,0.5 -5.1,1.5z m49.3,-26.2 2.6,-2.5 3.2,-2.6 1.5,-0.6 0.2,-2 -0.6,-6.1 -1.5,-2.3 -0.6,-1.9 0.7,-0.2 2.7,5.5 0.4,4.4 -0.2,3.4 -3.4,1.5 -2.8,2.4 -1.1,1.2z', + abbreviation: 'NC', + name: 'North Carolina', + }, + ND: { + dimensions: + 'M471,126.4 l-0.4,-6.2 -1.8,-7.3 -1.8,-13.61 -0.5,-9.7 -1.9,-3.18 -1.6,-5.32 v-10.41 l0.6,-3.85 -1.8,-5.54 -28.6,-0.59 -18.6,-0.6 -26.5,-1.3 -25.2,-2.16 -0.9,14.42 -4.7,50.94 56.8,3.9 56.9,1.7z', + abbreviation: 'ND', + name: 'North Dakota', + }, + NE: { + dimensions: + 'M470.3,204.3 l-1,-2.3 -0.5,-1.6 -2.9,-1.6 -4.8,-1.5 -2.2,-1.2 -2.6,0.1 -3.7,0.4 -4.2,1.2 -6,-4.1 -2.2,-2 -10.7,0.6 -41.5,-2.4 -35.6,-2.2 -4.3,43.7 33.1,3.3 -1.4,21.1 21.7,1 40.6,1.2 43.8,0.6 h4.5 l-2.2,-3 -2.6,-3.9 0.1,-2.3 -1.4,-2.7 -1.9,-5.2 -0.4,-6.7 -1.4,-4.1 -0.5,-5 -2.3,-3.7 -1,-4.7 -2.8,-7.9 -1,-5.3z', + abbreviation: 'NE', + name: 'Nebraska', + }, + NH: { + dimensions: + 'M881.7,141.3 l1.1,-3.2 -2.7,-1.2 -0.5,-3.1 -4.1,-1.1 -0.3,-3 -11.7,-37.48 -0.7,0.08 -0.6,1.6 -0.6,-0.5 -1,-1 -1.5,1.9 -0.2,2.29 0.5,8.41 1.9,2.8 v4.3 l-3.9,4.8 -2.4,0.9 v0.7 l1.1,1.9 v8.6 l-0.8,9.2 -0.2,4.7 1,1.4 -0.2,4.7 -0.5,1.5 1,1.1 5.1,-1.2 13.8,-3.5 1.7,-2.9 4,-1.9z', + abbreviation: 'NH', + name: 'New Hampshire', + }, + NJ: { + dimensions: + 'M823.7,228.3 l0.1,-1.5 2.7,-1.3 1.7,-2.8 1.7,-2.4 3.3,-3.2 v-1.2 l-6.1,-4.1 -1,-2.7 -2.7,-0.3 -0.1,-0.9 -0.7,-2.2 2.2,-1.1 0.2,-2.9 -1.3,-1.3 0.2,-1.2 1.9,-3.1 v-3.1 l2.5,-3.1 5.6,2.5 6.4,1.9 2.5,1.2 0.1,1.8 -0.5,2.7 0.4,4.5 -2.1,1.9 -1.1,1 0.5,0.5 2.7,-0.3 1.1,-0.8 1.6,3.4 0.2,9.4 0.6,1.1 -1.1,5.5 -3.1,6.5 -2.7,4 -0.8,4.8 -2.1,2.4 h-0.8 l-0.3,-2.7 0.8,-1 -0.2,-1.5 -4,-0.6 -4.8,-2.3 -3.2,-2.9 -1,-2z', + abbreviation: 'NJ', + name: 'New Jersey', + }, + NM: { + dimensions: + 'M270.2,429.4 l-16.7,-2.6 -1.2,9.6 -15.8,-2 6,-39.7 7,-53.2 4.4,-30.9 34,3.9 37.4,4.4 32,2.8 -0.3,10.8 -1.4,-0.1 -7.4,97.7 -28.4,-1.8 -38.1,-3.7 0.7,6.3z', + abbreviation: 'NM', + name: 'New Mexico', + }, + NV: { + dimensions: + 'M123.1,173.6 l38.7,8.5 26,5.2 -10.6,53.1 -5.4,29.8 -3.3,15.5 -2.1,11.1 -2.6,16.4 -1.7,3.1 -1.6,-0.1 -1.2,-2.6 -2.8,-0.5 -1.3,-1.1 -1.8,0.1 -0.9,0.8 -1.8,1.3 -0.3,7.3 -0.3,1.5 -0.5,12.4 -1.1,1.8 -16.7,-25.5 -42.1,-62.1 -12.43,-19 8.55,-32.6 8.01,-31.3z', + abbreviation: 'NV', + name: 'Nevada', + }, + NY: { + dimensions: + 'M843.4,200 l0.5,-2.7 -0.2,-2.4 -3,-1.5 -6.5,-2 -6,-2.6 -0.6,-0.4 -2.7,-0.3 -2,-1.5 -2.1,-5.9 -3.3,-0.5 -2.4,-2.4 -38.4,8.1 -31.6,6 -0.5,-6.5 1.6,-1.2 1.3,-1.1 1,-1.6 1.8,-1.1 1.9,-1.8 0.5,-1.6 2.1,-2.7 1.1,-1 -0.2,-1 -1.3,-3.1 -1.8,-0.2 -1.9,-6.1 2.9,-1.8 4.4,-1.5 4,-1.3 3.2,-0.5 6.3,-0.2 1.9,1.3 1.6,0.2 2.1,-1.3 2.6,-1.1 5.2,-0.5 2.1,-1.8 1.8,-3.2 1.6,-1.9 h2.1 l1.9,-1.1 0.2,-2.3 -1.5,-2.1 -0.3,-1.5 1.1,-2.1 v-1.5 h-1.8 l-1.8,-0.8 -0.8,-1.1 -0.2,-2.6 5.8,-5.5 0.6,-0.8 1.5,-2.9 2.9,-4.5 2.7,-3.7 2.1,-2.4 2.4,-1.8 3.1,-1.2 5.5,-1.3 3.2,0.2 4.5,-1.5 7.4,-2.2 0.7,4.9 2.4,6.5 0.8,5 -1,4.2 2.6,4.5 0.8,2 -0.9,3.2 3.7,1.7 2.7,10.2 v5.8 l-0.6,10.9 0.8,5.4 0.7,3.6 1.5,7.3 v8.1 l-1.1,2.3 2.1,2.7 0.5,0.9 -1.9,1.8 0.3,1.3 1.3,-0.3 1.5,-1.3 2.3,-2.6 1.1,-0.6 1.6,0.6 2.3,0.2 7.9,-3.9 2.9,-2.7 1.3,-1.5 4.2,1.6 -3.4,3.6 -3.9,2.9 -7.1,5.3 -2.6,1 -5.8,1.9 -4,1.1 -1,-0.4z', + abbreviation: 'NY', + name: 'New York', + }, + OH: { + dimensions: + 'M663.8,211.2 l1.7,15.5 4.8,41.1 3.9,-0.2 2.3,-0.8 3.6,1.8 1.7,4.2 5.4,0.1 1.8,2 h1.7 l2.4,-1.4 3.1,0.5 1.5,1.3 1.8,-2 2.3,-1.4 2.4,-0.4 0.6,2.7 1.6,1 2.6,2 0.8,0.2 2,-0.1 1.2,-0.6 v-2.1 l1.7,-1.5 0.1,-4.8 1.1,-4.2 1.9,-1.3 1,0.7 1,1.1 0.7,0.2 0.4,-0.4 -0.9,-2.7 v-2.2 l1.1,-1.4 2.5,-3.6 1.3,-1.5 2.2,0.5 2.1,-1.5 3,-3.3 2.2,-3.7 0.2,-5.4 0.5,-5 v-4.6 l-1.2,-3.2 1.2,-1.8 1.3,-1.2 -0.6,-2.8 -4.3,-25.6 -6.2,3.7 -3.9,2.3 -3.4,3.7 -4,3.9 -3.2,0.8 -2.9,0.5 -5.5,2.6 -2.1,0.2 -3.4,-3.1 -5.2,0.6 -2.6,-1.5 -2.2,-1.3z', + abbreviation: 'OH', + name: 'Ohio', + }, + OK: { + dimensions: + 'M411.9,334.9 l-1.8,24.3 -0.9,18 0.2,1.6 4,3.6 1.7,0.9 h0.9 l0.9,-2.1 1.5,1.9 1.6,0.1 0.3,-0.2 0.2,-1.1 2.8,1.4 -0.4,3.5 3.8,0.5 2.5,1 4.2,0.6 2.3,1.6 2.5,-1.7 3.5,0.7 2.2,3.1 1.2,0.1 v2.3 l2.1,0.7 2.5,-2.1 1.8,0.6 2.7,0.1 0.7,2.3 4.4,1.8 1.7,-0.3 1.9,-4.2 h1.3 l1.1,2.1 4.2,0.8 3.4,1.3 3,0.8 1.6,-0.7 0.7,-2.7 h4.5 l1.9,0.9 2.7,-1.9 h1.4 l0.6,1.4 h3.6 l2,-1.8 2.3,0.6 1.7,2.2 3,1.7 3.4,0.9 1.9,1.2 -0.3,-37.6 -1.4,-10.9 -0.1,-8.6 -1.5,-6.6 -0.6,-6.8 0.1,-4.3 -12.6,0.3 -46.3,-0.5 -44.7,-2.1 -41.5,-1.8 -0.4,10.7z', + abbreviation: 'OK', + name: 'Oklahoma', + }, + OR: { + dimensions: + 'M67.44,158.9 l28.24,7.2 27.52,6.5 17,3.7 8.8,-35.1 1.2,-4.4 2.4,-5.5 -0.7,-1.3 -2.5,0.1 -1.3,-1.8 0.6,-1.5 0.4,-3.3 4.7,-5.7 1.9,-0.9 0.9,-0.8 0.7,-2.7 0.8,-1.1 3.9,-5.7 3.7,-4 0.2,-3.26 -3.4,-2.49 -1.2,-4.55 -13.1,-3.83 -15.3,-3.47 -14.8,0.37 -1.1,-1.31 -5.1,1.84 -4.5,-0.48 -2.4,-1.58 -1.3,0.54 -4.68,-0.29 -1.96,-1.43 -4.84,-1.77 -1.1,-0.07 -4.45,-1.27 -1.76,1.52 -6.26,-0.24 -5.31,-3.85 0.21,-9.28 -2.05,-3.5 -4.1,-0.6 -0.7,-2.5 -2.4,-0.5 -5.8,2.1 -2.3,6.5 -3.2,10 -3.2,6.5 -5,14.1 -6.5,13.6 -8.1,12.6 -1.9,2.9 -0.8,8.6 -1.3,6 2.71,3.5z', + abbreviation: 'OR', + name: 'Oregon', + }, + PA: { + dimensions: + 'M736.6,192.2 l1.3,-0.5 5.7,-5.5 0.7,6.9 33.5,-6.5 36.9,-7.8 2.3,2.3 3.1,0.4 2,5.6 2.4,1.9 2.8,0.4 0.1,0.1 -2.6,3.2 v3.1 l-1.9,3.1 -0.2,1.9 1.3,1.3 -0.2,1.9 -2.4,1.1 1,3.4 0.2,1.1 2.8,0.3 0.9,2.5 5.9,3.9 v0.4 l-3.1,3 -1.5,2.2 -1.7,2.8 -2.7,1.2 -1.4,0.3 -2.1,1.3 -1.6,1.4 -22.4,4.3 -38.7,7.8 -11.3,1.4 -3.9,0.7 -5.1,-22.4 -4.3,-25.9z', + abbreviation: 'PA', + name: 'Pennsylvania', + }, + RI: { + dimensions: + 'M873.6,175.7 l-0.8,-4.4 -1.6,-6 5.7,-1.5 1.5,1.3 3.4,4.3 2.8,4.4 -2.8,1.4 -1.3,-0.2 -1.1,1.8 -2.4,1.9 -2.8,1.1z', + abbreviation: 'RI', + name: 'Rhode Island', + }, + SC: { + dimensions: + 'M759,413.6 l-2.1,-1 -1.9,-5.6 -2.5,-2.3 -2.5,-0.5 -1.5,-4.6 -3,-6.5 -4.2,-1.8 -1.9,-1.8 -1.2,-2.6 -2.4,-2 -2.3,-1.3 -2.2,-2.9 -3.2,-2.4 -4.4,-1.7 -0.4,-1.4 -2.3,-2.8 -0.5,-1.5 -3.8,-5.4 -3.4,0.1 -3.9,-2.5 -1.2,-1.2 -0.2,-1.4 0.6,-1.6 2.7,-1.3 -0.8,-2 6.4,-2.7 9.2,-4.5 7.1,-0.9 16.4,-0.5 2.3,1.9 1.8,3.5 4.6,-0.8 12.6,-1.5 2.7,0.8 12.5,7.4 10.1,8.3 -5.3,5.4 -2.6,6.1 -0.5,6.3 -1.6,0.8 -1.1,2.7 -2.4,0.6 -2.1,3.6 -2.7,2.7 -2.3,3.4 -1.6,0.8 -3.6,3.4 -2.9,0.2 1,3.2 -5,5.3 -2.3,1.6z', + abbreviation: 'SC', + name: 'South Carolina', + }, + SD: { + dimensions: + 'M471,181.1 l-0.9,3.2 0.4,3 2.6,2 -1.2,5.4 -1.8,4.1 1.5,3.3 0.7,1.1 -1.3,0.1 -0.7,-1.6 -0.6,-2 -3.3,-1.8 -4.8,-1.5 -2.5,-1.3 -2.9,0.1 -3.9,0.4 -3.8,1.2 -5.3,-3.8 -2.7,-2.4 -10.9,0.8 -41.5,-2.4 -35.6,-2.2 1.5,-24.8 2.8,-34 0.4,-5 56.9,3.9 56.9,1.7 v2.7 l-1.3,1.5 -2,1.5 -0.1,2.2 1.1,2.2 4.1,3.4 0.5,2.7 v35.9z', + abbreviation: 'SD', + name: 'South Dakota', + }, + TN: { + dimensions: + 'M670.8,359.6 l-13.1,1.2 -23.3,2.2 -37.6,2.7 -11.8,0.4 0.9,-0.6 0.9,-4.5 -1.2,-3.6 3.9,-2.3 0.4,-2.5 1.2,-4.3 3,-9.5 0.5,-5.6 0.3,-0.2 12.3,-0.2 13.6,-0.8 0.1,-3.9 3.5,-0.1 30.4,-3.3 54,-5.2 10.3,-1.5 7.6,-0.2 2.4,-1.9 1.3,0.3 -0.1,3.3 -0.4,1.6 -2.4,2.2 -1.6,3.6 -2,-0.4 -2.4,0.9 -2.2,3.3 -1.4,-0.2 -0.8,-1.2 -1.1,0.4 -4.3,4 -0.8,3.1 -4.2,2.2 -4.3,3.6 -3.8,1.5 -4.4,2.8 -0.6,3.6 -2.5,0.5 -2,1.7 -0.2,4.8z', + abbreviation: 'TN', + name: 'Tennessee', + }, + TX: { + dimensions: + 'M282.8,425.6 l37,3.6 29.3,1.9 7.4,-97.7 54.4,2.4 -1.7,23.3 -1,18 0.2,2 4.4,4.1 2,1.1 h1.8 l0.5,-1.2 0.7,0.9 2.4,0.2 1.1,-0.6 v-0.2 l1,0.5 -0.4,3.7 4.5,0.7 2.4,0.9 4.2,0.7 2.6,1.8 2.8,-1.9 2.7,0.6 2.2,3.1 0.8,0.1 v2.1 l3.3,1.1 2.5,-2.1 1.5,0.5 2.1,0.1 0.6,2.1 5.2,2 2.3,-0.5 1.9,-4 h0.1 l1.1,1.9 4.6,0.9 3.4,1.3 3.2,1 2.4,-1.2 0.7,-2.3 h3.6 l2.1,1 3,-2 h0.4 l0.5,1.4 h4.7 l1.9,-1.8 1.3,0.4 1.7,2.1 3.3,1.9 3.4,1 2.5,1.4 2.7,2 3.1,-1.2 2.1,0.8 0.7,20 0.7,9.5 0.6,4.1 2.6,4.4 0.9,4.5 4.2,5.9 0.3,3.1 0.6,0.8 -0.7,7.7 -2.9,4.8 1.3,2.6 -0.5,2.4 -0.8,7.2 -1.3,3 0.3,4.2 -5.6,1.6 -9.9,4.5 -1,1.9 -2.6,1.9 -2.1,1.5 -1.3,0.8 -5.7,5.3 -2.7,2.1 -5.3,3.2 -5.7,2.4 -6.3,3.4 -1.8,1.5 -5.8,3.6 -3.4,0.6 -3.9,5.5 -4,0.3 -1,1.9 2.3,1.9 -1.5,5.5 -1.3,4.5 -1.1,3.9 -0.8,4.5 0.8,2.4 1.8,7 1,6.1 1.8,2.7 -1,1.5 -3.1,1.9 -5.7,-3.9 -5.5,-1.1 -1.3,0.5 -3.2,-0.6 -4.2,-3.1 -5.2,-1.1 -7.6,-3.4 -2.1,-3.9 -1.3,-6.5 -3.2,-1.9 -0.6,-2.3 0.6,-0.6 0.3,-3.4 -1.3,-0.6 -0.6,-1 1.3,-4.4 -1.6,-2.3 -3.2,-1.3 -3.4,-4.4 -3.6,-6.6 -4.2,-2.6 0.2,-1.9 -5.3,-12.3 -0.8,-4.2 -1.8,-1.9 -0.2,-1.5 -6,-5.3 -2.6,-3.1 v-1.1 l-2.6,-2.1 -6.8,-1.1 -7.4,-0.6 -3.1,-2.3 -4.5,1.8 -3.6,1.5 -2.3,3.2 -1,3.7 -4.4,6.1 -2.4,2.4 -2.6,-1 -1.8,-1.1 -1.9,-0.6 -3.9,-2.3 v-0.6 l-1.8,-1.9 -5.2,-2.1 -7.4,-7.8 -2.3,-4.7 v-8.1 l-3.2,-6.5 -0.5,-2.7 -1.6,-1 -1.1,-2.1 -5,-2.1 -1.3,-1.6 -7.1,-7.9 -1.3,-3.2 -4.7,-2.3 -1.5,-4.4 -2.6,-2.9 -1.7,-0.5z m174.4,141.7 -0.6,-7.1 -2.7,-7.2 -0.6,-7 1.5,-8.2 3.3,-6.9 3.5,-5.4 3.2,-3.6 0.6,0.2 -4.8,6.6 -4.4,6.5 -2,6.6 -0.3,5.2 0.9,6.1 2.6,7.2 0.5,5.2 0.2,1.5z', + abbreviation: 'TX', + name: 'Texas', + }, + UT: { + dimensions: + 'M228.4,305.9 l24.6,3.6 1.9,-13.7 7,-50.5 2.3,-22 -32.2,-3.5 2.2,-13.1 1.8,-10.6 -34.7,-6.1 -12.5,-2.5 -10.6,52.9 -5.4,30 -3.3,15.4 -1.7,9.2z', + abbreviation: 'UT', + name: 'Utah', + }, + VA: { + dimensions: + 'M834.7,265.2 l-0.2,2.8 -2.9,3.8 -0.4,4.6 0.5,3.4 -1.8,5 -2.2,1.9 -1.5,-4.6 0.4,-5.4 1.6,-4.2 0.7,-3.3 -0.1,-1.7z m-60.3,44.6 -38.6,5.6 -4.8,-0.1 -2.2,-0.3 -2.5,1.9 -7.3,0.1 -10.3,1.6 -6.7,0.6 4.1,-2.6 4.1,-2.3 v-2.1 l5.7,-7.3 4.1,-3.7 2.2,-2.5 3.6,4.3 3.8,0.9 2.7,-1 2,-1.5 2.4,1.2 4.6,-1.3 1.7,-4.4 2.4,0.7 3.2,-2.3 1.6,0.4 2.8,-3.2 0.2,-2.7 -0.8,-1.2 4.8,-10.5 1.8,-5.2 0.5,-4.7 0.7,-0.2 1.1,1.7 1.5,1.2 3.9,-0.2 1.7,-8.1 3,-0.6 0.8,-2.6 2.8,-2.2 1.1,-2.1 1.8,-4.3 0.1,-4.6 3.6,1.4 6.6,3.1 0.3,-5.2 3.4,1.2 -0.6,2.9 8.6,3.1 1.4,1.8 -0.8,3.3 -1.3,1.3 -0.5,1.7 0.5,2.4 2,1.3 3.9,1.4 2.9,1 4.9,0.9 2.2,2.1 3.2,0.4 0.9,1.2 -0.4,4.7 1.4,1.1 -0.5,1.9 1.2,0.8 -0.2,1.4 -2.7,-0.1 0.1,1.6 2.3,1.5 0.1,1.4 1.8,1.8 0.5,2.5 -2.6,1.4 1.6,1.5 5.8,-1.7 3.7,6.2z', + abbreviation: 'VA', + name: 'Virginia', + }, + VT: { + dimensions: + 'M832.7,111.3 l2.4,6.5 0.8,5.3 -1,3.9 2.5,4.4 0.9,2.3 -0.7,2.6 3.3,1.5 2.9,10.8 v5.3 l11.5,-2.1 -1,-1.1 0.6,-1.9 0.2,-4.3 -1,-1.4 0.2,-4.7 0.8,-9.3 v-8.5 l-1.1,-1.8 v-1.6 l2.8,-1.1 3.5,-4.4 v-3.6 l-1.9,-2.7 -0.3,-5.79 -26.1,6.79z', + abbreviation: 'VT', + name: 'Vermont', + }, + WA: { + dimensions: + 'M74.5,67.7 l-2.3,-4.3 -4.1,-0.7 -0.4,-2.4 -2.5,-0.6 -2.9,-0.5 -1.8,1 -2.3,-2.9 0.3,-2.9 2.7,-0.3 1.6,-4 -2.6,-1.1 0.2,-3.7 4.4,-0.6 -2.7,-2.7 -1.5,-7.1 0.6,-2.9 v-7.9 l-1.8,-3.2 2.3,-9.4 2.1,0.5 2.4,2.9 2.7,2.6 3.2,1.9 4.5,2.1 3.1,0.6 2.9,1.5 3.4,1 2.3,-0.2 v-2.4 l1.3,-1.1 2.1,-1.3 0.3,1.1 0.3,1.8 -2.3,0.5 -0.3,2.1 1.8,1.5 1.1,2.4 0.6,1.9 1.5,-0.2 0.2,-1.3 -1,-1.3 -0.5,-3.2 0.8,-1.8 -0.6,-1.5 v-2.6 l1.8,-3.6 -1.1,-2.6 -2.4,-4.8 0.3,-0.8 1.4,-0.8 4.4,1.5 9.7,2.7 8.6,1.9 20,5.7 23,5.7 15,3.49 -4.8,17.56 -4.5,20.83 -3.4,16.25 -0.4,9.18 v0 l-12.9,-3.72 -15.3,-3.47 -14.5,0.32 -1.1,-1.53 -5.7,2.09 -3.9,-0.42 -2.6,-1.79 -1.7,0.65 -4.15,-0.25 -1.72,-1.32 -5.16,-1.82 -1.18,-0.16 -4.8,-1.39 -1.92,1.65 -5.65,-0.25 -4.61,-3.35z m9.6,-55.4 2,-0.2 0.5,1.4 1.5,-1.6 h2.3 l0.8,1.5 -1.5,1.7 0.6,0.8 -0.7,2 -1.4,0.4 c0,0 -0.9,0.1 -0.9,-0.2 0,-0.3 1.5,-2.6 1.5,-2.6 l-1.7,-0.6 -0.3,1.5 -0.7,0.6 -1.5,-2.3z', + abbreviation: 'WA', + name: 'Washington', + }, + WI: { + dimensions: + 'M541.4,109.9 l2.9,0.5 2.9,-0.6 7.4,-3.2 2.9,-1.9 2.1,-0.8 1.9,1.5 -1.1,1.1 -1.9,3.1 -0.6,1.9 1,0.6 1.8,-1 1.1,-0.2 2.7,0.8 0.6,1.1 1.1,0.2 0.6,-1.1 4,5.3 8.2,1.2 8.2,2.2 2.6,1.1 12.3,2.6 1.6,2.3 3.6,1.2 1.7,10.2 1.6,1.4 1.5,0.9 -1.1,2.3 -1.8,1.6 -2.1,4.7 -1.3,2.4 0.2,1.8 1.5,0.3 1.1,-1.9 1.5,-0.8 0.8,-2.3 1.9,-1.8 2.7,-4 4.2,-6.3 0.8,-0.5 0.3,1 -0.2,2.3 -2.9,6.8 -2.7,5.7 -0.5,3.2 -0.6,2.6 0.8,1.3 -0.2,2.7 -1.9,2.4 -0.5,1.8 0.6,3.6 0.6,3.4 -1.5,2.6 -0.8,2.9 -1,3.1 1.1,2.4 0.6,6.1 1.6,4.5 -0.2,3 -15.9,1.8 -17.5,1 h-12.7 l-0.7,-1.5 -2.9,-0.4 -2.6,-1.3 -2.3,-3.7 -0.3,-3.6 2,-2.9 -0.5,-1.4 -2.1,-2.2 -0.8,-3.3 -0.6,-6.8 -2.1,-2.5 -7,-4.5 -3.8,-5.4 -3.4,-1 -2.2,-2.8 h-3.2 l-2.9,-3.3 -0.5,-6.5 0.1,-3.8 1.5,-3.1 -0.8,-3.2 -2.5,-2.8 1.8,-5.4 5.2,-3.8 1.6,-1.9 -0.2,-8.1 0.2,-2.8 2.4,-2.8z', + abbreviation: 'WI', + name: 'Wisconsin', + }, + WV: { + dimensions: + 'M758.9,254.3 l5.8,-6 2.6,-0.8 1.6,-1.5 1.5,-2.2 1.1,0.3 3.1,-0.2 4.6,-3.6 1.5,-0.5 1.3,1 2.6,1.2 3,3 -0.4,4.3 -5.4,-2.6 -4.8,-1.8 -0.1,5.9 -2.6,5.7 -2.9,2.4 -0.8,2.3 -3,0.5 -1.7,8.1 -2.8,0.2 -1.1,-1 -1.2,-2 -2.2,0.5 -0.5,5.1 -1.8,5.1 -5,11 0.9,1.4 -0.1,2 -2.2,2.5 -1.6,-0.4 -3.1,2.3 -2.8,-0.8 -1.8,4.9 -3.8,1 -2.5,-1.3 -2.5,1.9 -2.3,0.7 -3.2,-0.8 -3.8,-4.5 -3.5,-2.2 -2.5,-2.5 -2.9,-3.7 -0.5,-2.3 -2.8,-1.7 -0.6,-1.3 -0.2,-5.6 0.3,0.1 2.4,-0.2 1.8,-1 v-2.2 l1.7,-1.5 0.1,-5.2 0.9,-3.6 1.1,-0.7 0.4,0.3 1,1.1 1.7,0.5 1.1,-1.3 -1,-3.1 v-1.6 l3.1,-4.6 1.2,-1.3 2,0.5 2.6,-1.8 3.1,-3.4 2.4,-4.1 0.2,-5.6 0.5,-4.8 v-4.9 l-1.1,-3 0.9,-1.3 0.8,-0.7 4.3,19.3 4.3,-0.8 11.2,-1.3z', + abbreviation: 'WV', + name: 'West Virginia', + }, + WY: { + dimensions: + 'M353,161.9 l-1.5,25.4 -4.4,44 -2.7,-0.3 -83.3,-9.1 -27.9,-3 2,-12 6.9,-41 3.8,-24.2 1.3,-11.2 48.2,7 59.1,6.5z', + abbreviation: 'WY', + name: 'Wyoming', + }, +} diff --git a/web/components/usa-map/state-election-map.tsx b/web/components/usa-map/state-election-map.tsx new file mode 100644 index 00000000..8f7bb284 --- /dev/null +++ b/web/components/usa-map/state-election-map.tsx @@ -0,0 +1,85 @@ +import { zip } from 'lodash' +import Router from 'next/router' +import { useEffect, useState } from 'react' + +import { getProbability } from 'common/calculate' +import { Contract, CPMMBinaryContract } from 'common/contract' +import { Customize, USAMap } from './usa-map' +import { + getContractFromSlug, + listenForContract, +} from 'web/lib/firebase/contracts' + +export interface StateElectionMarket { + creatorUsername: string + slug: string + isWinRepublican: boolean + state: string +} + +export function StateElectionMap(props: { markets: StateElectionMarket[] }) { + const { markets } = props + + const contracts = useContracts(markets.map((m) => m.slug)) + const probs = contracts.map((c) => + c ? getProbability(c as CPMMBinaryContract) : 0.5 + ) + const marketsWithProbs = zip(markets, probs) as [ + StateElectionMarket, + number + ][] + + const stateInfo = marketsWithProbs.map(([market, prob]) => [ + market.state, + { + fill: probToColor(prob, market.isWinRepublican), + clickHandler: () => + Router.push(`/${market.creatorUsername}/${market.slug}`), + }, + ]) + + const config = Object.fromEntries(stateInfo) as Customize + + return <USAMap customize={config} /> +} + +const probToColor = (prob: number, isWinRepublican: boolean) => { + const p = isWinRepublican ? prob : 1 - prob + const hue = p > 0.5 ? 350 : 240 + const saturation = 100 + const lightness = 100 - 50 * Math.abs(p - 0.5) + return `hsl(${hue}, ${saturation}%, ${lightness}%)` +} + +const useContracts = (slugs: string[]) => { + const [contracts, setContracts] = useState<(Contract | undefined)[]>( + slugs.map(() => undefined) + ) + + useEffect(() => { + Promise.all(slugs.map((slug) => getContractFromSlug(slug))).then( + (contracts) => setContracts(contracts) + ) + }, [slugs]) + + useEffect(() => { + if (contracts.some((c) => c === undefined)) return + + // listen to contract updates + const unsubs = (contracts as Contract[]).map((c, i) => + listenForContract( + c.id, + (newC) => newC && setContracts(setAt(contracts, i, newC)) + ) + ) + return () => unsubs.forEach((u) => u()) + }, [contracts]) + + return contracts +} + +function setAt<T>(arr: T[], i: number, val: T) { + const newArr = [...arr] + newArr[i] = val + return newArr +} diff --git a/web/components/usa-map/usa-map.tsx b/web/components/usa-map/usa-map.tsx new file mode 100644 index 00000000..2841e04c --- /dev/null +++ b/web/components/usa-map/usa-map.tsx @@ -0,0 +1,106 @@ +// https://github.com/jb-1980/usa-map-react +// MIT License + +import { DATA } from './data' +import { USAState } from './usa-state' + +export type ClickHandler<E = SVGPathElement | SVGCircleElement, R = any> = ( + e: React.MouseEvent<E, MouseEvent> +) => R +export type GetClickHandler = (stateKey: string) => ClickHandler | undefined +export type CustomizeObj = { + fill?: string + clickHandler?: ClickHandler +} +export interface Customize { + [key: string]: CustomizeObj +} + +export type StatesProps = { + hideStateTitle?: boolean + fillStateColor: (stateKey: string) => string + stateClickHandler: GetClickHandler +} +const States = ({ + hideStateTitle, + fillStateColor, + stateClickHandler, +}: StatesProps) => + Object.entries(DATA).map(([stateKey, data]) => ( + <USAState + key={stateKey} + hideStateTitle={hideStateTitle} + stateName={data.name} + dimensions={data.dimensions} + state={stateKey} + fill={fillStateColor(stateKey)} + onClickState={stateClickHandler(stateKey)} + /> + )) + +type USAMapPropTypes = { + onClick?: ClickHandler + width?: number + height?: number + title?: string + defaultFill?: string + customize?: Customize + hideStateTitle?: boolean + className?: string +} + +export const USAMap = ({ + onClick = (e) => { + console.log(e.currentTarget.dataset.name) + }, + width = 959, + height = 593, + title = 'US states map', + defaultFill = '#d3d3d3', + customize, + hideStateTitle, + className, +}: USAMapPropTypes) => { + const fillStateColor = (state: string) => + customize?.[state]?.fill ? (customize[state].fill as string) : defaultFill + + const stateClickHandler = (state: string) => customize?.[state]?.clickHandler + + return ( + <svg + className={className} + xmlns="http://www.w3.org/2000/svg" + width={width} + height={height} + viewBox="0 0 959 593" + > + <title>{title} + + {States({ + hideStateTitle, + fillStateColor, + stateClickHandler, + })} + + + + + + + ) +} diff --git a/web/components/usa-map/usa-state.tsx b/web/components/usa-map/usa-state.tsx new file mode 100644 index 00000000..9bebd027 --- /dev/null +++ b/web/components/usa-map/usa-state.tsx @@ -0,0 +1,34 @@ +import clsx from 'clsx' +import { ClickHandler } from './usa-map' + +type USAStateProps = { + state: string + dimensions: string + fill: string + onClickState?: ClickHandler + stateName: string + hideStateTitle?: boolean +} +export const USAState = ({ + state, + dimensions, + fill, + onClickState, + stateName, + hideStateTitle, +}: USAStateProps) => { + return ( + + {hideStateTitle ? null : {stateName}} + + ) +} diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx new file mode 100644 index 00000000..1ae72f7a --- /dev/null +++ b/web/pages/midterms.tsx @@ -0,0 +1,92 @@ +import { Col } from 'web/components/layout/col' +import { Page } from 'web/components/page' +import { Title } from 'web/components/title' +import { + StateElectionMarket, + StateElectionMap, +} from 'web/components/usa-map/state-election-map' + +const senateMidterms: StateElectionMarket[] = [ + { + state: 'AZ', + creatorUsername: 'BTE', + slug: 'will-blake-masters-win-the-arizona', + isWinRepublican: true, + }, + { + state: 'OH', + creatorUsername: 'BTE', + slug: 'will-jd-vance-win-the-ohio-senate-s', + isWinRepublican: true, + }, + { + state: 'WI', + creatorUsername: 'BTE', + slug: 'will-ron-johnson-be-reelected-in-th', + isWinRepublican: true, + }, + { + state: 'FL', + creatorUsername: 'BTE', + slug: 'will-marco-rubio-be-reelected-to-th', + isWinRepublican: true, + }, + { + state: 'PA', + creatorUsername: 'MattP', + slug: 'will-dr-oz-be-elected-to-the-us-sen', + isWinRepublican: true, + }, + { + state: 'GA', + creatorUsername: 'NcyRocks', + slug: 'will-a-democrat-win-the-2022-us-sen-3d2432ba6d79', + isWinRepublican: false, + }, + { + state: 'NV', + creatorUsername: 'NcyRocks', + slug: 'will-a-democrat-win-the-2022-us-sen', + isWinRepublican: false, + }, + { + state: 'NC', + creatorUsername: 'NcyRocks', + slug: 'will-a-democrat-win-the-2022-us-sen-6f1a901e1fcf', + isWinRepublican: false, + }, + { + state: 'NH', + creatorUsername: 'NcyRocks', + slug: 'will-a-democrat-win-the-2022-us-sen-23194a72f1b7', + isWinRepublican: false, + }, + { + state: 'UT', + creatorUsername: 'SG', + slug: 'will-mike-lee-win-the-2022-utah-sen', + isWinRepublican: true, + }, +] + +const App = () => { + return ( + + + + <StateElectionMap markets={senateMidterms} /> + + <iframe + src="https://manifold.markets/embed/NathanpmYoung/will-the-democrats-control-the-sena" + title="Will the Democrats control the Senate after the Midterms?" + frameBorder="0" + width={800} + height={400} + className="mt-8" + ></iframe> + </Col> + </Page> + ) +} + +export default App From 3ed29877cee1b50ff0a488c4fd394d2cfdfdcec2 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 27 Sep 2022 18:55:07 -0400 Subject: [PATCH 08/13] Add dating docs to menu bar --- web/components/nav/sidebar.tsx | 2 ++ web/pages/date-docs/index.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index b0a9862b..71842559 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -164,6 +164,7 @@ function getMoreDesktopNavigation(user?: User | null) { { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'Dating docs', href: '/date-docs' }, { name: 'Help & About', href: 'https://help.manifold.markets/' }, { name: 'Sign out', @@ -226,6 +227,7 @@ function getMoreMobileNav() { { name: 'Charity', href: '/charity' }, { name: 'Send M$', href: '/links' }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' }, + { name: 'Dating docs', href: '/date-docs' }, ], signOut ) diff --git a/web/pages/date-docs/index.tsx b/web/pages/date-docs/index.tsx index d2dd874c..9ddeb57f 100644 --- a/web/pages/date-docs/index.tsx +++ b/web/pages/date-docs/index.tsx @@ -41,7 +41,7 @@ export default function DatePage(props: { return ( <Page> <div className="mx-auto w-full max-w-xl"> - <Row className="items-center justify-between"> + <Row className="items-center justify-between p-4 sm:p-0"> <Title className="!my-0 px-2 text-blue-500" text="Date docs" /> {!hasDoc && ( <SiteLink href="/date-docs/create" className="!no-underline"> From b87e29d7c0ea5097e01a1f58fed35731cd5839e0 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 27 Sep 2022 17:34:48 -0400 Subject: [PATCH 09/13] Rename script --- .../scripts/contest/{create-markets.ts => bulk-create-markets.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename functions/src/scripts/contest/{create-markets.ts => bulk-create-markets.ts} (100%) diff --git a/functions/src/scripts/contest/create-markets.ts b/functions/src/scripts/contest/bulk-create-markets.ts similarity index 100% rename from functions/src/scripts/contest/create-markets.ts rename to functions/src/scripts/contest/bulk-create-markets.ts From 14c008234a57e084ef51ab6652ddef3c9d030923 Mon Sep 17 00:00:00 2001 From: Austin Chen <akrolsmir@gmail.com> Date: Tue, 27 Sep 2022 17:38:30 -0400 Subject: [PATCH 10/13] Script: Add liquidity to all markets in a group --- docs/docs/api.md | 9 +++- .../src/scripts/contest/bulk-add-liquidity.ts | 52 +++++++++++++++++++ web/pages/api/v0/market/[id]/add-liquidity.ts | 28 ++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 functions/src/scripts/contest/bulk-add-liquidity.ts create mode 100644 web/pages/api/v0/market/[id]/add-liquidity.ts diff --git a/docs/docs/api.md b/docs/docs/api.md index 007f6fa6..5fc95a4a 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -55,6 +55,7 @@ Returns the authenticated user. Gets all groups, in no particular order. Parameters: + - `availableToUserId`: Optional. if specified, only groups that the user can join and groups they've already joined will be returned. @@ -81,7 +82,6 @@ Gets a group's markets by its unique ID. Requires no authorization. Note: group is singular in the URL. - ### `GET /v0/markets` Lists all markets, ordered by creation date descending. @@ -582,12 +582,17 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat "initialProb":25}' ``` +### `POST /v0/market/[marketId]/add-liquidity` + +Adds a specified amount of liquidity into the market. + +- `amount`: Required. The amount of liquidity to add, in M$. ### `POST /v0/market/[marketId]/close` Closes a market on behalf of the authorized user. -- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past. +- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past. ### `POST /v0/market/[marketId]/resolve` diff --git a/functions/src/scripts/contest/bulk-add-liquidity.ts b/functions/src/scripts/contest/bulk-add-liquidity.ts new file mode 100644 index 00000000..99d5f12b --- /dev/null +++ b/functions/src/scripts/contest/bulk-add-liquidity.ts @@ -0,0 +1,52 @@ +// Run with `npx ts-node src/scripts/contest/resolve-markets.ts` + +const DOMAIN = 'http://localhost:3000' +// Dev API key for Cause Exploration Prizes (@CEP) +// const API_KEY = '188f014c-0ba2-4c35-9e6d-88252e281dbf' +// DEV API key for Criticism and Red Teaming (@CARTBot) +const API_KEY = '6ff1f78a-32fe-43b2-b31b-9e3c78c5f18c' + +// Warning: Checking these in can be dangerous! +// Prod API key for @CEPBot + +// Can just curl /v0/group/{slug} to get a group +async function getGroupBySlug(slug: string) { + const resp = await fetch(`${DOMAIN}/api/v0/group/${slug}`) + return await resp.json() +} + +async function getMarketsByGroupId(id: string) { + // API structure: /v0/group/by-id/[id]/markets + const resp = await fetch(`${DOMAIN}/api/v0/group/by-id/${id}/markets`) + return await resp.json() +} + +async function addLiquidityById(id: string, amount: number) { + const resp = await fetch(`${DOMAIN}/api/v0/market/${id}/add-liquidity`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Key ${API_KEY}`, + }, + body: JSON.stringify({ + amount: amount, + }), + }) + return await resp.json() +} + +async function main() { + const group = await getGroupBySlug('cart-contest') + const markets = await getMarketsByGroupId(group.id) + + // Count up some metrics + console.log('Number of markets', markets.length) + + // Resolve each market to NO + for (const market of markets.slice(0, 3)) { + console.log(market.slug, market.totalLiquidity) + const resp = await addLiquidityById(market.id, 200) + console.log(resp) + } +} +main() diff --git a/web/pages/api/v0/market/[id]/add-liquidity.ts b/web/pages/api/v0/market/[id]/add-liquidity.ts new file mode 100644 index 00000000..3e1a3ed9 --- /dev/null +++ b/web/pages/api/v0/market/[id]/add-liquidity.ts @@ -0,0 +1,28 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' +import { fetchBackend, forwardResponse } from 'web/lib/api/proxy' + +export const config = { api: { bodyParser: true } } + +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + const { id } = req.query + const contractId = id as string + + if (req.body) req.body.contractId = contractId + try { + const backendRes = await fetchBackend(req, 'addliquidity') + await forwardResponse(res, backendRes) + } catch (err) { + console.error('Error talking to cloud function: ', err) + res.status(500).json({ message: 'Error communicating with backend.' }) + } +} From 13dad9a10c821c4248145d920c3e1028aeb88235 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 27 Sep 2022 19:03:14 -0400 Subject: [PATCH 11/13] Date doc: Remove photo as first-class feature --- common/post.ts | 1 - functions/src/create-post.ts | 1 - web/pages/date-docs/[username].tsx | 7 +--- web/pages/date-docs/create.tsx | 57 +----------------------------- 4 files changed, 2 insertions(+), 64 deletions(-) diff --git a/common/post.ts b/common/post.ts index 13a90821..45503b22 100644 --- a/common/post.ts +++ b/common/post.ts @@ -12,7 +12,6 @@ export type Post = { export type DateDoc = Post & { bounty: number birthday: number - photoUrl: string type: 'date-doc' contractSlug: string } diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index a342dc05..675ce3e1 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -42,7 +42,6 @@ const postSchema = z.object({ // Date doc fields: bounty: z.number().optional(), birthday: z.number().optional(), - photoUrl: z.string().optional(), type: z.string().optional(), question: z.string().optional(), }) diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index d6ac37cd..2b6c4909 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -85,7 +85,7 @@ export function DateDocPost(props: { link?: boolean }) { const { dateDoc, creator, link } = props - const { content, birthday, photoUrl, contractSlug } = dateDoc + const { content, birthday, contractSlug } = dateDoc const { name, username } = creator const user = useUser() @@ -133,11 +133,6 @@ export function DateDocPost(props: { </Button> </Col> </Row> - <img - className="w-full max-w-lg rounded-lg object-cover" - src={photoUrl} - alt={name} - /> </Col> </SiteLink> {user && user.id === creator.id ? ( diff --git a/web/pages/date-docs/create.tsx b/web/pages/date-docs/create.tsx index 5d72da42..6065796d 100644 --- a/web/pages/date-docs/create.tsx +++ b/web/pages/date-docs/create.tsx @@ -13,8 +13,6 @@ import { Button } from 'web/components/button' import dayjs from 'dayjs' import { MINUTE_MS } from 'common/util/time' import { Col } from 'web/components/layout/col' -import { uploadImage } from 'web/lib/firebase/storage' -import { LoadingIndicator } from 'web/components/loading-indicator' import { MAX_QUESTION_LENGTH } from 'common/contract' export default function CreateDateDocPage() { @@ -26,8 +24,6 @@ export default function CreateDateDocPage() { const title = `${user?.name}'s Date Doc` const [birthday, setBirthday] = useState<undefined | string>(undefined) - const [photoUrl, setPhotoUrl] = useState('') - const [avatarLoading, setAvatarLoading] = useState(false) const [question, setQuestion] = useState( 'Will I find a partner in the next 3 months?' ) @@ -40,30 +36,7 @@ export default function CreateDateDocPage() { const birthdayTime = birthday ? dayjs(birthday).valueOf() : undefined const isValid = - user && - birthday && - photoUrl && - editor && - editor.isEmpty === false && - question - - const fileHandler = async (event: any) => { - if (!user) return - - const file = event.target.files[0] - - setAvatarLoading(true) - - await uploadImage(user.username, file) - .then(async (url) => { - setPhotoUrl(url) - setAvatarLoading(false) - }) - .catch(() => { - setAvatarLoading(false) - setPhotoUrl('') - }) - } + user && birthday && editor && editor.isEmpty === false && question async function saveDateDoc() { if (!user || !editor || !birthdayTime) return @@ -76,7 +49,6 @@ export default function CreateDateDocPage() { content: editor.getJSON(), bounty: 0, birthday: birthdayTime, - photoUrl, type: 'date-doc', question, } @@ -122,33 +94,6 @@ export default function CreateDateDocPage() { /> </Col> - <Col className="gap-4"> - <div className="">Photo</div> - <Row className="items-center gap-4"> - {avatarLoading ? ( - <LoadingIndicator /> - ) : ( - <> - {photoUrl && ( - <img - src={photoUrl} - width={80} - height={80} - className="flex h-[80px] w-[80px] items-center justify-center rounded-lg bg-gray-400 object-cover" - /> - )} - <input - className="text-sm text-gray-500" - type="file" - name="file" - accept="image/*" - onChange={fileHandler} - /> - </> - )} - </Row> - </Col> - <Col className="gap-4"> <div className=""> Tell us about you! What are you looking for? From a7abdbb1dbf934b0893ff679f02044a3f35ecc30 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 27 Sep 2022 19:10:35 -0400 Subject: [PATCH 12/13] Add to dating group --- functions/src/create-post.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index 675ce3e1..e9d6ae8f 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -75,6 +75,8 @@ export const createpost = newEndpoint({}, async (req, auth) => { outcomeType: 'BINARY', visibility: 'unlisted', initialProb: 50, + // Dating group! + groupId: 'j3ZE8fkeqiKmRGumy3O1', }, auth ) From 9dc0d1696e2b15ada208f5aa49e67add42ed1081 Mon Sep 17 00:00:00 2001 From: James Grugett <jahooma@gmail.com> Date: Tue, 27 Sep 2022 19:36:32 -0400 Subject: [PATCH 13/13] Fix bug --- web/pages/date-docs/[username].tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/pages/date-docs/[username].tsx b/web/pages/date-docs/[username].tsx index 2b6c4909..17a41445 100644 --- a/web/pages/date-docs/[username].tsx +++ b/web/pages/date-docs/[username].tsx @@ -98,13 +98,13 @@ export function DateDocPost(props: { return ( <Col className="gap-6 rounded-lg bg-white px-6 py-6"> <SiteLink href={link ? `/date-docs/${creator.username}` : undefined}> - <Col className="gap-6 self-center"> - <Row className="relative items-center justify-between gap-4 text-2xl"> + <Col className="gap-6"> + <Row className="relative justify-between gap-4 text-2xl"> <div> {name}, {age} </div> - <Col className="absolute right-0 px-2"> + <Col className={clsx(link && 'absolute', 'right-0 px-2')}> <Button size="lg" color="gray-white"